yzt15806542928 commited on
Commit
73d9e73
·
verified ·
1 Parent(s): cfc62cb

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .deps/torch_harmonics-0.8.0.dist-info/INSTALLER +1 -0
  2. .deps/torch_harmonics-0.8.0.dist-info/METADATA +324 -0
  3. .deps/torch_harmonics-0.8.0.dist-info/RECORD +86 -0
  4. .deps/torch_harmonics-0.8.0.dist-info/REQUESTED +0 -0
  5. .deps/torch_harmonics-0.8.0.dist-info/WHEEL +5 -0
  6. .deps/torch_harmonics-0.8.0.dist-info/licenses/AUTHORS +12 -0
  7. .deps/torch_harmonics-0.8.0.dist-info/licenses/LICENSE +27 -0
  8. .deps/torch_harmonics-0.8.0.dist-info/top_level.txt +1 -0
  9. .deps/torch_harmonics/__init__.py +40 -0
  10. .deps/torch_harmonics/_disco_convolution.py +212 -0
  11. .deps/torch_harmonics/_neighborhood_attention.py +589 -0
  12. .deps/torch_harmonics/attention.py +382 -0
  13. .deps/torch_harmonics/cache.py +74 -0
  14. .deps/torch_harmonics/convolution.py +666 -0
  15. .deps/torch_harmonics/csrc/attention/attention.cuh +50 -0
  16. .deps/torch_harmonics/csrc/attention/attention_bwd_cuda.cu +960 -0
  17. .deps/torch_harmonics/csrc/attention/attention_fwd_cuda.cu +525 -0
  18. .deps/torch_harmonics/csrc/attention/attention_interface.cu +38 -0
  19. .deps/torch_harmonics/csrc/attention/attention_utils.cu +180 -0
  20. .deps/torch_harmonics/csrc/attention/attention_utils.cuh +373 -0
  21. .deps/torch_harmonics/csrc/attention/cudamacro.h +47 -0
  22. .deps/torch_harmonics/csrc/disco/disco.h +39 -0
  23. .deps/torch_harmonics/csrc/disco/disco_cuda.cuh +54 -0
  24. .deps/torch_harmonics/csrc/disco/disco_cuda_bwd.cu +281 -0
  25. .deps/torch_harmonics/csrc/disco/disco_cuda_fwd.cu +265 -0
  26. .deps/torch_harmonics/csrc/disco/disco_helpers.cpp +149 -0
  27. .deps/torch_harmonics/csrc/disco/disco_interface.cu +38 -0
  28. .deps/torch_harmonics/distributed/__init__.py +58 -0
  29. .deps/torch_harmonics/distributed/distributed_convolution.py +474 -0
  30. .deps/torch_harmonics/distributed/distributed_resample.py +247 -0
  31. .deps/torch_harmonics/distributed/distributed_sht.py +612 -0
  32. .deps/torch_harmonics/distributed/primitives.py +470 -0
  33. .deps/torch_harmonics/distributed/utils.py +92 -0
  34. .deps/torch_harmonics/examples/__init__.py +36 -0
  35. .deps/torch_harmonics/examples/losses.py +417 -0
  36. .deps/torch_harmonics/examples/metrics.py +291 -0
  37. .deps/torch_harmonics/examples/models/__init__.py +36 -0
  38. .deps/torch_harmonics/examples/models/_layers.py +621 -0
  39. .deps/torch_harmonics/examples/models/lsno.py +623 -0
  40. .deps/torch_harmonics/examples/models/s2segformer.py +810 -0
  41. .deps/torch_harmonics/examples/models/s2transformer.py +589 -0
  42. .deps/torch_harmonics/examples/models/s2unet.py +610 -0
  43. .deps/torch_harmonics/examples/models/sfno.py +423 -0
  44. .deps/torch_harmonics/examples/pde_dataset.py +159 -0
  45. .deps/torch_harmonics/examples/pde_sphere.py +227 -0
  46. .deps/torch_harmonics/examples/shallow_water_equations.py +394 -0
  47. .deps/torch_harmonics/examples/stanford_2d3ds_dataset.py +777 -0
  48. .deps/torch_harmonics/filter_basis.py +326 -0
  49. .deps/torch_harmonics/legendre.py +215 -0
  50. .deps/torch_harmonics/plotting.py +265 -0
.deps/torch_harmonics-0.8.0.dist-info/INSTALLER ADDED
@@ -0,0 +1 @@
 
 
1
+ pip
.deps/torch_harmonics-0.8.0.dist-info/METADATA ADDED
@@ -0,0 +1,324 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Metadata-Version: 2.4
2
+ Name: torch_harmonics
3
+ Version: 0.8.0
4
+ Summary: Differentiable signal processing on the sphere for PyTorch.
5
+ Author: Boris Bonev, Thorsten Kurth, Max Rietmann, Mauro Bisson, Andrea Paris, Alberto Carpentieri, Massimiliano Fatica, Jean Kossaifi, Nikola Kovachki, Christian Hundt
6
+ Maintainer-email: Boris Bonev <bbonev@nvidia.com>, Thorsten Kurth <tkurth@nvidia.com>
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Programming Language :: Python :: 3.9
9
+ Classifier: License :: OSI Approved :: BSD License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ License-File: AUTHORS
15
+ Requires-Dist: torch>=2.4.0
16
+ Requires-Dist: numpy>=1.22.4
17
+ Provides-Extra: dev
18
+ Requires-Dist: pytest>=6.0.0; extra == "dev"
19
+ Requires-Dist: coverage>=6.5.0; extra == "dev"
20
+ Provides-Extra: 2d3ds
21
+ Requires-Dist: requests; extra == "2d3ds"
22
+ Requires-Dist: tarfile; extra == "2d3ds"
23
+ Requires-Dist: tqdm; extra == "2d3ds"
24
+ Requires-Dist: PIL; extra == "2d3ds"
25
+ Requires-Dist: h5py; extra == "2d3ds"
26
+ Dynamic: license-file
27
+
28
+ <!--
29
+ SPDX-FileCopyrightText: Copyright (c) 2022 The torch-harmonics Authors. All rights reserved.
30
+
31
+ SPDX-License-Identifier: BSD-3-Clause
32
+
33
+ Redistribution and use in source and binary forms, with or without
34
+ modification, are permitted provided that the following conditions are met:
35
+
36
+ 1. Redistributions of source code must retain the above copyright notice, this
37
+ list of conditions and the following disclaimer.
38
+ 2. Redistributions in binary form must reproduce the above copyright notice,
39
+ this list of conditions and the following disclaimer in the documentation
40
+ and/or other materials provided with the distribution.
41
+
42
+ 3. Neither the name of the copyright holder nor the names of its
43
+ contributors may be used to endorse or promote products derived from
44
+ this software without specific prior written permission.
45
+
46
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
47
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
48
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
49
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
50
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
51
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
52
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
53
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
54
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
55
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
56
+ -->
57
+
58
+ <!-- <div align="center">
59
+ <img src="https://raw.githubusercontent.com/NVIDIA/torch-harmonics/main/images/logo/logo.png" width="568">
60
+ <br>
61
+ <a href="https://github.com/NVIDIA/torch-harmonics/actions/workflows/tests.yml"><img src="https://github.com/NVIDIA/torch-harmonics/actions/workflows/tests.yml/badge.svg"></a>
62
+ <a href="https://pypi.org/project/torch_harmonics/"><img src="https://img.shields.io/pypi/v/torch_harmonics"></a>
63
+ </div> -->
64
+
65
+ <!--
66
+ [![pypi](https://img.shields.io/pypi/v/torch_harmonics)](https://pypi.org/project/torch_harmonics/)
67
+ -->
68
+
69
+ <!-- # spherical harmonic transforms -->
70
+
71
+ # torch-harmonics
72
+
73
+ [**Overview**](#overview) | [**Installation**](#installation) | [**More information**](#more-about-torch-harmonics) | [**Getting started**](#getting-started) | [**Contributors**](#contributors) | [**Cite us**](#cite-us) | [**References**](#references)
74
+
75
+ [![tests](https://github.com/NVIDIA/torch-harmonics/actions/workflows/tests.yml/badge.svg)](https://github.com/NVIDIA/torch-harmonics/actions/workflows/tests.yml)
76
+ [![pypi](https://img.shields.io/pypi/v/torch_harmonics)](https://pypi.org/project/torch_harmonics/)
77
+
78
+ ## Overview
79
+
80
+ torch-harmonics implements differentiable signal processing on the sphere. This includes differentiable implementations of the spherical harmonic transforms, vector spherical harmonic transforms and discrete-continuous convolutions on the sphere. The package was originally implemented to enable Spherical Fourier Neural Operators (SFNO) [1].
81
+
82
+ The SHT algorithm uses quadrature rules to compute the projection onto the associated Legendre polynomials and FFTs for the projection onto the harmonic basis. This algorithm tends to outperform others with better asymptotic scaling for most practical purposes [2].
83
+
84
+ torch-harmonics uses PyTorch primitives to implement these operations, making it fully differentiable. Moreover, the quadrature can be distributed onto multiple ranks making it spatially distributed.
85
+
86
+ torch-harmonics has been used to implement a variety of differentiable PDE solvers which generated the animations below. Moreover, it has enabled the development of Spherical Fourier Neural Operators [1].
87
+
88
+ <div align="center">
89
+ <table border="0" cellspacing="0" cellpadding="0">
90
+ <tr>
91
+ <td><img src="https://media.githubusercontent.com/media/NVIDIA/torch-harmonics/main/images/sfno.gif" width="240"></td>
92
+ <td><img src="https://media.githubusercontent.com/media/NVIDIA/torch-harmonics/main/images/zonal_jet.gif" width="240"></td>
93
+ <td><img src="https://media.githubusercontent.com/media/NVIDIA/torch-harmonics/main/images/allen-cahn.gif" width="240"></td>
94
+ </tr>
95
+ <!-- <tr>
96
+ <td style="text-align:center; border-style : hidden!important;">Shallow Water Eqns.</td>
97
+ <td style="text-align:center; border-style : hidden!important;">Ginzburg-Landau Eqn.</td>
98
+ <td style="text-align:center; border-style : hidden!important;">Allen-Cahn Eqn.</td>
99
+ </tr> -->
100
+ </table>
101
+ </div>
102
+
103
+
104
+ ## Installation
105
+ A simple installation can be directly done from PyPI:
106
+
107
+ ```bash
108
+ pip install torch-harmonics
109
+ ```
110
+ If you are planning to use spherical convolutions, we recommend building the corresponding custom CUDA kernels. To enforce this, you can set the `FORCE_CUDA_EXTENSION` flag. You may also want to set appropriate architectures with the `TORCH_CUDA_ARCH_LIST` flag. Finally, make sure to disable build isolation via the `--no-build-isolation` flag to ensure that the custom kernels are built with the existing torch installation.
111
+ ```bash
112
+ export FORCE_CUDA_EXTENSION=1
113
+ export TORCH_CUDA_ARCH_LIST="7.0 7.2 7.5 8.0 8.6 8.7 9.0+PTX"
114
+ pip install --no-build-isolation torch-harmonics
115
+ ```
116
+ :warning: Please note that the custom CUDA extensions currently only support CUDA architectures >= 7.0.
117
+
118
+ If you want to actively develop torch-harmonics, we recommend building it in your environment from github:
119
+
120
+ ```bash
121
+ git clone git@github.com:NVIDIA/torch-harmonics.git
122
+ cd torch-harmonics
123
+ pip install -e .
124
+ ```
125
+
126
+ Alternatively, use the Dockerfile to build your custom container after cloning:
127
+
128
+ ```bash
129
+ git clone git@github.com:NVIDIA/torch-harmonics.git
130
+ cd torch-harmonics
131
+ docker build . -t torch_harmonics
132
+ docker run --gpus all -it --rm --ipc=host --ulimit memlock=-1 --ulimit stack=67108864 torch_harmonics
133
+ ```
134
+
135
+ ## More about torch-harmonics
136
+
137
+ ### Spherical harmonics
138
+
139
+ The [spherical harmonics](https://en.wikipedia.org/wiki/Spherical_harmonics) are special functions defined on the two-dimensional sphere $S^2$ (embedded in three dimensions). They form an orthonormal basis of the space of square-integrable functions defined on the sphere $L^2(S^2)$ and are comparable to the harmonic functions defined on a circle/torus. The spherical harmonics are defined as
140
+
141
+ $$
142
+ Y_l^m(\theta, \lambda) = \sqrt{\frac{(2l + 1)}{4 \pi} \frac{(l - m)!}{(l + m)!}} P_l^m(\cos \theta) \exp(im\lambda),
143
+ $$
144
+
145
+ where $\theta$ and $\lambda$ are colatitude and longitude respectively, and $P_l^m$ the normalized, [associated Legendre polynomials](https://en.wikipedia.org/wiki/Associated_Legendre_polynomials).
146
+
147
+ <div align="center">
148
+ <img src="https://media.githubusercontent.com/media/NVIDIA/torch-harmonics/main/images/spherical_harmonics.gif" width="432">
149
+ <br>
150
+ Spherical harmonics up to degree 5
151
+ </div>
152
+
153
+ ### Spherical harmonic transform
154
+
155
+ The spherical harmonic transform (SHT)
156
+
157
+ $$
158
+ f_l^m = \int_{S^2} \overline{Y_{l}^{m}}(\theta, \lambda) f(\theta, \lambda) \mathrm{d} \mu(\theta, \lambda)
159
+ $$
160
+
161
+ realizes the projection of a signal $f(\theta, \lambda)$ on $S^2$ onto the spherical harmonics basis. The SHT generalizes the Fourier transform on the sphere. Conversely, a truncated series expansion of a function $f$ can be written in terms of spherical harmonics as
162
+
163
+ $$
164
+ f (\theta, \lambda) = \sum_{m=-M}^{M} \exp(im\lambda) \sum_{l=|m|}^{M} \hat f_l^m P_l^m (\cos \theta),
165
+ $$
166
+
167
+ where $\hat{f}_l^m$, are the expansion coefficients associated to the mode $m$, $n$.
168
+
169
+ The implementation of the SHT follows the algorithm as presented in [2]. A direct spherical harmonic transform can be accomplished by a Fourier transform
170
+
171
+ $$
172
+ \hat f^m(\theta) = \frac{1}{2 \pi} \int_{0}^{2\pi} f(\theta, \lambda) \exp(-im\lambda) \mathrm{d} \lambda
173
+ $$
174
+
175
+ in longitude and a Legendre transform
176
+
177
+ $$
178
+ \hat f_l^m = \frac{1}{2} \int^{\pi}_0 \hat f^{m} (\theta) P_l^m (\cos \theta) \sin \theta \mathrm{d} \theta
179
+ $$
180
+
181
+ in latitude.
182
+
183
+ ### Discrete Legendre transform
184
+
185
+ The second integral, which computed the projection onto the Legendre polynomials is realized with quadrature. On the Gaussian grid, we use Gaussian quadrature in the $\cos \theta$ domain. The integral
186
+
187
+ $$
188
+ \hat f_l^m = \frac{1}{2} \int_{-1}^1 \hat{f}^m(\arccos x) P_l^m (x) \mathrm{d} x
189
+ $$
190
+
191
+ is obtained with the substitution $x = \cos \theta$ and then approximated by the sum
192
+
193
+ $$
194
+ \hat f_l^m = \sum_{j=1}^{N_\theta} \hat{f}^m(\arccos x_j) P_l^m(x_j) w_j.
195
+ $$
196
+
197
+ Here, $x_j \in [-1,1]$ are the quadrature nodes with the respective quadrature weights $w_j$.
198
+
199
+ ### Discrete-continuous convolutions on the sphere
200
+
201
+ torch-harmonics now provides local discrete-continuous (DISCO) convolutions as outlined in [5] on the sphere. These are use in local neural operators [2] to generalize convolutions to structured and unstructured meshes on the sphere.
202
+
203
+ ### Spherical (neighborhood) attention
204
+
205
+ torch-harmonics introducers spherical attention mechanisms which correctly generalize the attention mechanism to the sphere. The use of quadrature rules makes the resulting operations approximately equivariant and equivariant in the continuous limit. Moreover, neighborhood attention is correctly generalized onto the sphere by using the geodesic distance to determine the size of the neighborhood.
206
+
207
+ ## Getting started
208
+
209
+ The main functionality of `torch_harmonics` is provided in the form of `torch.nn.Modules` for composability. A minimum example is given by:
210
+
211
+ ```python
212
+ import torch
213
+ import torch_harmonics as th
214
+
215
+ device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
216
+
217
+ nlat = 512
218
+ nlon = 2*nlat
219
+ batch_size = 32
220
+ signal = torch.randn(batch_size, nlat, nlon, device=device)
221
+
222
+ # transform data on an equiangular grid
223
+ sht = th.RealSHT(nlat, nlon, grid="equiangular").to(device)
224
+
225
+ coeffs = sht(signal)
226
+ ```
227
+
228
+ To enable scalable model-parallelism, `torch-harmonics` implements a distributed variant of the SHT located in `torch_harmonics.distributed`.
229
+
230
+ Detailed usage of torch-harmonics, alongside helpful analysis provided in a series of notebooks:
231
+
232
+ 1. [Getting started](./notebooks/getting_started.ipynb)
233
+ 2. [Quadrature](./notebooks/quadrature.ipynb)
234
+ 3. [Visualizing the spherical harmonics](./notebooks/plot_spherical_harmonics.ipynb)
235
+ 4. [Spectral fitting vs. SHT](./notebooks/gradient_analysis.ipynb)
236
+ 5. [Conditioning of the Gramian](./notebooks/conditioning_sht.ipynb)
237
+ 6. [Solving the Helmholtz equation](./notebooks/helmholtz.ipynb)
238
+ 7. [Solving the shallow water equations](./notebooks/shallow_water_equations.ipynb)
239
+ 8. [Training Spherical Fourier Neural Operators (SFNO)](./notebooks/train_sfno.ipynb)
240
+ 9. [Resampling signals on the sphere](./notebooks/resample_sphere.ipynb)
241
+
242
+ ## Examples and reproducibility
243
+
244
+ The `examples` folder contains training scripts for three distinct tasks:
245
+
246
+ * [solution of the shallow water equations on the rotating sphere](./examples/shallow_water_equations/train.py)
247
+ * [depth estimation on the sphere](./examples/depth/train.py)
248
+ * [semantic segmentation on the sphere](./examples/segmentation/train.py)
249
+
250
+ Results from the papers can generally be reproduced by running `python train.py`. In the case of some older results the number of epochs and learning-rate may need to be adjusted by passing the corresponding command line argument.
251
+
252
+ ## Remarks on automatic mixed precision (AMP) support
253
+
254
+ Note that torch-harmonics uses Fourier transforms from `torch.fft` which in turn uses kernels from the optimized `cuFFT` library. This library supports fourier transforms of `float32` and `float64` (i.e. `single` and `double` precision) tensors for all input sizes. For `float16` (i.e. `half` precision) and `bfloat16` inputs however, the dimensions which are transformed are restricted to powers of two. Since data is converted to one of these reduced precision floating point formats when `torch.autocast` is used, torch-harmonics will issue an error when the input shapes are not powers of two. For these cases, we recommend disabling autocast for the harmonics transform specifically:
255
+
256
+ ```python
257
+ import torch
258
+ import torch_harmonics as th
259
+
260
+ sht = th.RealSHT(512, 1024, grid="equiangular").cuda()
261
+
262
+ with torch.autocast(device_type="cuda", enabled = True):
263
+ # do some AMP converted math here
264
+ x = some_math(x)
265
+ # convert tensor to float32
266
+ x = x.to(torch.float32)
267
+ # now disable autocast specifically for the transform,
268
+ # making sure that the tensors are not converted
269
+ # back to reduced precision internally
270
+ with torch.autocast(device_type="cuda", enabled = False):
271
+ xt = sht(x)
272
+
273
+ # continue operating on the transformed tensor
274
+ xt = some_more_math(xt)
275
+ ```
276
+
277
+ Depending on the problem, it might be beneficial to upcast data to `float64` instead of `float32` precision for numerical stability.
278
+
279
+ ## Contributors
280
+
281
+ [Boris Bonev](https://bonevbs.github.io) (bbonev@nvidia.com), [Thorsten Kurth](https://github.com/azrael417) (tkurth@nvidia.com), [Max Rietmann](https://github.com/rietmann-nv), [Mauro Bisson](https://scholar.google.com/citations?hl=en&user=f0JE-0gAAAAJ), [Andrea Paris](https://github.com/apaaris), [Alberto Carpentieri](https://github.com/albertocarpentieri), [Massimiliano Fatica](https://scholar.google.com/citations?user=Deaq4uUAAAAJ&hl=en), [Nikola Kovachki](https://kovachki.github.io), [Jean Kossaifi](http://jeankossaifi.com), [Christian Hundt](https://github.com/gravitino)
282
+
283
+ ## Cite us
284
+
285
+ If you use `torch-harmonics` in an academic paper, please cite [1]
286
+
287
+ ```bibtex
288
+ @misc{bonev2023spherical,
289
+ title={Spherical Fourier Neural Operators: Learning Stable Dynamics on the Sphere},
290
+ author={Boris Bonev and Thorsten Kurth and Christian Hundt and Jaideep Pathak and Maximilian Baust and Karthik Kashinath and Anima Anandkumar},
291
+ year={2023},
292
+ eprint={2306.03838},
293
+ archivePrefix={arXiv},
294
+ primaryClass={cs.LG}
295
+ }
296
+ ```
297
+
298
+ ## References
299
+
300
+ <a id="1">[1]</a>
301
+ Bonev B., Kurth T., Hundt C., Pathak, J., Baust M., Kashinath K., Anandkumar A.;
302
+ Spherical Fourier Neural Operators: Learning Stable Dynamics on the Sphere;
303
+ International Conference on Machine Learning, 2023. [arxiv link](https://arxiv.org/abs/2306.03838)
304
+
305
+ <a id="1">[2]</a>
306
+ Liu-Schiaffini M., Berner J., Bonev B., Kurth T., Azizzadenesheli K., Anandkumar A.;
307
+ Neural Operators with Localized Integral and Differential Kernels;
308
+ International Conference on Machine Learning, 2024. [arxiv link](https://arxiv.org/abs/2402.16845)
309
+
310
+ <a id="1">[3]</a>
311
+ Schaeffer N.;
312
+ Efficient spherical harmonic transforms aimed at pseudospectral numerical simulations;
313
+ G3: Geochemistry, Geophysics, Geosystems, 2013.
314
+
315
+ <a id="1">[4]</a>
316
+ Wang B., Wang L., Xie Z.;
317
+ Accurate calculation of spherical and vector spherical harmonic expansions via spectral element grids;
318
+ Adv Comput Math, 2018.
319
+
320
+ <a id="1">[5]</a>
321
+ Ocampo, Price, McEwen, Scalable and equivariant spherical CNNs by discrete-continuous (DISCO) convolutions, ICLR (2023), arXiv:2209.13603
322
+
323
+ <a id="1">[6]</a>
324
+ Bonev B., Rietmann M., Paris A., Carpentieri A., Kurth T.; Attention on the Sphere; [arxiv link](https://arxiv.org/abs/2505.11157)
.deps/torch_harmonics-0.8.0.dist-info/RECORD ADDED
@@ -0,0 +1,86 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ torch_harmonics-0.8.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
2
+ torch_harmonics-0.8.0.dist-info/METADATA,sha256=R5upGTlvdXzHVZsns-Tvrf-DYjeVrKfZAEMbmcXwEGU,16077
3
+ torch_harmonics-0.8.0.dist-info/RECORD,,
4
+ torch_harmonics-0.8.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ torch_harmonics-0.8.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
6
+ torch_harmonics-0.8.0.dist-info/licenses/AUTHORS,sha256=Lee9XR9hIxhBwbjp8RS0dZMwPDqtfoWnxxAGe1zLR3o,409
7
+ torch_harmonics-0.8.0.dist-info/licenses/LICENSE,sha256=vexnFPd7j3gnhT4EuEB5Fhgbpm_5DmSY5g5y2lDSXUI,1576
8
+ torch_harmonics-0.8.0.dist-info/top_level.txt,sha256=fPyCWy2b6BcyqFoikrfbgESNiG0oABgOFU2r5hgFhgw,16
9
+ torch_harmonics/__init__.py,sha256=MW9sMszgWYufNlBaHgkmCc6bicejzRsaF_W8F3WAsQw,1983
10
+ torch_harmonics/__pycache__/__init__.cpython-311.pyc,,
11
+ torch_harmonics/__pycache__/_disco_convolution.cpython-311.pyc,,
12
+ torch_harmonics/__pycache__/_neighborhood_attention.cpython-311.pyc,,
13
+ torch_harmonics/__pycache__/attention.cpython-311.pyc,,
14
+ torch_harmonics/__pycache__/cache.cpython-311.pyc,,
15
+ torch_harmonics/__pycache__/convolution.cpython-311.pyc,,
16
+ torch_harmonics/__pycache__/filter_basis.cpython-311.pyc,,
17
+ torch_harmonics/__pycache__/legendre.cpython-311.pyc,,
18
+ torch_harmonics/__pycache__/plotting.cpython-311.pyc,,
19
+ torch_harmonics/__pycache__/quadrature.cpython-311.pyc,,
20
+ torch_harmonics/__pycache__/random_fields.cpython-311.pyc,,
21
+ torch_harmonics/__pycache__/resample.cpython-311.pyc,,
22
+ torch_harmonics/__pycache__/sht.cpython-311.pyc,,
23
+ torch_harmonics/_disco_convolution.py,sha256=znDWSirhJpBWX1yNJH6Z9vP81FmEBhXHcRuhNW7dxN0,9744
24
+ torch_harmonics/_neighborhood_attention.py,sha256=eglziOF2yfpt4-1du2hFOqJ2ZaVHkwWVEhuhyUIdZuc,24120
25
+ torch_harmonics/attention.py,sha256=OmuY8HKpcMbLbzzGBP9dtvT0uBNTkFBeHLJg94tNSok,16296
26
+ torch_harmonics/cache.py,sha256=2W_ug1_skw4BP880CazlELzpx2ASxE_bopWHJ1uKjts,3023
27
+ torch_harmonics/convolution.py,sha256=zr6dL8SKnmygU-h3rnX79PK5scK40t8C6yadltMj6n4,24911
28
+ torch_harmonics/csrc/attention/attention.cuh,sha256=a4B-Ejs-kXMuAPnrmS9Mujy-m5Z48yj9BlkEOXNMIRw,2959
29
+ torch_harmonics/csrc/attention/attention_bwd_cuda.cu,sha256=V8Y-7s6aHPfB0Wklks_BEUQ9iYo4EDMuwzPDJ2cxpac,40904
30
+ torch_harmonics/csrc/attention/attention_fwd_cuda.cu,sha256=MnDys8h_ZgSEDbrb-QvcnHL3RTOA6NYp-gdnNuFuevk,21851
31
+ torch_harmonics/csrc/attention/attention_interface.cu,sha256=lFaHi8u5spNwt_3f-NclhcQKEaoKNUGXEG3ikrL22p4,1944
32
+ torch_harmonics/csrc/attention/attention_utils.cu,sha256=5kLzIlUfr45yb9bo5Xac6f1NbJKBrT7BMoxFHYNmIB8,7036
33
+ torch_harmonics/csrc/attention/attention_utils.cuh,sha256=1o9FshZSYbwCFPiFR1InHUgFPZ5hJJg2JpJ8n9mgyiY,11771
34
+ torch_harmonics/csrc/attention/cudamacro.h,sha256=rcc3li9Z2Fy4eOxPm0vRITzsHUsziaNtBX2mXnAJ06g,2633
35
+ torch_harmonics/csrc/disco/disco.h,sha256=_fu-JrHMJTC4KnMQ1hj9dcnWIXSbXaiFiPbKIJoIJFo,1907
36
+ torch_harmonics/csrc/disco/disco_cuda.cuh,sha256=VEowGAdWhaHUx4UeKVC1714sXPGKpcFKBXvsEnUrlXE,2699
37
+ torch_harmonics/csrc/disco/disco_cuda_bwd.cu,sha256=18UGV2g6ORO-3aW_h_1SgeKwsqeMig4I9P8p5fqmD1E,12598
38
+ torch_harmonics/csrc/disco/disco_cuda_fwd.cu,sha256=vFWpIR-GOvJjTzHBSuvU4N97JfmIjyENcbQuGlFq5iU,11588
39
+ torch_harmonics/csrc/disco/disco_helpers.cpp,sha256=uLG4wYUPNFCHeGlRLWYhS-73yt0iayq0DYiiIDadSyM,5310
40
+ torch_harmonics/csrc/disco/disco_interface.cu,sha256=57xKiwItqwA1pEsM-VglLGb-_rAsI1JHs2nvHw4B7e4,1875
41
+ torch_harmonics/distributed/__init__.py,sha256=tusU6vOpKMwSV8OkIEqAL4jfUcuzceYhkh3wV0jjsgg,2693
42
+ torch_harmonics/distributed/__pycache__/__init__.cpython-311.pyc,,
43
+ torch_harmonics/distributed/__pycache__/distributed_convolution.cpython-311.pyc,,
44
+ torch_harmonics/distributed/__pycache__/distributed_resample.cpython-311.pyc,,
45
+ torch_harmonics/distributed/__pycache__/distributed_sht.cpython-311.pyc,,
46
+ torch_harmonics/distributed/__pycache__/primitives.cpython-311.pyc,,
47
+ torch_harmonics/distributed/__pycache__/utils.cpython-311.pyc,,
48
+ torch_harmonics/distributed/distributed_convolution.py,sha256=4feNPcchZ8BglBLr6nnJbZ39TEiZ_WHpacPzBiiAs2M,19651
49
+ torch_harmonics/distributed/distributed_resample.py,sha256=1fSAveUH1YOzVWgPYSaV__UD6OV3EEjXrmanWUsX5wY,11138
50
+ torch_harmonics/distributed/distributed_sht.py,sha256=J7m9fcxhSW3YE51MrR7Ej6TUY0kr0YPYL6IqMzFvbGg,24988
51
+ torch_harmonics/distributed/primitives.py,sha256=TlBJSgEGPCuzhIrLb_ChBbmz_I1xos7va-UJwetFtGc,15052
52
+ torch_harmonics/distributed/utils.py,sha256=orUloyD9taV11TkRjtOLuJCpI49ObeybjvqYgxYRpGg,3301
53
+ torch_harmonics/examples/__init__.py,sha256=FgSFWGxp4m0oPdzyJYS5D6FO7AaXlX6bp1NbJd6P6FQ,1910
54
+ torch_harmonics/examples/__pycache__/__init__.cpython-311.pyc,,
55
+ torch_harmonics/examples/__pycache__/losses.cpython-311.pyc,,
56
+ torch_harmonics/examples/__pycache__/pde_dataset.cpython-311.pyc,,
57
+ torch_harmonics/examples/__pycache__/pde_sphere.cpython-311.pyc,,
58
+ torch_harmonics/examples/__pycache__/shallow_water_equations.cpython-311.pyc,,
59
+ torch_harmonics/examples/__pycache__/stanford_2d3ds_dataset.cpython-311.pyc,,
60
+ torch_harmonics/examples/losses.py,sha256=MST2ImGVjvu9cfueURM3GOIO44MY29tWDE_PxlPVD9s,15480
61
+ torch_harmonics/examples/metrics.py,sha256=QOHU2cfY7oDmsnTNhgRWfODAIu95_w6IUPf_OZ3wqmo,10914
62
+ torch_harmonics/examples/models/__init__.py,sha256=ovMQpqcnFo1HG3uwiXS0EoErOSRbENazuDrOjkcVzPg,1850
63
+ torch_harmonics/examples/models/__pycache__/__init__.cpython-311.pyc,,
64
+ torch_harmonics/examples/models/__pycache__/_layers.cpython-311.pyc,,
65
+ torch_harmonics/examples/models/__pycache__/lsno.cpython-311.pyc,,
66
+ torch_harmonics/examples/models/__pycache__/s2segformer.cpython-311.pyc,,
67
+ torch_harmonics/examples/models/__pycache__/s2transformer.cpython-311.pyc,,
68
+ torch_harmonics/examples/models/__pycache__/s2unet.cpython-311.pyc,,
69
+ torch_harmonics/examples/models/__pycache__/sfno.cpython-311.pyc,,
70
+ torch_harmonics/examples/models/_layers.py,sha256=2B5WqBGmQ3YWk9i7CngB9auqErmh4utbX79uyQX8I2o,22696
71
+ torch_harmonics/examples/models/lsno.py,sha256=K8TA7Jvnt13OszNdCpcnh9NtIRNSIvGuobvgwi50huA,22295
72
+ torch_harmonics/examples/models/s2segformer.py,sha256=AKhEHH6Pbw3hjFPWpp77ni5NB_W2qQc5YSsiFQj3R2M,27198
73
+ torch_harmonics/examples/models/s2transformer.py,sha256=FzxsB2Z8JDpzdlee9zw4XWYaBZMoufNYPb_AQ9yrf5Q,20331
74
+ torch_harmonics/examples/models/s2unet.py,sha256=s_oWn1bUoWswRTWBTOMM9hKLLSpwxhd6K5uIBU4yGNI,21564
75
+ torch_harmonics/examples/models/sfno.py,sha256=eRYTaUA6VoFtzAWdafIY7C6GoyjEdsCLDJSH64QptgI,16005
76
+ torch_harmonics/examples/pde_dataset.py,sha256=mvY6FBcY7PSd7eLRGr2bmXbz7JPVXeTO2X5CnNpClHk,5564
77
+ torch_harmonics/examples/pde_sphere.py,sha256=cupdwimLpVco0bghk4VWaI3bbY4oV8eOIxfnUdYshcg,8256
78
+ torch_harmonics/examples/shallow_water_equations.py,sha256=yfZUw_Fnn-N40vvom6lYNv_Nr1lSQGr0yH9eLbc5JhA,16103
79
+ torch_harmonics/examples/stanford_2d3ds_dataset.py,sha256=WMvjEGxn7Vr697n3iBdOhmG5ox-xKqomwMbPmG8pQ3w,30650
80
+ torch_harmonics/filter_basis.py,sha256=7104x7Fvof_G9FjwJJdhPk0BbR3mpPp-lVVspVqtrjM,13140
81
+ torch_harmonics/legendre.py,sha256=wDog2ViC3JIU-bQqFzRXCbWitTD9Xoh7Riu99wmNA6k,9008
82
+ torch_harmonics/plotting.py,sha256=7vYee0mv1lVa0ztl1uUNYmjpVNggTWHC-9ZqR5q3uWA,8822
83
+ torch_harmonics/quadrature.py,sha256=a7emSieECNDL6e8StLxorZYTXSPOkidHKSuz5JaoL1s,10536
84
+ torch_harmonics/random_fields.py,sha256=yGOfM24RUdHWMFCwjmfG2Tsxa6CftdIiXNgQYl08y-E,4399
85
+ torch_harmonics/resample.py,sha256=lNX-6F0XFMkSdOqJaTiO6bUJTF1d-0b9m9cHz_CSIcg,8409
86
+ torch_harmonics/sht.py,sha256=ig1J0qpHw-6nlcdkVIo3-UdptU_BkwZGqqtPJnXqcVQ,19118
.deps/torch_harmonics-0.8.0.dist-info/REQUESTED ADDED
File without changes
.deps/torch_harmonics-0.8.0.dist-info/WHEEL ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
.deps/torch_harmonics-0.8.0.dist-info/licenses/AUTHORS ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ The code was authored by the following people:
2
+
3
+ Boris Bonev - NVIDIA Corporation
4
+ Thorsten Kurth - NVIDIA Corporation
5
+ Max Rietmann - NVIDIA Corporation
6
+ Mauro Bisson - NVIDIA Corporation
7
+ Andrea Paris - NVIDIA Corporation
8
+ Alberto Carpentieri - NVIDIA Corporation
9
+ Massimiliano Fatica - NVIDIA Corporation
10
+ Jean Kossaifi - NVIDIA Corporation
11
+ Nikola Kovachki - NVIDIA Corporation
12
+ Christian Hundt - NVIDIA Corporation
.deps/torch_harmonics-0.8.0.dist-info/licenses/LICENSE ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ SPDX-FileCopyrightText: Copyright (c) 2022 The torch-harmonics Authors. All rights reserved.
2
+
3
+ SPDX-License-Identifier: BSD-3-Clause
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+ 2. Redistributions in binary form must reproduce the above copyright notice,
11
+ this list of conditions and the following disclaimer in the documentation
12
+ and/or other materials provided with the distribution.
13
+
14
+ 3. Neither the name of the copyright holder nor the names of its
15
+ contributors may be used to endorse or promote products derived from
16
+ this software without specific prior written permission.
17
+
18
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
.deps/torch_harmonics-0.8.0.dist-info/top_level.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ torch_harmonics
.deps/torch_harmonics/__init__.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2022 The torch-harmonics Authors. All rights reserved.
4
+ # SPDX-License-Identifier: BSD-3-Clause
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are met:
8
+ #
9
+ # 1. Redistributions of source code must retain the above copyright notice, this
10
+ # list of conditions and the following disclaimer.
11
+ #
12
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ # this list of conditions and the following disclaimer in the documentation
14
+ # and/or other materials provided with the distribution.
15
+ #
16
+ # 3. Neither the name of the copyright holder nor the names of its
17
+ # contributors may be used to endorse or promote products derived from
18
+ # this software without specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+ #
31
+
32
+ __version__ = "0.8.0"
33
+
34
+ from .sht import RealSHT, InverseRealSHT, RealVectorSHT, InverseRealVectorSHT
35
+ from .convolution import DiscreteContinuousConvS2, DiscreteContinuousConvTransposeS2
36
+ from .resample import ResampleS2
37
+ from .attention import AttentionS2, NeighborhoodAttentionS2
38
+ from . import quadrature
39
+ from . import random_fields
40
+ from . import examples
.deps/torch_harmonics/_disco_convolution.py ADDED
@@ -0,0 +1,212 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2022 The torch-harmonics Authors. All rights reserved.
4
+ # SPDX-License-Identifier: BSD-3-Clause
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are met:
8
+ #
9
+ # 1. Redistributions of source code must retain the above copyright notice, this
10
+ # list of conditions and the following disclaimer.
11
+ #
12
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ # this list of conditions and the following disclaimer in the documentation
14
+ # and/or other materials provided with the distribution.
15
+ #
16
+ # 3. Neither the name of the copyright holder nor the names of its
17
+ # contributors may be used to endorse or promote products derived from
18
+ # this software without specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+ #
31
+
32
+ from typing import Optional
33
+ import math
34
+
35
+ import torch
36
+ from torch.amp import custom_fwd, custom_bwd
37
+
38
+ try:
39
+ import disco_cuda_extension
40
+ except ImportError as err:
41
+ disco_cuda_extension = None
42
+
43
+ # some helper functions
44
+ def _get_psi(kernel_size: int, psi_idx: torch.Tensor, psi_vals: torch.Tensor, nlat_in: int, nlon_in: int, nlat_out: int, nlon_out: int, nlat_in_local: Optional[int] = None, nlat_out_local: Optional[int] = None, semi_transposed: Optional[bool] = False):
45
+ """Creates a sparse tensor for spherical harmonic convolution operations."""
46
+ nlat_in_local = nlat_in_local if nlat_in_local is not None else nlat_in
47
+ nlat_out_local = nlat_out_local if nlat_out_local is not None else nlat_out
48
+
49
+ if semi_transposed:
50
+ # do partial transpose
51
+ # we do a semi-transposition to faciliate the computation
52
+ tout = psi_idx[2] // nlon_out
53
+ pout = psi_idx[2] % nlon_out
54
+ # flip the axis of longitudes
55
+ pout = nlon_out - 1 - pout
56
+ tin = psi_idx[1]
57
+ idx = torch.stack([psi_idx[0], tout, tin * nlon_out + pout], dim=0)
58
+ psi = torch.sparse_coo_tensor(idx, psi_vals, size=(kernel_size, nlat_out_local, nlat_in_local * nlon_out)).coalesce()
59
+ else:
60
+ psi = torch.sparse_coo_tensor(psi_idx, psi_vals, size=(kernel_size, nlat_out_local, nlat_in_local * nlon_in)).coalesce()
61
+ return psi
62
+
63
+
64
+ class _DiscoS2ContractionCuda(torch.autograd.Function):
65
+ @staticmethod
66
+ @custom_fwd(device_type="cuda")
67
+ def forward(ctx, x: torch.Tensor, roff_idx: torch.Tensor, ker_idx: torch.Tensor,
68
+ row_idx: torch.Tensor, col_idx: torch.Tensor, vals: torch.Tensor,
69
+ kernel_size: int, nlat_out: int, nlon_out: int):
70
+
71
+ ctx.save_for_backward(roff_idx, ker_idx, row_idx, col_idx, vals)
72
+ ctx.kernel_size = kernel_size
73
+ ctx.nlat_in = x.shape[-2]
74
+ ctx.nlon_in = x.shape[-1]
75
+ xtype = x.dtype
76
+ x = x.to(torch.float32).contiguous()
77
+ output = disco_cuda_extension.forward(x, roff_idx, ker_idx, row_idx, col_idx, vals, kernel_size, nlat_out, nlon_out)
78
+ output = output.to(xtype)
79
+
80
+ return output
81
+
82
+ @staticmethod
83
+ @custom_bwd(device_type="cuda")
84
+ def backward(ctx, grad_output):
85
+
86
+ roff_idx, ker_idx, row_idx, col_idx, vals = ctx.saved_tensors
87
+ gtype = grad_output.dtype
88
+ grad_output = grad_output.to(torch.float32).contiguous()
89
+ grad_input = disco_cuda_extension.backward(grad_output, roff_idx, ker_idx, row_idx, col_idx, vals,
90
+ ctx.kernel_size, ctx.nlat_in, ctx.nlon_in)
91
+ grad_input = grad_input.to(gtype)
92
+
93
+ return grad_input, None, None, None, None, None, None, None, None
94
+
95
+
96
+ class _DiscoS2TransposeContractionCuda(torch.autograd.Function):
97
+ @staticmethod
98
+ @custom_fwd(device_type="cuda")
99
+ def forward(ctx, x: torch.Tensor, roff_idx: torch.Tensor, ker_idx: torch.Tensor,
100
+ row_idx: torch.Tensor, col_idx: torch.Tensor, vals: torch.Tensor,
101
+ kernel_size: int, nlat_out: int, nlon_out: int):
102
+
103
+ ctx.save_for_backward(roff_idx, ker_idx, row_idx, col_idx, vals)
104
+ ctx.kernel_size = kernel_size
105
+ ctx.nlat_in = x.shape[-2]
106
+ ctx.nlon_in = x.shape[-1]
107
+ xtype = x.dtype
108
+ x = x.to(torch.float32).contiguous()
109
+ output = disco_cuda_extension.backward(x, roff_idx, ker_idx, row_idx, col_idx, vals, kernel_size, nlat_out, nlon_out)
110
+ output = output.to(xtype)
111
+
112
+ return output
113
+
114
+ @staticmethod
115
+ @custom_bwd(device_type="cuda")
116
+ def backward(ctx, grad_output):
117
+
118
+ roff_idx, ker_idx, row_idx, col_idx, vals = ctx.saved_tensors
119
+ gtype = grad_output.dtype
120
+ grad_output = grad_output.to(torch.float32).contiguous()
121
+ grad_input = disco_cuda_extension.forward(grad_output, roff_idx, ker_idx, row_idx, col_idx, vals,
122
+ ctx.kernel_size, ctx.nlat_in, ctx.nlon_in)
123
+ grad_input = grad_input.to(gtype)
124
+
125
+ return grad_input, None, None, None, None, None, None, None, None
126
+
127
+ # CUDA
128
+ def _disco_s2_contraction_cuda(x: torch.Tensor, roff_idx: torch.Tensor, ker_idx: torch.Tensor,
129
+ row_idx: torch.Tensor, col_idx: torch.Tensor, vals: torch.Tensor,
130
+ kernel_size: int, nlat_out: int, nlon_out: int) -> torch.Tensor:
131
+ return _DiscoS2ContractionCuda.apply(x, roff_idx, ker_idx, row_idx, col_idx, vals,
132
+ kernel_size, nlat_out, nlon_out)
133
+
134
+ def _disco_s2_transpose_contraction_cuda(x: torch.Tensor, roff_idx: torch.Tensor, ker_idx: torch.Tensor,
135
+ row_idx: torch.Tensor, col_idx: torch.Tensor, vals: torch.Tensor,
136
+ kernel_size: int, nlat_out: int, nlon_out: int) -> torch.Tensor:
137
+ return _DiscoS2TransposeContractionCuda.apply(x, roff_idx, ker_idx, row_idx, col_idx, vals,
138
+ kernel_size, nlat_out, nlon_out)
139
+
140
+
141
+ def _disco_s2_contraction_torch(x: torch.Tensor, psi: torch.Tensor, nlon_out: int):
142
+ """
143
+ Reference implementation of the custom contraction as described in [1]. This requires repeated
144
+ shifting of the input tensor, which can potentially be costly. For an efficient implementation
145
+ on GPU, make sure to use the custom kernel written in CUDA.
146
+ """
147
+
148
+ assert len(psi.shape) == 3
149
+ assert len(x.shape) == 4
150
+ psi = psi.to(x.device)
151
+
152
+ batch_size, n_chans, nlat_in, nlon_in = x.shape
153
+ kernel_size, nlat_out, _ = psi.shape
154
+
155
+ assert psi.shape[-1] == nlat_in * nlon_in
156
+ assert nlon_in % nlon_out == 0
157
+ assert nlon_in >= nlat_out
158
+ pscale = nlon_in // nlon_out
159
+
160
+ # add a dummy dimension for nkernel and move the batch and channel dims to the end
161
+ x = x.reshape(1, batch_size * n_chans, nlat_in, nlon_in).permute(0, 2, 3, 1)
162
+ x = x.expand(kernel_size, -1, -1, -1)
163
+
164
+ y = torch.zeros(nlon_out, kernel_size, nlat_out, batch_size * n_chans, device=x.device, dtype=x.dtype)
165
+
166
+ for pout in range(nlon_out):
167
+ # sparse contraction with psi
168
+ y[pout] = torch.bmm(psi, x.reshape(kernel_size, nlat_in * nlon_in, -1))
169
+ # we need to repeatedly roll the input tensor to faciliate the shifted multiplication
170
+ x = torch.roll(x, -pscale, dims=2)
171
+
172
+ # reshape y back to expose the correct dimensions
173
+ y = y.permute(3, 1, 2, 0).reshape(batch_size, n_chans, kernel_size, nlat_out, nlon_out)
174
+
175
+ return y
176
+
177
+
178
+ def _disco_s2_transpose_contraction_torch(x: torch.Tensor, psi: torch.Tensor, nlon_out: int):
179
+ assert len(psi.shape) == 3
180
+ assert len(x.shape) == 5
181
+ psi = psi.to(x.device)
182
+
183
+ batch_size, n_chans, kernel_size, nlat_in, nlon_in = x.shape
184
+ kernel_size, nlat_out, n_out = psi.shape
185
+
186
+ assert n_out % nlon_out == 0
187
+ assert nlon_out >= nlon_in
188
+ pscale = nlon_out // nlon_in
189
+
190
+ # interleave zeros along the longitude dimension to allow for fractional offsets to be considered
191
+ x_ext = torch.zeros(kernel_size, nlat_in, nlon_out, batch_size * n_chans, device=x.device, dtype=x.dtype)
192
+ x = x.reshape(batch_size * n_chans, kernel_size, nlat_in, nlon_in).permute(1, 2, 3, 0)
193
+
194
+ # x has shape kernel_size x nlat_in x nlon_in x batch_size * n_chans
195
+ # we only need to apoply the nlon stride here, since nlat stride is taken care of by the kernel
196
+ x_ext[:, :, ::pscale, :] = x[...]
197
+
198
+ # create output tensor
199
+ y = torch.zeros(kernel_size, nlon_out, nlat_out, batch_size * n_chans, device=x.device, dtype=x.dtype)
200
+
201
+ for pout in range(nlon_out):
202
+ # we need to repeatedly roll the input tensor to faciliate the shifted multiplication
203
+ # TODO: double-check why this has to happen first
204
+ x_ext = torch.roll(x_ext, -1, dims=2)
205
+ # sparse contraction with the modified psi
206
+ y[:, pout, :, :] = torch.bmm(psi, x_ext.reshape(kernel_size, nlat_in * nlon_out, -1))
207
+
208
+ # sum over the kernel dimension and reshape to the correct output size
209
+ y = y.sum(dim=0).permute(2, 1, 0).reshape(batch_size, n_chans, nlat_out, nlon_out).contiguous()
210
+
211
+ return y
212
+
.deps/torch_harmonics/_neighborhood_attention.py ADDED
@@ -0,0 +1,589 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2025 The torch-harmonics Authors. All rights reserved.
4
+ # SPDX-License-Identifier: BSD-3-Clause
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are met:
8
+ #
9
+ # 1. Redistributions of source code must retain the above copyright notice, this
10
+ # list of conditions and the following disclaimer.
11
+ #
12
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ # this list of conditions and the following disclaimer in the documentation
14
+ # and/or other materials provided with the distribution.
15
+ #
16
+ # 3. Neither the name of the copyright holder nor the names of its
17
+ # contributors may be used to endorse or promote products derived from
18
+ # this software without specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+ #
31
+
32
+ import math
33
+ from typing import Union
34
+
35
+ import torch
36
+ import torch.nn.functional as F
37
+ from torch.amp import custom_fwd, custom_bwd
38
+
39
+ try:
40
+ import attention_cuda_extension
41
+ _cuda_extension_available = True
42
+ except ImportError as err:
43
+ attention_cuda_extension = None
44
+ _cuda_extension_available = False
45
+
46
+ # s2 neighborhood attention forward pass
47
+ # uses qdotk_max update trick to avoid two loops when computing the softmax
48
+ # see e.g., https://arxiv.org/abs/1805.02867
49
+ # and https://alexdremov.me/understanding-flash-attention-writing-the-algorithm-from-scratch-in-triton/
50
+ def _neighborhood_attention_s2_fwd_torch(kx: torch.Tensor, vx: torch.Tensor, qy: torch.Tensor,
51
+ quad_weights: torch.Tensor, col_idx: torch.Tensor, row_off: torch.Tensor,
52
+ nlon_in: int, nlat_out: int, nlon_out: int) -> torch.Tensor:
53
+ # prepare result tensor
54
+ y = torch.zeros_like(qy)
55
+
56
+ for ho in range(nlat_out):
57
+
58
+ # get number of nonzeros
59
+ zstart = row_off[ho]
60
+ zend = row_off[ho+1]
61
+
62
+ for wo in range(nlon_out):
63
+
64
+ alpha_sum = torch.zeros((y.shape[0],), dtype=y.dtype, device=y.device)
65
+ qdotk_max = torch.zeros((y.shape[0],), dtype=y.dtype, device=y.device)
66
+
67
+ for idz in range(zstart, zend):
68
+ nz_col_idx = col_idx[idz]
69
+
70
+ # compute input indices from psi datastructure
71
+ hi = nz_col_idx // nlon_in
72
+ # account for output shift and ensure positive index due to circular condition
73
+ wi = nz_col_idx % nlon_in
74
+ wip = (wi + wo) % nlon_in
75
+
76
+ # compute correlation & softmax numerator
77
+ q_ho_wo = qy[:, :, ho, wo]
78
+ k_hi_wip = kx[:, :, hi, wip]
79
+ qdotk = torch.sum(q_ho_wo * k_hi_wip, dim=1)
80
+
81
+ # tmp max
82
+ qdotk_max_tmp = torch.maximum(qdotk_max, qdotk)
83
+
84
+ # alpha sum update
85
+ alpha = torch.exp(qdotk - qdotk_max_tmp) * quad_weights[hi]
86
+ alpha_sum = alpha + alpha_sum * torch.exp(qdotk_max - qdotk_max_tmp)
87
+ # update output
88
+ y[:,:,ho,wo] = y[:,:,ho,wo] * torch.exp(qdotk_max - qdotk_max_tmp).unsqueeze(1) + alpha[:, None] * vx[:,:,hi,wip]
89
+
90
+ # define new max
91
+ qdotk_max = qdotk_max_tmp
92
+
93
+ y[:,:,ho,wo] = y[:,:,ho,wo] / alpha_sum[:, None]
94
+
95
+ return y
96
+
97
+
98
+ # Explicit gradient w.r.t. vx: dM/dv
99
+ # provided as a reference for CUDA & other hand-written gradients
100
+ def _neighborhood_attention_s2_bwd_dv_torch(kx: torch.Tensor, vx: torch.Tensor, qy: torch.Tensor, dy: torch.Tensor,
101
+ quad_weights: torch.Tensor, col_idx: torch.Tensor, row_off: torch.Tensor,
102
+ nlon_in: int, nlat_out: int, nlon_out: int):
103
+
104
+ # shapes:
105
+ # input
106
+ # kx: B, C, Hi, Wi
107
+ # vx: B, C, Hi, Wi
108
+ # qy: B, C, Ho, Wo
109
+ # quad_weights: Hi
110
+ # output
111
+ # dvx: B, C, Hi, Wi
112
+
113
+ dvx = torch.zeros_like(vx)
114
+
115
+ for ho in range(nlat_out):
116
+
117
+ # get number of nonzeros
118
+ zstart = row_off[ho]
119
+ zend = row_off[ho+1]
120
+
121
+ for wo in range(nlon_out):
122
+
123
+ alpha_nz = torch.zeros((dy.shape[0], zend-zstart), dtype=dy.dtype, device=dy.device)
124
+ qdotk_nz = torch.zeros((dy.shape[0], zend-zstart), dtype=dy.dtype, device=dy.device)
125
+ alpha_sum = torch.zeros((dy.shape[0],), dtype=dy.dtype, device=dy.device)
126
+ for idz in range(zstart, zend):
127
+ nz_col_idx = col_idx[idz]
128
+
129
+ # compute input indices from psi datastructure
130
+ hi = nz_col_idx // nlon_in
131
+ # account for output shift and ensure positive index due to circular condition
132
+ wi = nz_col_idx % nlon_in
133
+ wip = (wi+wo) % nlon_in
134
+
135
+ # compute correlation & softmax numerator
136
+ q_ho_wo = qy[:, :, ho, wo]
137
+ k_hi_wi = kx[:, :, hi, wip]
138
+ qdotk_nz[:,idz-zstart] = torch.sum(q_ho_wo * k_hi_wi, dim=1)
139
+
140
+ qdotk_max, _ = torch.max(qdotk_nz, dim=1)
141
+
142
+ for idz in range(zstart, zend):
143
+ nz_col_idx = col_idx[idz]
144
+
145
+ # compute input indices from psi datastructure
146
+ hi = nz_col_idx // nlon_in
147
+ # account for output shift and ensure positive index due to circular condition
148
+ wi = nz_col_idx % nlon_in
149
+ wip = (wi+wo) % nlon_in
150
+ alpha_nz[:,idz-zstart] = torch.exp(qdotk_nz[:,idz-zstart] - qdotk_max) * quad_weights[hi]
151
+ alpha_sum[:] += alpha_nz[:,idz-zstart]
152
+
153
+ for idz in range(zstart, zend):
154
+ nz_col_idx = col_idx[idz]
155
+
156
+ # compute input indices from psi datastructure
157
+ hi = nz_col_idx // nlon_in
158
+ # account for output shift and ensure positive index due to circular condition
159
+ wi = nz_col_idx % nlon_in
160
+ wip = (wi+wo) % nlon_in
161
+ dvx[:,:,hi, wip] += (alpha_nz[:, None, idz-zstart] / alpha_sum[:, None]) * dy[:,:,ho,wo]
162
+
163
+ return dvx
164
+
165
+
166
+ # Explicit gradient w.r.t. kx: dM/dk
167
+ # provided as a reference for CUDA & other hand-written gradients
168
+ def _neighborhood_attention_s2_bwd_dk_torch(kx: torch.Tensor, vx: torch.Tensor, qy: torch.Tensor, dy: torch.Tensor,
169
+ quad_weights: torch.Tensor, col_idx: torch.Tensor, row_off: torch.Tensor,
170
+ nlon_in: int, nlat_out: int, nlon_out: int):
171
+ # shapes:
172
+ # input
173
+ # kx: B, C, Hi, Wi
174
+ # vx: B, C, Hi, Wi
175
+ # qy: B, C, Ho, Wo
176
+ # quad_weights: Hi
177
+ # output
178
+ # dkx: B, C, Hi, Wi
179
+
180
+ dkx = torch.zeros_like(kx)
181
+
182
+ for ho in range(nlat_out):
183
+
184
+ # get number of nonzeros
185
+ zstart = row_off[ho]
186
+ zend = row_off[ho+1]
187
+
188
+ for wo in range(nlon_out):
189
+
190
+ qdotk_nz = torch.zeros((dy.shape[0], zend-zstart), dtype=dy.dtype, device=dy.device)
191
+ integral = torch.zeros((dy.shape[0],), dtype=dy.dtype, device=dy.device)
192
+ alpha = torch.zeros((dy.shape[0], zend-zstart), dtype=dy.dtype, device=dy.device)
193
+ alpha_sum = torch.zeros((dy.shape[0],), dtype=dy.dtype, device=dy.device)
194
+ for idz in range(zstart, zend):
195
+ nz_col_idx = col_idx[idz]
196
+
197
+ # compute input indices from psi datastructure
198
+ hj = nz_col_idx // nlon_in
199
+ # account for output shift and ensure positive index due to circular condition
200
+ wj = nz_col_idx % nlon_in
201
+ wjp = (wj+wo) % nlon_in
202
+
203
+ # compute correlation & softmax numerator
204
+ q_ho_wo = qy[:, :, ho, wo]
205
+ k_hj_wjp = kx[:, :, hj, wjp]
206
+ qdotk_nz[:,idz-zstart] = torch.sum(q_ho_wo * k_hj_wjp, dim=1)
207
+
208
+ qdotk_max, _ = torch.max(qdotk_nz, dim=1)
209
+
210
+ for idz in range(zstart, zend):
211
+ nz_col_idx = col_idx[idz]
212
+
213
+ # compute input indices from psi datastructure
214
+ hj = nz_col_idx // nlon_in
215
+ # account for output shift and ensure positive index due to circular condition
216
+ wj = nz_col_idx % nlon_in
217
+ wjp = (wj+wo) % nlon_in
218
+
219
+ alpha[:, idz-zstart] = torch.exp(qdotk_nz[:,idz-zstart] - qdotk_max) * quad_weights[hj]
220
+ alpha_sum[:] += alpha[:, idz-zstart]
221
+
222
+ # input dot
223
+ gdotv = torch.sum(dy[:,:,ho, wo] * vx[:,:,hj, wjp], dim=1)
224
+
225
+ # integral term
226
+ integral[:] += alpha[:, idz-zstart] * gdotv[:]
227
+
228
+ integral[:] = integral[:] / alpha_sum[:]
229
+
230
+ for idz in range(zstart, zend):
231
+ nz_col_idx = col_idx[idz]
232
+
233
+ # compute input indices from psi datastructure
234
+ hi = nz_col_idx // nlon_in
235
+ # account for output shift and ensure positive index due to circular condition
236
+ wi = nz_col_idx % nlon_in
237
+ wip = (wi+wo) % nlon_in
238
+
239
+ # compute correlation & softmax numerator
240
+ gdotv = torch.sum(dy[:,:,ho, wo] * vx[:,:,hi, wip], dim=1)
241
+
242
+ dkx[:,:,hi,wip] += qy[:, :, ho, wo] * (alpha[:, None, idz-zstart] / alpha_sum[:, None]) * (gdotv[:, None] - integral[:, None])
243
+
244
+ return dkx
245
+
246
+ # Explicit gradient w.r.t. qy: dM/dq
247
+ # provided as a reference for CUDA & other hand-written gradients
248
+ def _neighborhood_attention_s2_bwd_dq_torch(kx: torch.Tensor, vx: torch.Tensor, qy: torch.Tensor, dy: torch.Tensor,
249
+ quad_weights: torch.Tensor, col_idx: torch.Tensor, row_off: torch.Tensor,
250
+ nlon_in: int, nlat_out: int, nlon_out: int):
251
+
252
+
253
+ # shapes:
254
+ # input
255
+ # kx: B, C, Hi, Wi
256
+ # vx: B, C, Hi, Wi
257
+ # qy: B, C, Ho, Wo
258
+ # quad_weights: Hi
259
+ # output
260
+ # dvx: B, C, Hi, Wi
261
+
262
+ dqy = torch.zeros_like(qy)
263
+
264
+ for ho in range(nlat_out):
265
+
266
+ # get number of nonzeros
267
+ zstart = row_off[ho]
268
+ zend = row_off[ho+1]
269
+
270
+ for wo in range(nlon_out):
271
+
272
+ alpha = torch.zeros((dy.shape[0], zend-zstart), dtype=dy.dtype, device=dy.device)
273
+ qdotk_nz = torch.zeros((dy.shape[0], zend-zstart), dtype=dy.dtype, device=dy.device)
274
+ alpha_k = torch.zeros((dy.shape[0], dy.shape[1]), dtype=dy.dtype, device=dy.device)
275
+ alpha_vw = torch.zeros((dy.shape[0], dy.shape[1]), dtype=dy.dtype, device=dy.device)
276
+ alpha_kvw = torch.zeros((dy.shape[0], dy.shape[1]), dtype=dy.dtype, device=dy.device)
277
+ alpha_sum = torch.zeros((dy.shape[0],), dtype=dy.dtype, device=dy.device)
278
+ alpha_sum2 = torch.zeros((dy.shape[0],), dtype=dy.dtype, device=dy.device)
279
+ for idz in range(zstart, zend):
280
+ nz_col_idx = col_idx[idz]
281
+
282
+ # compute input indices from psi datastructure
283
+ hi = nz_col_idx // nlon_in
284
+ # account for output shift and ensure positive index due to circular condition
285
+ wi = nz_col_idx % nlon_in
286
+ wip = (wi+wo) % nlon_in
287
+
288
+ idz_i = idz-zstart
289
+
290
+ # compute correlation & softmax numerator
291
+ q_ho_wo = qy[:, :, ho, wo]
292
+ k_hi_wi = kx[:, :, hi, wip]
293
+ qdotk_nz[:,idz-zstart] = torch.sum(q_ho_wo * k_hi_wi, dim=1)
294
+
295
+ qdotk_max,_ = qdotk_nz.max(dim=1)
296
+
297
+ for idz in range(zstart, zend):
298
+ nz_col_idx = col_idx[idz]
299
+
300
+ # compute input indices from psi datastructure
301
+ hi = nz_col_idx // nlon_in
302
+ # account for output shift and ensure positive index due to circular condition
303
+ wi = nz_col_idx % nlon_in
304
+ wip = (wi+wo) % nlon_in
305
+
306
+ q_ho_wo = qy[:, :, ho, wo]
307
+ k_hi_wi = kx[:, :, hi, wip]
308
+ idz_i = idz-zstart
309
+ alpha[:, idz_i] = torch.exp(qdotk_nz[:,idz-zstart] - qdotk_max) * quad_weights[hi]
310
+ alpha_sum[:] += alpha[:, idz_i]
311
+
312
+ gdotv = torch.sum(dy[:,:,ho, wo] * vx[:,:,hi, wip], dim=1)
313
+ alpha_k[:,:] += alpha[:, None, idz_i] * k_hi_wi
314
+ alpha_vw[:,:] += alpha[:, None, idz_i] * gdotv[:,None]
315
+ alpha_kvw[:,:] += alpha[:, None, idz_i] * k_hi_wi * gdotv[:,None]
316
+
317
+ dqy[:,:,ho,wo] = (alpha_kvw*alpha_sum[:,None] - alpha_vw*alpha_k) / (alpha_sum[:,None]*alpha_sum[:,None])
318
+
319
+ return dqy
320
+
321
+ class _NeighborhoodAttentionS2(torch.autograd.Function):
322
+
323
+ @staticmethod
324
+ @custom_fwd(device_type="cpu")
325
+ def forward(ctx, k: torch.Tensor, v: torch.Tensor, q: torch.Tensor,
326
+ wk: torch.Tensor, wv: torch.Tensor, wq: torch.Tensor,
327
+ bk: Union[torch.Tensor, None], bv: Union[torch.Tensor, None], bq: Union[torch.Tensor, None],
328
+ quad_weights: torch.Tensor, col_idx: torch.Tensor, row_off: torch.Tensor,
329
+ nh: int, nlon_in: int, nlat_out: int, nlon_out: int):
330
+
331
+ ctx.save_for_backward(col_idx, row_off, quad_weights, k, v, q, wk, wv, wq, bk, bv, bq)
332
+ ctx.nh = nh
333
+ ctx.nlon_in = nlon_in
334
+ ctx.nlat_out = nlat_out
335
+ ctx.nlon_out = nlon_out
336
+
337
+ kw = F.conv2d(k, weight=wk, bias=bk)
338
+ vw = F.conv2d(v, weight=wv, bias=bv)
339
+ qw = F.conv2d(q, weight=wq, bias=bq)
340
+
341
+ # reshape, folding num heads into batch dim
342
+ B, _, H, W = kw.shape
343
+ kw = kw.reshape(B*nh, -1, H, W)
344
+ B, _, H, W = vw.shape
345
+ vw = vw.reshape(B*nh, -1, H, W)
346
+ B, _, H, W = qw.shape
347
+ qw = qw.reshape(B*nh, -1, H, W)
348
+
349
+ kw = kw.to(torch.float32)
350
+ vw = vw.to(torch.float32)
351
+ qw = qw.to(torch.float32)
352
+
353
+ output = _neighborhood_attention_s2_fwd_torch(kw, vw, qw, quad_weights,
354
+ col_idx, row_off,
355
+ nlon_in, nlat_out, nlon_out)
356
+
357
+ _, C, H, W = output.shape
358
+ output = output.reshape(B, -1, H, W)
359
+
360
+ return output
361
+
362
+ @staticmethod
363
+ @custom_bwd(device_type="cpu")
364
+ def backward(ctx, grad_output):
365
+ col_idx, row_off, quad_weights, k, v, q, wk, wv, wq, bk, bv, bq = ctx.saved_tensors
366
+ nh = ctx.nh
367
+ nlon_in = ctx.nlon_in
368
+ nlat_out = ctx.nlat_out
369
+ nlon_out = ctx.nlon_out
370
+
371
+ kw = F.conv2d(k, weight=wk, bias=bk)
372
+ vw = F.conv2d(v, weight=wv, bias=bv)
373
+ qw = F.conv2d(q, weight=wq, bias=bq)
374
+
375
+ # reshape, folding num heads into batch dim
376
+ B, _, H, W = kw.shape
377
+ kw = kw.reshape(B*nh, -1, H, W)
378
+ B, _, H, W = vw.shape
379
+ vw = vw.reshape(B*nh, -1, H, W)
380
+ B, _, H, W = qw.shape
381
+ qw = qw.reshape(B*nh, -1, H, W)
382
+ B, _, H, W = grad_output.shape
383
+ grad_output = grad_output.reshape(B*nh, -1, H, W)
384
+
385
+ dvw = _neighborhood_attention_s2_bwd_dv_torch(kw, vw, qw, grad_output,
386
+ quad_weights,
387
+ col_idx, row_off,
388
+ nlon_in, nlat_out, nlon_out)
389
+
390
+ dkw = _neighborhood_attention_s2_bwd_dk_torch(kw, vw, qw, grad_output,
391
+ quad_weights,
392
+ col_idx, row_off,
393
+ nlon_in, nlat_out, nlon_out)
394
+
395
+ dqw = _neighborhood_attention_s2_bwd_dq_torch(kw, vw, qw, grad_output,
396
+ quad_weights,
397
+ col_idx, row_off,
398
+ nlon_in, nlat_out, nlon_out)
399
+
400
+ # reshape again
401
+ _, C, H, W = dkw.shape
402
+ dkw = dkw.reshape(B, -1, H, W)
403
+ _, C, H, W = dvw.shape
404
+ dvw = dvw.reshape(B, -1, H, W)
405
+ _, C, H, W = dqw.shape
406
+ dqw = dqw.reshape(B, -1, H, W)
407
+
408
+ # input grads
409
+ dv = torch.nn.functional.conv2d(dvw, weight=wv.permute([1,0,2,3]), bias=None)
410
+ dk = torch.nn.functional.conv2d(dkw, weight=wk.permute([1,0,2,3]), bias=None)
411
+ dq = torch.nn.functional.conv2d(dqw, weight=wq.permute([1,0,2,3]), bias=None)
412
+
413
+ # weight grads
414
+ dwv = torch.einsum("bchw,bfhw->cf", dvw, v).reshape(*wv.shape).contiguous()
415
+ dwk = torch.einsum("bchw,bfhw->cf", dkw, k).reshape(*wk.shape).contiguous()
416
+ dwq = torch.einsum("bchw,bfhw->cf", dqw, q).reshape(*wq.shape).contiguous()
417
+
418
+ # bias grads:
419
+ if bv is not None:
420
+ dbv = torch.sum(dvw, dim=(0,2,3))
421
+ else:
422
+ dbv = None
423
+
424
+ if bk is not None:
425
+ dbk = torch.sum(dkw, dim=(0,2,3))
426
+ else:
427
+ dbk = None
428
+
429
+ if bq is not None:
430
+ dbq = torch.sum(dqw, dim=(0,2,3))
431
+ else:
432
+ dbq = None
433
+
434
+ return dk, dv, dq, dwk, dwv, dwq, dbk, dbv, dbq, \
435
+ None, None, None, None, None, None, None
436
+
437
+
438
+ def _neighborhood_attention_s2_torch(k: torch.Tensor, v: torch.Tensor, q: torch.Tensor,
439
+ wk: torch.Tensor, wv: torch.Tensor, wq: torch.Tensor,
440
+ bk: Union[torch.Tensor, None], bv: Union[torch.Tensor, None],
441
+ bq: Union[torch.Tensor, None], quad_weights: torch.Tensor,
442
+ col_idx: torch.Tensor, row_off: torch.Tensor,
443
+ nh: int, nlon_in: int, nlat_out: int, nlon_out: int) -> torch.Tensor:
444
+
445
+ return _NeighborhoodAttentionS2.apply(k, v, q, wk, wv, wq, bk, bv, bq,
446
+ quad_weights, col_idx, row_off,
447
+ nh, nlon_in, nlat_out, nlon_out)
448
+
449
+
450
+ class _NeighborhoodAttentionS2Cuda(torch.autograd.Function):
451
+
452
+
453
+ @staticmethod
454
+ @custom_fwd(device_type="cuda")
455
+ def forward(ctx, k: torch.Tensor, v: torch.Tensor, q: torch.Tensor,
456
+ wk: torch.Tensor, wv: torch.Tensor, wq: torch.Tensor,
457
+ bk: Union[torch.Tensor, None], bv: Union[torch.Tensor, None], bq: Union[torch.Tensor, None],
458
+ quad_weights: torch.Tensor, col_idx: torch.Tensor, row_off: torch.Tensor,
459
+ max_psi_nnz: int, nh: int, nlon_in: int, nlat_out: int, nlon_out: int):
460
+
461
+ ctx.save_for_backward(col_idx, row_off, quad_weights, k, v, q, wk, wv, wq, bk, bv, bq)
462
+ ctx.nh = nh
463
+ ctx.max_psi_nnz = max_psi_nnz
464
+ ctx.nlon_in = nlon_in
465
+ ctx.nlat_out = nlat_out
466
+ ctx.nlon_out = nlon_out
467
+
468
+ kw = F.conv2d(k, weight=wk, bias=bk)
469
+ vw = F.conv2d(v, weight=wv, bias=bv)
470
+ qw = F.conv2d(q, weight=wq, bias=bq)
471
+
472
+ # reshape, folding num heads into batch dim
473
+ B, _, H, W = kw.shape
474
+ kw = kw.reshape(B*nh, -1, H, W)
475
+ B, _, H, W = vw.shape
476
+ vw = vw.reshape(B*nh, -1, H, W)
477
+ B, _, H, W = qw.shape
478
+ qw = qw.reshape(B*nh, -1, H, W)
479
+
480
+ # convert to float32
481
+ inp_dtype = kw.dtype
482
+ kw = kw.to(torch.float32).contiguous()
483
+ vw = vw.to(torch.float32).contiguous()
484
+ qw = qw.to(torch.float32).contiguous()
485
+
486
+ output = attention_cuda_extension.forward(kw, vw, qw, quad_weights,
487
+ col_idx, row_off,
488
+ nlon_in, nlat_out, nlon_out)
489
+
490
+ _, C, H, W = output.shape
491
+ output = output.reshape(B, -1, H, W)
492
+
493
+ # convert back precision
494
+ output = output.to(dtype=inp_dtype)
495
+
496
+ return output
497
+
498
+ @staticmethod
499
+ @custom_bwd(device_type="cuda")
500
+ def backward(ctx, grad_output):
501
+ col_idx, row_off, quad_weights, k, v, q, wk, wv, wq, bk, bv, bq = ctx.saved_tensors
502
+ nh = ctx.nh
503
+ max_psi_nnz = ctx.max_psi_nnz
504
+ nlon_in = ctx.nlon_in
505
+ nlat_out = ctx.nlat_out
506
+ nlon_out = ctx.nlon_out
507
+
508
+ kw = F.conv2d(k, weight=wk, bias=bk)
509
+ vw = F.conv2d(v, weight=wv, bias=bv)
510
+ qw = F.conv2d(q, weight=wq, bias=bq)
511
+
512
+ # reshape, folding num heads into batch dim
513
+ B, _, H, W = kw.shape
514
+ kw = kw.reshape(B*nh, -1, H, W)
515
+ B, _, H, W = vw.shape
516
+ vw = vw.reshape(B*nh, -1, H, W)
517
+ B, _, H, W = qw.shape
518
+ qw = qw.reshape(B*nh, -1, H, W)
519
+ B, _, H, W = grad_output.shape
520
+ grad_output = grad_output.reshape(B*nh, -1, H, W)
521
+
522
+ # save type and convert to float32
523
+ kw_dtype = kw.dtype
524
+ vw_dtype = vw.dtype
525
+ qw_dtype = qw.dtype
526
+
527
+ kw = kw.to(torch.float32).contiguous()
528
+ vw = vw.to(torch.float32).contiguous()
529
+ qw = qw.to(torch.float32).contiguous()
530
+ grad_output = grad_output.to(torch.float32).contiguous()
531
+
532
+ dkw,dvw,dqw = attention_cuda_extension.backward_dkvq(kw, vw, qw, grad_output,
533
+ quad_weights,
534
+ col_idx, row_off,
535
+ nlon_in, nlat_out, nlon_out)
536
+
537
+ # reshape again
538
+ _, C, H, W = dkw.shape
539
+ dkw = dkw.reshape(B, -1, H, W)
540
+ _, C, H, W = dvw.shape
541
+ dvw = dvw.reshape(B, -1, H, W)
542
+ _, C, H, W = dqw.shape
543
+ dqw = dqw.reshape(B, -1, H, W)
544
+
545
+ # convert precision
546
+ dkw = dkw.to(dtype=kw_dtype)
547
+ dvw = dvw.to(dtype=vw_dtype)
548
+ dqw = dqw.to(dtype=qw_dtype)
549
+
550
+ # input grads
551
+ dv = torch.nn.functional.conv2d(dvw, weight=wv.permute([1,0,2,3]), bias=None)
552
+ dk = torch.nn.functional.conv2d(dkw, weight=wk.permute([1,0,2,3]), bias=None)
553
+ dq = torch.nn.functional.conv2d(dqw, weight=wq.permute([1,0,2,3]), bias=None)
554
+
555
+ # weight grads
556
+ dwv = torch.einsum("bchw,bfhw->cf", dvw, v).reshape(*wv.shape).contiguous()
557
+ dwk = torch.einsum("bchw,bfhw->cf", dkw, k).reshape(*wk.shape).contiguous()
558
+ dwq = torch.einsum("bchw,bfhw->cf", dqw, q).reshape(*wq.shape).contiguous()
559
+
560
+ # bias grads:
561
+ if bv is not None:
562
+ dbv = torch.sum(dvw, dim=(0,2,3))
563
+ else:
564
+ dbv = None
565
+
566
+ if bk is not None:
567
+ dbk = torch.sum(dkw, dim=(0,2,3))
568
+ else:
569
+ dbk = None
570
+
571
+ if bq is not None:
572
+ dbq = torch.sum(dqw, dim=(0,2,3))
573
+ else:
574
+ dbq = None
575
+
576
+ return dk, dv, dq, dwk, dwv, dwq, dbk, dbv, dbq, \
577
+ None, None, None, None, None, None, None, None
578
+
579
+
580
+ def _neighborhood_attention_s2_cuda(k: torch.Tensor, v: torch.Tensor, q: torch.Tensor,
581
+ wk: torch.Tensor, wv: torch.Tensor, wq: torch.Tensor,
582
+ bk: Union[torch.Tensor, None], bv: Union[torch.Tensor, None],
583
+ bq: Union[torch.Tensor, None], quad_weights: torch.Tensor,
584
+ col_idx: torch.Tensor, row_off: torch.Tensor, max_psi_nnz: int,
585
+ nh: int, nlon_in: int, nlat_out: int, nlon_out: int) -> torch.Tensor:
586
+
587
+ return _NeighborhoodAttentionS2Cuda.apply(k, v, q, wk, wv, wq, bk, bv, bq,
588
+ quad_weights, col_idx, row_off, max_psi_nnz,
589
+ nh, nlon_in, nlat_out, nlon_out)
.deps/torch_harmonics/attention.py ADDED
@@ -0,0 +1,382 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2025 The torch-harmonics Authors. All rights reserved.
4
+ # SPDX-License-Identifier: BSD-3-Clause
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are met:
8
+ #
9
+ # 1. Redistributions of source code must retain the above copyright notice, this
10
+ # list of conditions and the following disclaimer.
11
+ #
12
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ # this list of conditions and the following disclaimer in the documentation
14
+ # and/or other materials provided with the distribution.
15
+ #
16
+ # 3. Neither the name of the copyright holder nor the names of its
17
+ # contributors may be used to endorse or promote products derived from
18
+ # this software without specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+ #
31
+
32
+ from typing import List, Tuple, Union, Optional
33
+ from warnings import warn
34
+
35
+ import math
36
+
37
+ import torch
38
+ import torch.nn as nn
39
+ import numpy as np
40
+
41
+ from torch_harmonics.quadrature import _precompute_latitudes
42
+ from torch_harmonics.convolution import _precompute_convolution_tensor_s2
43
+ from torch_harmonics._neighborhood_attention import _neighborhood_attention_s2_torch, _neighborhood_attention_s2_cuda
44
+ from torch_harmonics.filter_basis import get_filter_basis
45
+
46
+ # import custom C++/CUDA extensions
47
+ try:
48
+ import attention_cuda_extension
49
+
50
+ _cuda_extension_available = True
51
+ except ImportError as err:
52
+ attention_cuda_extension = None
53
+ _cuda_extension_available = False
54
+
55
+ class AttentionS2(nn.Module):
56
+ """
57
+ (Global) attention on the 2-sphere.
58
+ Parameters
59
+ -----------
60
+ in_channels: int
61
+ number of channels of the input signal (corresponds to embed_dim in MHA in PyTorch)
62
+ num_heads: int
63
+ number of attention heads
64
+ in_shape: tuple
65
+ shape of the input grid
66
+ out_shape: tuple
67
+ shape of the output grid
68
+ grid_in: str, optional
69
+ input grid type, "equiangular" by default
70
+ grid_out: str, optional
71
+ output grid type, "equiangular" by default
72
+ bias: bool, optional
73
+ if specified, adds bias to input / output projection layers
74
+ k_channels: int
75
+ number of dimensions for interior inner product in the attention matrix (corresponds to kdim in MHA in PyTorch)
76
+ out_channels: int, optional
77
+ number of dimensions for interior inner product in the attention matrix (corresponds to vdim in MHA in PyTorch)
78
+ """
79
+
80
+ def __init__(
81
+ self,
82
+ in_channels: int,
83
+ num_heads: int,
84
+ in_shape: Tuple[int],
85
+ out_shape: Tuple[int],
86
+ grid_in: Optional[str] = "equiangular",
87
+ grid_out: Optional[str] = "equiangular",
88
+ scale: Optional[Union[torch.Tensor, float]] = None,
89
+ bias: Optional[bool] = True,
90
+ k_channels: Optional[int] = None,
91
+ out_channels: Optional[int] = None,
92
+ drop_rate: Optional[float]=0.0,
93
+ ):
94
+ super().__init__()
95
+
96
+ self.nlat_in, self.nlon_in = in_shape
97
+ self.nlat_out, self.nlon_out = out_shape
98
+
99
+ self.in_channels = in_channels
100
+ self.num_heads = num_heads
101
+ self.k_channels = in_channels if k_channels is None else k_channels
102
+ self.out_channels = in_channels if out_channels is None else out_channels
103
+ self.drop_rate = drop_rate
104
+ self.scale = scale
105
+
106
+ # integration weights
107
+ _, wgl = _precompute_latitudes(self.nlat_in, grid=grid_in)
108
+ quad_weights = 2.0 * torch.pi * wgl.to(dtype=torch.float32) / self.nlon_in
109
+ # we need to tile and flatten them accordingly
110
+ quad_weights = torch.tile(quad_weights.reshape(-1, 1), (1, self.nlon_in)).flatten()
111
+
112
+ # compute log because they are applied as an addition prior to the softmax ('attn_mask'), which includes an exponential.
113
+ # see https://pytorch.org/docs/stable/generated/torch.nn.functional.scaled_dot_product_attention.html
114
+ # for info on how 'attn_mask' is applied to the attention weights
115
+ log_quad_weights = torch.log(quad_weights).reshape(1,1,-1)
116
+ self.register_buffer("log_quad_weights", log_quad_weights, persistent=False)
117
+
118
+ # learnable parameters
119
+ # TODO: double-check that this gives us the correct initialization magnitudes
120
+ # the standard MHA uses xavier uniform, NATTEN uses kaiming. Let's use that for now
121
+ if self.k_channels % self.num_heads != 0:
122
+ raise ValueError(f"Please make sure that number of heads {self.num_heads} divides k_channels {self.k_channels} evenly.")
123
+ if self.out_channels % self.num_heads != 0:
124
+ raise ValueError(f"Please make sure that number of heads {self.num_heads} divides out_channels {self.out_channels} evenly.")
125
+ scale_qkv = math.sqrt(3.0 / self.in_channels)
126
+ self.q_weights = nn.Parameter(scale_qkv * (2 * torch.rand(self.k_channels, self.in_channels, 1, 1) - 1))
127
+ self.k_weights = nn.Parameter(scale_qkv * (2 * torch.rand(self.k_channels, self.in_channels, 1, 1) - 1))
128
+ self.v_weights = nn.Parameter(scale_qkv * (2 * torch.rand(self.out_channels, self.in_channels, 1, 1) - 1))
129
+ scale_proj = math.sqrt(3.0 / self.out_channels)
130
+ self.proj_weights = nn.Parameter(scale_proj * (2 * torch.rand(self.out_channels, self.out_channels, 1, 1) - 1))
131
+
132
+ if bias:
133
+ self.q_bias = nn.Parameter(torch.zeros(self.k_channels))
134
+ self.k_bias = nn.Parameter(torch.zeros(self.k_channels))
135
+ self.v_bias = nn.Parameter(torch.zeros(self.out_channels))
136
+ self.proj_bias = nn.Parameter(torch.zeros(self.out_channels))
137
+ else:
138
+ self.q_bias = None
139
+ self.k_bias = None
140
+ self.v_bias = None
141
+ self.proj_bias = None
142
+
143
+
144
+ def extra_repr(self):
145
+ return f"in_shape={(self.nlat_in, self.nlon_in)}, out_shape={(self.nlat_out, self.nlon_out)}, in_channels={self.in_channels}, out_channels={self.out_channels}, k_channels={self.k_channels}"
146
+
147
+ def forward(self, query: torch.Tensor, key: Optional[torch.Tensor] = None, value: Optional[torch.Tensor] = None) -> torch.Tensor:
148
+
149
+ # self attention simplification
150
+ if key is None:
151
+ key = query
152
+
153
+ if value is None:
154
+ value = query
155
+
156
+ # change this later to allow arbitrary number of batch dims
157
+ assert (query.dim() == key.dim()) and (key.dim() == value.dim()) and (value.dim() == 4)
158
+
159
+ # perform MLP
160
+ query = nn.functional.conv2d(query, self.q_weights, bias=self.q_bias)
161
+ key = nn.functional.conv2d(key, self.k_weights, bias=self.k_bias)
162
+ value = nn.functional.conv2d(value, self.v_weights, bias=self.v_bias)
163
+
164
+ # reshape
165
+ B, _, H, W = query.shape
166
+ query = query.reshape(B, self.num_heads, -1, H, W)
167
+ B, _, H, W = key.shape
168
+ key = key.reshape(B, self.num_heads, -1, H, W)
169
+ B, _, H, W = value.shape
170
+ value = value.reshape(B, self.num_heads, -1, H, W)
171
+
172
+ # reshape to the right dimensions
173
+ B, _, C, H, W = query.shape
174
+ query = query.permute(0,1,3,4,2).reshape(B, self.num_heads, H*W, C)
175
+ B, _, C, H, W = key.shape
176
+ key = key.permute(0,1,3,4,2).reshape(B, self.num_heads, H*W, C)
177
+ B, _, C, H, W = value.shape
178
+ value = value.permute(0,1,3,4,2).reshape(B, self.num_heads, H*W, C)
179
+
180
+ # multiply the query, key and value tensors
181
+ out = nn.functional.scaled_dot_product_attention(query, key, value, attn_mask=self.log_quad_weights, dropout_p=self.drop_rate, scale=self.scale)
182
+
183
+ # reshape
184
+ B, _, _, C = out.shape
185
+ # (B, heads, H*W, C)
186
+ out = out.permute(0,1,3,2)
187
+ # (B, heads, C, H*W)
188
+ out = out.reshape(B, self.num_heads*C, self.nlat_out, self.nlon_out)
189
+ # (B, heads*C, H, W)
190
+ out = nn.functional.conv2d(out, self.proj_weights, bias=self.proj_bias)
191
+
192
+ return out
193
+
194
+
195
+ class NeighborhoodAttentionS2(nn.Module):
196
+ """
197
+ Neighborhood attention on the 2-sphere.
198
+
199
+ Parameters
200
+ -----------
201
+ in_channels: int
202
+ number of channels of the input signal (corresponds to embed_dim in MHA in PyTorch)
203
+ in_shape: tuple
204
+ shape of the input grid
205
+ out_shape: tuple
206
+ shape of the output grid
207
+ grid_in: str, optional
208
+ input grid type, "equiangular" by default
209
+ grid_out: str, optional
210
+ output grid type, "equiangular" by default
211
+ bias: bool, optional
212
+ if specified, adds bias to input / output projection layers
213
+ theta_cutoff: float, optional
214
+ neighborhood size
215
+ k_channels: int
216
+ number of dimensions for interior inner product in the attention matrix (corresponds to kdim in MHA in PyTorch)
217
+ out_channels: int, optional
218
+ number of dimensions for interior inner product in the attention matrix (corresponds to vdim in MHA in PyTorch)
219
+ """
220
+
221
+ def __init__(
222
+ self,
223
+ in_channels: int,
224
+ in_shape: Tuple[int],
225
+ out_shape: Tuple[int],
226
+ grid_in: Optional[str] = "equiangular",
227
+ grid_out: Optional[str] = "equiangular",
228
+ num_heads: Optional[int] = 1,
229
+ scale: Optional[Union[torch.Tensor, float]] = None,
230
+ bias: Optional[bool] = True,
231
+ theta_cutoff: Optional[float] = None,
232
+ k_channels: Optional[int] = None,
233
+ out_channels: Optional[int] = None,
234
+ ):
235
+ super().__init__()
236
+
237
+ self.nlat_in, self.nlon_in = in_shape
238
+ self.nlat_out, self.nlon_out = out_shape
239
+
240
+ self.in_channels = in_channels
241
+ self.num_heads = num_heads
242
+ self.k_channels = in_channels if k_channels is None else k_channels
243
+ self.out_channels = in_channels if out_channels is None else out_channels
244
+
245
+ # heuristic to compute theta cutoff based on the bandlimit of the input field and overlaps of the basis functions
246
+ if theta_cutoff is None:
247
+ theta_cutoff = torch.pi / float(self.nlat_out - 1)
248
+
249
+ if theta_cutoff <= 0.0:
250
+ raise ValueError("Error, theta_cutoff has to be positive.")
251
+
252
+ # integration weights
253
+ _, wgl = _precompute_latitudes(self.nlat_in, grid=grid_in)
254
+ quad_weights = 2.0 * torch.pi * wgl.to(dtype=torch.float32) / self.nlon_in
255
+ self.register_buffer("quad_weights", quad_weights, persistent=False)
256
+
257
+ # create a dummy filter basis to pass to the construction of the convolution tensor
258
+ # this is to avoid code duplication as the logic of pre-computing the sparsity pattern
259
+ # is identical to convolutions with a constant filter function
260
+ fb = get_filter_basis(kernel_shape=1, basis_type="zernike")
261
+
262
+ # precompute the neighborhood sparsity pattern
263
+ idx, _, roff = _precompute_convolution_tensor_s2(
264
+ in_shape,
265
+ out_shape,
266
+ fb,
267
+ grid_in=grid_in,
268
+ grid_out=grid_out,
269
+ theta_cutoff=theta_cutoff,
270
+ transpose_normalization=False,
271
+ basis_norm_mode="none",
272
+ merge_quadrature=True,
273
+ )
274
+
275
+ # this is kept for legacy resons in case we want to resuse sorting of these entries
276
+ row_idx = idx[1, ...].contiguous()
277
+ col_idx = idx[2, ...].contiguous()
278
+ roff_idx = roff.contiguous()
279
+
280
+ # store some metadata
281
+ self.max_psi_nnz = col_idx.max().item() + 1
282
+ self.register_buffer("psi_row_idx", row_idx, persistent=False)
283
+ self.register_buffer("psi_col_idx", col_idx, persistent=False)
284
+ self.register_buffer("psi_roff_idx", roff_idx, persistent=False)
285
+
286
+ # learnable parameters
287
+ # TODO: double-check that this gives us the correct initialization magnitudes
288
+ # the standard MHA uses xavier uniform, NATTEN uses kaiming. Let's use that for now
289
+ if self.k_channels % self.num_heads != 0:
290
+ raise ValueError(f"Please make sure that number of heads {self.num_heads} divides k_channels {self.k_channels} evenly.")
291
+ if self.out_channels % self.num_heads != 0:
292
+ raise ValueError(f"Please make sure that number of heads {self.num_heads} divides out_channels {self.out_channels} evenly.")
293
+ scale_qkv = math.sqrt(3.0 / self.in_channels)
294
+ self.q_weights = nn.Parameter(scale_qkv * (2 * torch.rand(self.k_channels, self.in_channels, 1, 1) - 1))
295
+ self.k_weights = nn.Parameter(scale_qkv * (2 * torch.rand(self.k_channels, self.in_channels, 1, 1) - 1))
296
+ self.v_weights = nn.Parameter(scale_qkv * (2 * torch.rand(self.out_channels, self.in_channels, 1, 1) - 1))
297
+ scale_proj = math.sqrt(3.0 / self.out_channels)
298
+ self.proj_weights = nn.Parameter(scale_proj * (2 * torch.rand(self.out_channels, self.out_channels, 1, 1) - 1))
299
+
300
+ if scale is not None:
301
+ self.scale = scale
302
+ else:
303
+ self.scale = 1 / math.sqrt(self.k_channels)
304
+
305
+ if bias:
306
+ self.q_bias = nn.Parameter(torch.zeros(self.k_channels))
307
+ self.k_bias = nn.Parameter(torch.zeros(self.k_channels))
308
+ self.v_bias = nn.Parameter(torch.zeros(self.out_channels))
309
+ self.proj_bias = nn.Parameter(torch.zeros(self.out_channels))
310
+ else:
311
+ self.q_bias = None
312
+ self.k_bias = None
313
+ self.v_bias = None
314
+ self.proj_bias = None
315
+
316
+ def extra_repr(self):
317
+ return f"in_shape={(self.nlat_in, self.nlon_in)}, out_shape={(self.nlat_out, self.nlon_out)}, in_channels={self.in_channels}, out_channels={self.out_channels}, k_channels={self.k_channels}"
318
+
319
+ def forward(self, query: torch.Tensor, key: Optional[torch.Tensor] = None, value: Optional[torch.Tensor] = None) -> torch.Tensor:
320
+
321
+ # self attention simplification
322
+ if key is None:
323
+ key = query
324
+
325
+ if value is None:
326
+ value = query
327
+
328
+ # change this later to allow arbitrary number of batch dims
329
+ assert (query.dim() == key.dim()) and (key.dim() == value.dim()) and (value.dim() == 4)
330
+
331
+ # do the scaling
332
+ query_scaled = query * self.scale
333
+
334
+ # TODO: insert dimension checks for input
335
+ if query.is_cuda and _cuda_extension_available:
336
+
337
+ out = _neighborhood_attention_s2_cuda(
338
+ key,
339
+ value,
340
+ query_scaled,
341
+ self.k_weights,
342
+ self.v_weights,
343
+ self.q_weights,
344
+ self.k_bias,
345
+ self.v_bias,
346
+ self.q_bias,
347
+ self.quad_weights,
348
+ self.psi_col_idx,
349
+ self.psi_roff_idx,
350
+ self.max_psi_nnz,
351
+ self.num_heads,
352
+ self.nlon_in,
353
+ self.nlat_out,
354
+ self.nlon_out,
355
+ )
356
+ else:
357
+ if query.is_cuda:
358
+ warn("couldn't find CUDA extension, falling back to slow PyTorch implementation")
359
+
360
+ # call attention
361
+ out = _neighborhood_attention_s2_torch(
362
+ key,
363
+ value,
364
+ query_scaled,
365
+ self.k_weights,
366
+ self.v_weights,
367
+ self.q_weights,
368
+ self.k_bias,
369
+ self.v_bias,
370
+ self.q_bias,
371
+ self.quad_weights,
372
+ self.psi_col_idx,
373
+ self.psi_roff_idx,
374
+ self.num_heads,
375
+ self.nlon_in,
376
+ self.nlat_out,
377
+ self.nlon_out,
378
+ )
379
+
380
+ out = nn.functional.conv2d(out, self.proj_weights, bias=self.proj_bias)
381
+
382
+ return out
.deps/torch_harmonics/cache.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2025 The torch-harmonics Authors. All rights reserved.
4
+ # SPDX-License-Identifier: BSD-3-Clause
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are met:
8
+ #
9
+ # 1. Redistributions of source code must retain the above copyright notice, this
10
+ # list of conditions and the following disclaimer.
11
+ #
12
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ # this list of conditions and the following disclaimer in the documentation
14
+ # and/or other materials provided with the distribution.
15
+ #
16
+ # 3. Neither the name of the copyright holder nor the names of its
17
+ # contributors may be used to endorse or promote products derived from
18
+ # this software without specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+ #
31
+
32
+ import functools
33
+ from copy import deepcopy
34
+
35
+ # copying LRU cache decorator a la:
36
+ # https://stackoverflow.com/questions/54909357/how-to-get-functools-lru-cache-to-return-new-instances
37
+ def lru_cache(maxsize=20, typed=False, copy=False):
38
+ """
39
+ Least Recently Used (LRU) cache decorator with optional deep copying.
40
+
41
+ This is a wrapper around functools.lru_cache that adds the ability to return
42
+ deep copies of cached results to prevent unintended modifications to cached objects.
43
+
44
+ Parameters
45
+ -----------
46
+ maxsize : int, optional
47
+ Maximum number of items to cache, by default 20
48
+ typed : bool, optional
49
+ Whether to cache different types separately, by default False
50
+ copy : bool, optional
51
+ Whether to return deep copies of cached results, by default False
52
+
53
+ Returns
54
+ -------
55
+ function
56
+ Decorated function with LRU caching
57
+
58
+ Example
59
+ -------
60
+ >>> @lru_cache(maxsize=10, copy=True)
61
+ ... def expensive_function(x):
62
+ ... return [x, x*2, x*3]
63
+ """
64
+ def decorator(f):
65
+ cached_func = functools.lru_cache(maxsize=maxsize, typed=typed)(f)
66
+ def wrapper(*args, **kwargs):
67
+ res = cached_func(*args, **kwargs)
68
+ if copy:
69
+ return deepcopy(res)
70
+ else:
71
+ return res
72
+
73
+ return wrapper
74
+ return decorator
.deps/torch_harmonics/convolution.py ADDED
@@ -0,0 +1,666 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2022 The torch-harmonics Authors. All rights reserved.
4
+ # SPDX-License-Identifier: BSD-3-Clause
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are met:
8
+ #
9
+ # 1. Redistributions of source code must retain the above copyright notice, this
10
+ # list of conditions and the following disclaimer.
11
+ #
12
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ # this list of conditions and the following disclaimer in the documentation
14
+ # and/or other materials provided with the distribution.
15
+ #
16
+ # 3. Neither the name of the copyright holder nor the names of its
17
+ # contributors may be used to endorse or promote products derived from
18
+ # this software without specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+ #
31
+
32
+ import abc
33
+ from typing import List, Tuple, Union, Optional
34
+ from warnings import warn
35
+
36
+ import math
37
+
38
+ import torch
39
+ import torch.nn as nn
40
+
41
+ from functools import partial
42
+
43
+ from torch_harmonics.cache import lru_cache
44
+ from torch_harmonics.quadrature import _precompute_grid, _precompute_latitudes, _precompute_longitudes
45
+ from torch_harmonics._disco_convolution import _get_psi, _disco_s2_contraction_torch, _disco_s2_transpose_contraction_torch
46
+ from torch_harmonics._disco_convolution import _disco_s2_contraction_cuda, _disco_s2_transpose_contraction_cuda
47
+ from torch_harmonics.filter_basis import FilterBasis, get_filter_basis
48
+
49
+ # import custom C++/CUDA extensions if available
50
+ try:
51
+ from disco_helpers import preprocess_psi
52
+ import disco_cuda_extension
53
+
54
+ _cuda_extension_available = True
55
+ except ImportError as err:
56
+ disco_cuda_extension = None
57
+ _cuda_extension_available = False
58
+
59
+
60
+ def _normalize_convolution_tensor_s2(
61
+ psi_idx, psi_vals, in_shape, out_shape, kernel_size, quad_weights, transpose_normalization=False, basis_norm_mode="mean", merge_quadrature=False, eps=1e-9
62
+ ):
63
+ """Normalizes convolution tensor values based on specified normalization mode.
64
+
65
+ This function applies different normalization strategies to the convolution tensor
66
+ values based on the basis_norm_mode parameter. It can normalize individual basis
67
+ functions, compute mean normalization across all basis functions, or use support
68
+ weights. The function also optionally merges quadrature weights into the tensor.
69
+
70
+ Parameters
71
+ -----------
72
+ psi_idx: torch.Tensor
73
+ Index tensor for the sparse convolution tensor.
74
+ psi_vals: torch.Tensor
75
+ Value tensor for the sparse convolution tensor.
76
+ in_shape: Tuple[int]
77
+ Tuple of (nlat_in, nlon_in) representing input grid dimensions.
78
+ out_shape: Tuple[int]
79
+ Tuple of (nlat_out, nlon_out) representing output grid dimensions.
80
+ kernel_size: int
81
+ Number of kernel basis functions.
82
+ quad_weights: torch.Tensor
83
+ Quadrature weights for numerical integration.
84
+ transpose_normalization: bool
85
+ If True, applies normalization in transpose direction.
86
+ basis_norm_mode: str
87
+ Normalization mode, one of ["none", "individual", "mean", "support"].
88
+ merge_quadrature: bool
89
+ If True, multiplies values by quadrature weights.
90
+ eps: float
91
+ Small epsilon value to prevent division by zero.
92
+
93
+ Returns
94
+ -------
95
+ torch.Tensor
96
+ Normalized convolution tensor values.
97
+
98
+ Raises
99
+ ------
100
+ ValueError
101
+ If basis_norm_mode is not one of the supported modes.
102
+ """
103
+
104
+ # exit here if no normalization is needed
105
+ if basis_norm_mode == "none":
106
+ return psi_vals
107
+
108
+ # reshape the indices implicitly to be ikernel, out_shape[0], in_shape[0], in_shape[1]
109
+ idx = torch.stack([psi_idx[0], psi_idx[1], psi_idx[2] // in_shape[1], psi_idx[2] % in_shape[1]], dim=0)
110
+
111
+ # getting indices for adressing kernels, input and output latitudes
112
+ ikernel = idx[0]
113
+
114
+ if transpose_normalization:
115
+ ilat_out = idx[2]
116
+ ilat_in = idx[1]
117
+ # here we are deliberately swapping input and output shapes to handle transpose normalization with the same code
118
+ nlat_out = in_shape[0]
119
+ correction_factor = out_shape[1] / in_shape[1]
120
+ else:
121
+ ilat_out = idx[1]
122
+ ilat_in = idx[2]
123
+ nlat_out = out_shape[0]
124
+
125
+ # get the quadrature weights
126
+ q = quad_weights[ilat_in].reshape(-1)
127
+
128
+ # buffer to store intermediate values
129
+ vnorm = torch.zeros(kernel_size, nlat_out, device=psi_vals.device)
130
+ support = torch.zeros(kernel_size, nlat_out, device=psi_vals.device)
131
+
132
+ # loop through dimensions to compute the norms
133
+ for ik in range(kernel_size):
134
+ for ilat in range(nlat_out):
135
+
136
+ # find indices corresponding to the given output latitude and kernel basis function
137
+ iidx = torch.argwhere((ikernel == ik) & (ilat_out == ilat))
138
+
139
+ # compute the 1-norm
140
+ # vnorm[ik, ilat] = torch.sqrt(torch.sum(psi_vals[iidx].abs().pow(2) * q[iidx]))
141
+ vnorm[ik, ilat] = torch.sum(psi_vals[iidx].abs() * q[iidx])
142
+
143
+ # compute the support
144
+ support[ik, ilat] = torch.sum(q[iidx])
145
+
146
+ # loop over values and renormalize
147
+ for ik in range(kernel_size):
148
+ for ilat in range(nlat_out):
149
+
150
+ iidx = torch.argwhere((ikernel == ik) & (ilat_out == ilat))
151
+
152
+ if basis_norm_mode == "individual":
153
+ val = vnorm[ik, ilat]
154
+ elif basis_norm_mode == "mean":
155
+ val = vnorm[ik, :].mean()
156
+ elif basis_norm_mode == "support":
157
+ val = support[ik, ilat]
158
+ elif basis_norm_mode == "none":
159
+ val = 1.0
160
+ else:
161
+ raise ValueError(f"Unknown basis normalization mode {basis_norm_mode}.")
162
+
163
+ psi_vals[iidx] = psi_vals[iidx] / (val + eps)
164
+
165
+ if merge_quadrature:
166
+ psi_vals[iidx] = psi_vals[iidx] * q[iidx]
167
+
168
+ if transpose_normalization and merge_quadrature:
169
+ psi_vals = psi_vals / correction_factor
170
+
171
+ return psi_vals
172
+
173
+
174
+ @lru_cache(typed=True, copy=True)
175
+ def _precompute_convolution_tensor_s2(
176
+ in_shape: Tuple[int],
177
+ out_shape: Tuple[int],
178
+ filter_basis: FilterBasis,
179
+ grid_in: Optional[str] = "equiangular",
180
+ grid_out: Optional[str] = "equiangular",
181
+ theta_cutoff: Optional[float] = 0.01 * math.pi,
182
+ theta_eps: Optional[float] = 1e-3,
183
+ transpose_normalization: Optional[bool] = False,
184
+ basis_norm_mode: Optional[str] = "mean",
185
+ merge_quadrature: Optional[bool] = False,
186
+ ):
187
+ """
188
+ Precomputes the rotated filters at positions $R^{-1}_j \omega_i = R^{-1}_j R_i \nu = Y(-\theta_j)Z(\phi_i - \phi_j)Y(\theta_j)\nu$.
189
+ Assumes a tensorized grid on the sphere with an equidistant sampling in longitude as described in Ocampo et al.
190
+ The output tensor has shape kernel_shape x nlat_out x (nlat_in * nlon_in).
191
+
192
+ The rotation of the Euler angles uses the YZY convention, which applied to the northpole $(0,0,1)^T$ yields
193
+ $$
194
+ Y(\alpha) Z(\beta) Y(\gamma) n =
195
+ {\begin{bmatrix}
196
+ \cos(\gamma)\sin(\alpha) + \cos(\alpha)\cos(\beta)\sin(\gamma) \\
197
+ \sin(\beta)\sin(\gamma) \\
198
+ \cos(\alpha)\cos(\gamma)-\cos(\beta)\sin(\alpha)\sin(\gamma)
199
+ \end{bmatrix}}
200
+ $$
201
+
202
+ Parameters
203
+ -----------
204
+ in_shape: Tuple[int]
205
+ Input shape of the convolution tensor
206
+ out_shape: Tuple[int]
207
+ Output shape of the convolution tensor
208
+ filter_basis: FilterBasis
209
+ Filter basis functions
210
+ grid_in: str
211
+ Input grid type
212
+ grid_out: str
213
+ Output grid type
214
+ theta_cutoff: float
215
+ Theta cutoff for the filter basis functions
216
+ theta_eps: float
217
+ Epsilon for the theta cutoff
218
+ transpose_normalization: bool
219
+ Whether to normalize the convolution tensor in the transpose direction
220
+ basis_norm_mode: str
221
+ Mode for basis normalization
222
+ merge_quadrature: bool
223
+ Whether to merge the quadrature weights into the convolution tensor
224
+
225
+ Returns
226
+ -------
227
+ out_idx: torch.Tensor
228
+ Index tensor of the convolution tensor
229
+ out_vals: torch.Tensor
230
+ Values tensor of the convolution tensor
231
+
232
+ """
233
+
234
+ assert len(in_shape) == 2
235
+ assert len(out_shape) == 2
236
+
237
+ kernel_size = filter_basis.kernel_size
238
+
239
+ nlat_in, nlon_in = in_shape
240
+ nlat_out, nlon_out = out_shape
241
+
242
+ # precompute input and output grids
243
+ lats_in, win = _precompute_latitudes(nlat_in, grid=grid_in)
244
+ lats_out, wout = _precompute_latitudes(nlat_out, grid=grid_out)
245
+
246
+ # compute the phi differences
247
+ # It's imporatant to not include the 2 pi point in the longitudes, as it is equivalent to lon=0
248
+ lons_in = _precompute_longitudes(nlon_in)
249
+
250
+ # compute quadrature weights and merge them into the convolution tensor.
251
+ # These quadrature integrate to 1 over the sphere.
252
+ if transpose_normalization:
253
+ quad_weights = wout.reshape(-1, 1) / nlon_in / 2.0
254
+ else:
255
+ quad_weights = win.reshape(-1, 1) / nlon_in / 2.0
256
+
257
+ # effective theta cutoff if multiplied with a fudge factor to avoid aliasing with grid width (especially near poles)
258
+ theta_cutoff_eff = (1.0 + theta_eps) * theta_cutoff
259
+
260
+ out_idx = []
261
+ out_vals = []
262
+
263
+ beta = lons_in
264
+ gamma = lats_in.reshape(-1, 1)
265
+
266
+ # compute trigs
267
+ cbeta = torch.cos(beta)
268
+ sbeta = torch.sin(beta)
269
+ cgamma = torch.cos(gamma)
270
+ sgamma = torch.sin(gamma)
271
+
272
+ # compute row offsets
273
+ out_roff = torch.zeros(nlat_out + 1, dtype=torch.int64, device=lons_in.device)
274
+ out_roff[0] = 0
275
+ for t in range(nlat_out):
276
+ # the last angle has a negative sign as it is a passive rotation, which rotates the filter around the y-axis
277
+ alpha = -lats_out[t]
278
+
279
+ # compute cartesian coordinates of the rotated position
280
+ # This uses the YZY convention of Euler angles, where the last angle (alpha) is a passive rotation,
281
+ # and therefore applied with a negative sign
282
+ x = torch.cos(alpha) * cbeta * sgamma + cgamma * torch.sin(alpha)
283
+ y = sbeta * sgamma
284
+ z = -cbeta * torch.sin(alpha) * sgamma + torch.cos(alpha) * cgamma
285
+
286
+ # normalization is important to avoid NaNs when arccos and atan are applied
287
+ # this can otherwise lead to spurious artifacts in the solution
288
+ norm = torch.sqrt(x * x + y * y + z * z)
289
+ x = x / norm
290
+ y = y / norm
291
+ z = z / norm
292
+
293
+ # compute spherical coordinates, where phi needs to fall into the [0, 2pi) range
294
+ theta = torch.arccos(z)
295
+ phi = torch.arctan2(y, x)
296
+ phi = torch.where(phi < 0.0, phi + 2 * torch.pi, phi)
297
+
298
+ # find the indices where the rotated position falls into the support of the kernel
299
+ iidx, vals = filter_basis.compute_support_vals(theta, phi, r_cutoff=theta_cutoff_eff)
300
+
301
+ # add the output latitude and reshape such that psi has dimensions kernel_shape x nlat_out x (nlat_in*nlon_in)
302
+ idx = torch.stack([iidx[:, 0], t * torch.ones_like(iidx[:, 0]), iidx[:, 1] * nlon_in + iidx[:, 2]], dim=0)
303
+
304
+ # append indices and values to the COO datastructure, compute row offsets
305
+ out_idx.append(idx)
306
+ out_vals.append(vals)
307
+ out_roff[t + 1] = out_roff[t] + iidx.shape[0]
308
+
309
+ # concatenate the indices and values
310
+ out_idx = torch.cat(out_idx, dim=-1)
311
+ out_vals = torch.cat(out_vals, dim=-1)
312
+
313
+ out_vals = _normalize_convolution_tensor_s2(
314
+ out_idx,
315
+ out_vals,
316
+ in_shape,
317
+ out_shape,
318
+ kernel_size,
319
+ quad_weights,
320
+ transpose_normalization=transpose_normalization,
321
+ basis_norm_mode=basis_norm_mode,
322
+ merge_quadrature=merge_quadrature,
323
+ )
324
+
325
+ out_idx = out_idx.contiguous()
326
+ out_vals = out_vals.to(dtype=torch.float32).contiguous()
327
+
328
+ return out_idx, out_vals, out_roff
329
+
330
+
331
+ class DiscreteContinuousConv(nn.Module, metaclass=abc.ABCMeta):
332
+ """
333
+ Abstract base class for discrete-continuous convolutions
334
+
335
+ Parameters
336
+ -----------
337
+ in_channels: int
338
+ Number of input channels
339
+ out_channels: int
340
+ Number of output channels
341
+ kernel_shape: Union[int, Tuple[int], Tuple[int, int]]
342
+ Shape of the kernel
343
+ basis_type: Optional[str]
344
+ Type of the basis functions
345
+ groups: Optional[int]
346
+ Number of groups
347
+ bias: Optional[bool]
348
+ Whether to use bias
349
+
350
+ Returns
351
+ -------
352
+ out: torch.Tensor
353
+ Output tensor
354
+ """
355
+
356
+ def __init__(
357
+ self,
358
+ in_channels: int,
359
+ out_channels: int,
360
+ kernel_shape: Union[int, Tuple[int], Tuple[int, int]],
361
+ basis_type: Optional[str] = "piecewise linear",
362
+ groups: Optional[int] = 1,
363
+ bias: Optional[bool] = True,
364
+ ):
365
+ super().__init__()
366
+
367
+ self.kernel_shape = kernel_shape
368
+
369
+ # get the filter basis functions
370
+ self.filter_basis = get_filter_basis(kernel_shape=kernel_shape, basis_type=basis_type)
371
+
372
+ # groups
373
+ self.groups = groups
374
+
375
+ # weight tensor
376
+ if in_channels % self.groups != 0:
377
+ raise ValueError("Error, the number of input channels has to be an integer multiple of the group size")
378
+ if out_channels % self.groups != 0:
379
+ raise ValueError("Error, the number of output channels has to be an integer multiple of the group size")
380
+ self.groupsize = in_channels // self.groups
381
+ scale = math.sqrt(1.0 / self.groupsize / self.kernel_size)
382
+ self.weight = nn.Parameter(scale * torch.randn(out_channels, self.groupsize, self.kernel_size))
383
+
384
+ if bias:
385
+ self.bias = nn.Parameter(torch.zeros(out_channels))
386
+ else:
387
+ self.bias = None
388
+
389
+ @property
390
+ def kernel_size(self):
391
+ return self.filter_basis.kernel_size
392
+
393
+ @abc.abstractmethod
394
+ def forward(self, x: torch.Tensor):
395
+ raise NotImplementedError
396
+
397
+
398
+ class DiscreteContinuousConvS2(DiscreteContinuousConv):
399
+ """
400
+ Discrete-continuous (DISCO) convolutions on the 2-Sphere as described in [1].
401
+
402
+ Parameters
403
+ -----------
404
+ in_channels: int
405
+ Number of input channels
406
+ out_channels: int
407
+ Number of output channels
408
+ in_shape: Tuple[int]
409
+ Input shape of the convolution tensor
410
+ out_shape: Tuple[int]
411
+ Output shape of the convolution tensor
412
+ kernel_shape: Union[int, Tuple[int], Tuple[int, int]]
413
+ Shape of the kernel
414
+ basis_type: Optional[str]
415
+ Type of the basis functions
416
+ basis_norm_mode: Optional[str]
417
+ Mode for basis normalization
418
+ groups: Optional[int]
419
+ Number of groups
420
+ grid_in: Optional[str]
421
+ Input grid type
422
+ grid_out: Optional[str]
423
+ Output grid type
424
+ bias: Optional[bool]
425
+ Whether to use bias
426
+ theta_cutoff: Optional[float]
427
+ Theta cutoff for the filter basis functions
428
+
429
+ Returns
430
+ -------
431
+ out: torch.Tensor
432
+ Output tensor
433
+
434
+ References
435
+ ----------
436
+ [1] Ocampo, Price, McEwen, Scalable and equivariant spherical CNNs by discrete-continuous (DISCO) convolutions, ICLR (2023), arXiv:2209.13603
437
+ """
438
+
439
+ def __init__(
440
+ self,
441
+ in_channels: int,
442
+ out_channels: int,
443
+ in_shape: Tuple[int],
444
+ out_shape: Tuple[int],
445
+ kernel_shape: Union[int, Tuple[int], Tuple[int, int]],
446
+ basis_type: Optional[str] = "piecewise linear",
447
+ basis_norm_mode: Optional[str] = "mean",
448
+ groups: Optional[int] = 1,
449
+ grid_in: Optional[str] = "equiangular",
450
+ grid_out: Optional[str] = "equiangular",
451
+ bias: Optional[bool] = True,
452
+ theta_cutoff: Optional[float] = None,
453
+ ):
454
+ super().__init__(in_channels, out_channels, kernel_shape, basis_type, groups, bias)
455
+
456
+ self.nlat_in, self.nlon_in = in_shape
457
+ self.nlat_out, self.nlon_out = out_shape
458
+
459
+ # make sure the p-shift works by checking that longitudes are divisible
460
+ assert self.nlon_in % self.nlon_out == 0
461
+
462
+ # heuristic to compute theta cutoff based on the bandlimit of the input field and overlaps of the basis functions
463
+ if theta_cutoff is None:
464
+ theta_cutoff = torch.pi / float(self.nlat_out - 1)
465
+
466
+ if theta_cutoff <= 0.0:
467
+ raise ValueError("Error, theta_cutoff has to be positive.")
468
+
469
+ idx, vals, _ = _precompute_convolution_tensor_s2(
470
+ in_shape,
471
+ out_shape,
472
+ self.filter_basis,
473
+ grid_in=grid_in,
474
+ grid_out=grid_out,
475
+ theta_cutoff=theta_cutoff,
476
+ transpose_normalization=False,
477
+ basis_norm_mode=basis_norm_mode,
478
+ merge_quadrature=True,
479
+ )
480
+
481
+ # sort the values
482
+ ker_idx = idx[0, ...].contiguous()
483
+ row_idx = idx[1, ...].contiguous()
484
+ col_idx = idx[2, ...].contiguous()
485
+ vals = vals.contiguous()
486
+
487
+ if _cuda_extension_available:
488
+ # preprocessed data-structure for GPU kernel
489
+ roff_idx = preprocess_psi(self.kernel_size, self.nlat_out, ker_idx, row_idx, col_idx, vals).contiguous()
490
+ self.register_buffer("psi_roff_idx", roff_idx, persistent=False)
491
+
492
+ # save all datastructures
493
+ self.register_buffer("psi_ker_idx", ker_idx, persistent=False)
494
+ self.register_buffer("psi_row_idx", row_idx, persistent=False)
495
+ self.register_buffer("psi_col_idx", col_idx, persistent=False)
496
+ self.register_buffer("psi_vals", vals, persistent=False)
497
+
498
+ # also store psi as COO matrix just in case for torch input
499
+ self.psi = _get_psi(self.kernel_size, self.psi_idx, self.psi_vals, self.nlat_in, self.nlon_in, self.nlat_out, self.nlon_out)
500
+
501
+ def extra_repr(self):
502
+ return f"in_shape={(self.nlat_in, self.nlon_in)}, out_shape={(self.nlat_out, self.nlon_out)}, in_chans={self.groupsize * self.groups}, out_chans={self.weight.shape[0]}, filter_basis={self.filter_basis}, kernel_shape={self.kernel_shape}, groups={self.groups}"
503
+
504
+ @property
505
+ def psi_idx(self):
506
+ return torch.stack([self.psi_ker_idx, self.psi_row_idx, self.psi_col_idx], dim=0).contiguous()
507
+
508
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
509
+
510
+ if x.is_cuda and _cuda_extension_available:
511
+ x = _disco_s2_contraction_cuda(
512
+ x, self.psi_roff_idx, self.psi_ker_idx, self.psi_row_idx, self.psi_col_idx, self.psi_vals, self.kernel_size, self.nlat_out, self.nlon_out
513
+ )
514
+ else:
515
+ if x.is_cuda:
516
+ warn("couldn't find CUDA extension, falling back to slow PyTorch implementation")
517
+ x = _disco_s2_contraction_torch(x, self.psi.to(x.device), self.nlon_out)
518
+
519
+ # extract shape
520
+ B, C, K, H, W = x.shape
521
+ x = x.reshape(B, self.groups, self.groupsize, K, H, W)
522
+
523
+ # do weight multiplication
524
+ out = torch.einsum("bgckxy,gock->bgoxy", x, self.weight.reshape(self.groups, -1, self.weight.shape[1], self.weight.shape[2])).contiguous()
525
+ out = out.reshape(B, -1, H, W)
526
+
527
+ if self.bias is not None:
528
+ out = out + self.bias.reshape(1, -1, 1, 1)
529
+
530
+ return out
531
+
532
+
533
+ class DiscreteContinuousConvTransposeS2(DiscreteContinuousConv):
534
+ """
535
+ Discrete-continuous (DISCO) transpose convolutions on the 2-Sphere as described in [1].
536
+
537
+ Parameters
538
+ -----------
539
+ in_channels: int
540
+ Number of input channels
541
+ out_channels: int
542
+ Number of output channels
543
+ in_shape: Tuple[int]
544
+ Input shape of the convolution tensor
545
+ out_shape: Tuple[int]
546
+ Output shape of the convolution tensor
547
+ kernel_shape: Union[int, Tuple[int], Tuple[int, int]]
548
+ Shape of the kernel
549
+ basis_type: Optional[str]
550
+ Type of the basis functions
551
+ basis_norm_mode: Optional[str]
552
+ Mode for basis normalization
553
+ groups: Optional[int]
554
+ Number of groups
555
+ grid_in: Optional[str]
556
+ Input grid type
557
+ grid_out: Optional[str]
558
+ Output grid type
559
+ bias: Optional[bool]
560
+ Whether to use bias
561
+ theta_cutoff: Optional[float]
562
+ Theta cutoff for the filter basis functions
563
+
564
+ Returns
565
+ --------
566
+ out: torch.Tensor
567
+ Output tensor
568
+
569
+ References
570
+ ----------
571
+ [1] Ocampo, Price, McEwen, Scalable and equivariant spherical CNNs by discrete-continuous (DISCO) convolutions, ICLR (2023), arXiv:2209.13603
572
+ """
573
+
574
+ def __init__(
575
+ self,
576
+ in_channels: int,
577
+ out_channels: int,
578
+ in_shape: Tuple[int],
579
+ out_shape: Tuple[int],
580
+ kernel_shape: Union[int, Tuple[int], Tuple[int, int]],
581
+ basis_type: Optional[str] = "piecewise linear",
582
+ basis_norm_mode: Optional[str] = "mean",
583
+ groups: Optional[int] = 1,
584
+ grid_in: Optional[str] = "equiangular",
585
+ grid_out: Optional[str] = "equiangular",
586
+ bias: Optional[bool] = True,
587
+ theta_cutoff: Optional[float] = None,
588
+ ):
589
+ super().__init__(in_channels, out_channels, kernel_shape, basis_type, groups, bias)
590
+
591
+ self.nlat_in, self.nlon_in = in_shape
592
+ self.nlat_out, self.nlon_out = out_shape
593
+
594
+ # make sure the p-shift works by checking that longitudes are divisible
595
+ assert self.nlon_out % self.nlon_in == 0
596
+
597
+ # bandlimit
598
+ if theta_cutoff is None:
599
+ theta_cutoff = torch.pi / float(self.nlat_in - 1)
600
+
601
+ if theta_cutoff <= 0.0:
602
+ raise ValueError("Error, theta_cutoff has to be positive.")
603
+
604
+ # switch in_shape and out_shape since we want the transpose convolution
605
+ idx, vals, _ = _precompute_convolution_tensor_s2(
606
+ out_shape,
607
+ in_shape,
608
+ self.filter_basis,
609
+ grid_in=grid_out,
610
+ grid_out=grid_in,
611
+ theta_cutoff=theta_cutoff,
612
+ transpose_normalization=True,
613
+ basis_norm_mode=basis_norm_mode,
614
+ merge_quadrature=True,
615
+ )
616
+
617
+ # sort the values
618
+ ker_idx = idx[0, ...].contiguous()
619
+ row_idx = idx[1, ...].contiguous()
620
+ col_idx = idx[2, ...].contiguous()
621
+ vals = vals.contiguous()
622
+
623
+ if _cuda_extension_available:
624
+ # preprocessed data-structure for GPU kernel
625
+ roff_idx = preprocess_psi(self.kernel_size, self.nlat_in, ker_idx, row_idx, col_idx, vals).contiguous()
626
+ self.register_buffer("psi_roff_idx", roff_idx, persistent=False)
627
+
628
+ # save all datastructures
629
+ self.register_buffer("psi_ker_idx", ker_idx, persistent=False)
630
+ self.register_buffer("psi_row_idx", row_idx, persistent=False)
631
+ self.register_buffer("psi_col_idx", col_idx, persistent=False)
632
+ self.register_buffer("psi_vals", vals, persistent=False)
633
+
634
+ # also store psi just in case
635
+ self.psi_st = _get_psi(self.kernel_size, self.psi_idx, self.psi_vals, self.nlat_in, self.nlon_in, self.nlat_out, self.nlon_out, semi_transposed=True)
636
+
637
+ def extra_repr(self):
638
+ return f"in_shape={(self.nlat_in, self.nlon_in)}, out_shape={(self.nlat_out, self.nlon_out)}, in_chans={self.groupsize * self.groups}, out_chans={self.weight.shape[0]}, filter_basis={self.filter_basis}, kernel_shape={self.kernel_shape}, groups={self.groups}"
639
+
640
+ @property
641
+ def psi_idx(self):
642
+ return torch.stack([self.psi_ker_idx, self.psi_row_idx, self.psi_col_idx], dim=0).contiguous()
643
+
644
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
645
+
646
+ # extract shape
647
+ B, C, H, W = x.shape
648
+ x = x.reshape(B, self.groups, self.groupsize, H, W)
649
+
650
+ # do weight multiplication
651
+ x = torch.einsum("bgcxy,gock->bgokxy", x, self.weight.reshape(self.groups, -1, self.weight.shape[1], self.weight.shape[2])).contiguous()
652
+ x = x.reshape(B, -1, x.shape[-3], H, W)
653
+
654
+ if x.is_cuda and _cuda_extension_available:
655
+ out = _disco_s2_transpose_contraction_cuda(
656
+ x, self.psi_roff_idx, self.psi_ker_idx, self.psi_row_idx, self.psi_col_idx, self.psi_vals, self.kernel_size, self.nlat_out, self.nlon_out
657
+ )
658
+ else:
659
+ if x.is_cuda:
660
+ warn("couldn't find CUDA extension, falling back to slow PyTorch implementation")
661
+ out = _disco_s2_transpose_contraction_torch(x, self.psi_st.to(x.device), self.nlon_out)
662
+
663
+ if self.bias is not None:
664
+ out = out + self.bias.reshape(1, -1, 1, 1)
665
+
666
+ return out
.deps/torch_harmonics/csrc/attention/attention.cuh ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // coding=utf-8
2
+ //
3
+ // SPDX-FileCopyrightText: Copyright (c) 2025 The torch-harmonics Authors. All rights reserved.
4
+ // SPDX-License-Identifier: BSD-3-Clause
5
+ //
6
+ // Redistribution and use in source and binary forms, with or without
7
+ // modification, are permitted provided that the following conditions are met:
8
+ //
9
+ // 1. Redistributions of source code must retain the above copyright notice, this
10
+ // list of conditions and the following disclaimer.
11
+ //
12
+ // 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ // this list of conditions and the following disclaimer in the documentation
14
+ // and/or other materials provided with the distribution.
15
+ //
16
+ // 3. Neither the name of the copyright holder nor the names of its
17
+ // contributors may be used to endorse or promote products derived from
18
+ // this software without specific prior written permission.
19
+ //
20
+ // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+
31
+ #pragma once
32
+
33
+ #include <cmath>
34
+ #include <cstdint>
35
+ #include <torch/torch.h>
36
+
37
+ #define CHECK_CUDA_TENSOR(x) TORCH_INTERNAL_ASSERT(x.device().type() == torch::kCUDA)
38
+ #define CHECK_CONTIGUOUS_TENSOR(x) TORCH_INTERNAL_ASSERT(x.is_contiguous() || x.is_contiguous(at::MemoryFormat::ChannelsLast))
39
+ #define CHECK_CUDA_INPUT_TENSOR(x) \
40
+ CHECK_CUDA_TENSOR(x); \
41
+ CHECK_CONTIGUOUS_TENSOR(x)
42
+
43
+ torch::Tensor s2_attention_fwd_cuda(at::Tensor kx, at::Tensor vx, at::Tensor qy, at::Tensor quad_weights,
44
+ at::Tensor psi_col_idx, at::Tensor psi_row_off, int nlon_in, int nlat_out,
45
+ int nlon_out);
46
+
47
+ std::tuple<at::Tensor, at::Tensor, at::Tensor> s2_attention_bwd_dkvq_cuda(at::Tensor kx, at::Tensor vx, at::Tensor qy,
48
+ at::Tensor dy, at::Tensor quad_weights,
49
+ at::Tensor psi_col_idx, at::Tensor psi_row_off,
50
+ int nlon_in, int nlat_out, int nlon_out);
.deps/torch_harmonics/csrc/attention/attention_bwd_cuda.cu ADDED
@@ -0,0 +1,960 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // coding=utf-8
2
+ //
3
+ // SPDX-FileCopyrightText: Copyright (c) 2025 The torch-harmonics Authors. All rights reserved.
4
+ // SPDX-License-Identifier: BSD-3-Clause
5
+ //
6
+ // Redistribution and use in source and binary forms, with or without
7
+ // modification, are permitted provided that the following conditions are met:
8
+ //
9
+ // 1. Redistributions of source code must retain the above copyright notice, this
10
+ // list of conditions and the following disclaimer.
11
+ //
12
+ // 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ // this list of conditions and the following disclaimer in the documentation
14
+ // and/or other materials provided with the distribution.
15
+ //
16
+ // 3. Neither the name of the copyright holder nor the names of its
17
+ // contributors may be used to endorse or promote products derived from
18
+ // this software without specific prior written permission.
19
+ //
20
+ // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+
31
+ #include "attention.cuh"
32
+ #include "c10/core/MemoryFormat.h"
33
+
34
+ #include <ATen/core/TensorAccessor.h>
35
+ #include <ATen/cuda/detail/TensorInfo.cuh>
36
+ #include <ATen/cuda/detail/KernelUtils.h>
37
+ #include <ATen/cuda/detail/IndexUtils.cuh>
38
+ #include <ATen/cuda/CUDAUtils.h>
39
+
40
+ #include <ctime>
41
+ #include <cub/cub.cuh>
42
+ #include <limits>
43
+
44
+ #include "cudamacro.h"
45
+ #include "attention_utils.cuh"
46
+
47
+ #include <iostream>
48
+ #include <chrono>
49
+ #include <string>
50
+
51
+ #define THREADS (64)
52
+
53
+ #define MAX_LOCAL_ARR_LEN (16)
54
+
55
+ #if 0
56
+ class ScopeTimer
57
+ {
58
+ public:
59
+ explicit ScopeTimer(const std::string &label = "") :
60
+ label_(label), start_(std::chrono::high_resolution_clock::now())
61
+ {
62
+ }
63
+
64
+ ~ScopeTimer()
65
+ {
66
+ auto end = std::chrono::high_resolution_clock::now();
67
+ auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(end - start_);
68
+ std::cout << label_ << "Elapsed time: " << elapsed.count() << " ms" << std::endl;
69
+ }
70
+
71
+ private:
72
+ std::string label_;
73
+ std::chrono::high_resolution_clock::time_point start_;
74
+ };
75
+
76
+ // easier to understand version of manual shfl_xor_sync, performance appears similar
77
+ static __device__ float __warp_sum_cub(float val)
78
+ {
79
+ // use cub to reduce within a warp
80
+ __shared__ typename cub::WarpReduce<float>::TempStorage temp_storage;
81
+
82
+ // 1. Compute sum (initially only in lane 0)
83
+ float sum = cub::WarpReduce<float>(temp_storage).Sum(val);
84
+ // 2. Broadcast sum to all threads
85
+ sum = __shfl_sync(0xFFFFFFFF, sum, 0);
86
+ return sum;
87
+ }
88
+
89
+ // This kernel computes the backward pass for the S2 attention mechanism, using
90
+ // shared memory as a cache and one warp per output point, warp-parallel over
91
+ // channels, which should be layed out in the fastest dimension for coalesced
92
+ // memory access.
93
+ template <int BDIM_X>
94
+ __global__ __launch_bounds__(BDIM_X) void s2_attention_bwd_dkvq_kernel(
95
+ int num_channels, int nlon_in, int nlat_out, int nlon_out,
96
+ const torch::PackedTensorAccessor32<float, 4, torch::RestrictPtrTraits> kx,
97
+ const torch::PackedTensorAccessor32<float, 4, torch::RestrictPtrTraits> vx,
98
+ const torch::PackedTensorAccessor32<float, 4, torch::RestrictPtrTraits> qy,
99
+ const torch::PackedTensorAccessor32<float, 4, torch::RestrictPtrTraits> dy,
100
+ torch::PackedTensorAccessor32<float, 4, torch::RestrictPtrTraits> dydk,
101
+ torch::PackedTensorAccessor32<float, 4, torch::RestrictPtrTraits> dydv,
102
+ torch::PackedTensorAccessor32<float, 4, torch::RestrictPtrTraits> dydq,
103
+ const torch::PackedTensorAccessor64<int64_t, 1, torch::RestrictPtrTraits> psi_col_idx,
104
+ const torch::PackedTensorAccessor64<int64_t, 1, torch::RestrictPtrTraits> psi_row_offset,
105
+ const torch::PackedTensorAccessor32<float, 1, torch::RestrictPtrTraits> quad_weights)
106
+ {
107
+
108
+ extern __shared__ float sh[];
109
+ float *sh_alpha_k = sh + threadIdx.y * num_channels * 5;
110
+ float *sh_alpha_vw = sh_alpha_k + num_channels;
111
+ float *sh_alpha_kvw = sh_alpha_vw + num_channels;
112
+ float *sh_dy = sh_alpha_kvw + num_channels;
113
+ float *sh_qy = sh_dy + num_channels;
114
+ // (optionally, could use more shared memory for other intermediates)
115
+
116
+ const uint64_t batchId = blockIdx.y;
117
+ const uint64_t wid = uint64_t(blockIdx.x) * blockDim.y + threadIdx.y;
118
+ if (wid >= uint64_t(nlat_out) * nlon_in) return;
119
+ const int tidx = threadIdx.x;
120
+ const int ho = wid / nlon_out;
121
+ const int wo = wid - (ho * nlon_out);
122
+
123
+ // Zero shared memory
124
+ for (int chan = tidx; chan < num_channels; chan += WARP_SIZE) {
125
+ sh_alpha_k[chan] = 0.0f;
126
+ sh_alpha_vw[chan] = 0.0f;
127
+ sh_alpha_kvw[chan] = 0.0f;
128
+ sh_dy[chan] = dy[batchId][chan][ho][wo];
129
+ sh_qy[chan] = qy[batchId][chan][ho][wo];
130
+ }
131
+ float alpha_sum = 0.0f;
132
+ float qdotk_max = -FLT_MAX;
133
+ float integral = 0.0f;
134
+ __syncthreads();
135
+
136
+ const int64_t rbeg = psi_row_offset[ho];
137
+ const int64_t rend = psi_row_offset[ho + 1];
138
+ const int rlen = rend - rbeg;
139
+
140
+ // 1st pass: accumulate alpha_sum, integral, and shared stats, along with a progressively computed qdotk_max.
141
+ for (int off = 0; off < rlen; off++) {
142
+ const int64_t col = psi_col_idx[rbeg + off];
143
+ const int hi = col / nlon_in;
144
+ const int wi = col - (hi * nlon_in);
145
+ const int wip = (wi + wo) - ((wi + wo) / nlon_in) * nlon_in;
146
+ float qdotk = 0.0f, gdotv = 0.0f;
147
+ for (int chan = tidx; chan < num_channels; chan += WARP_SIZE) {
148
+ qdotk += sh_qy[chan] * kx[batchId][chan][hi][wip];
149
+ gdotv += sh_dy[chan] * vx[batchId][chan][hi][wip];
150
+ }
151
+ qdotk = __warp_sum_cub(qdotk);
152
+ gdotv = __warp_sum_cub(gdotv);
153
+ float qdotk_max_tmp = max(qdotk_max, qdotk);
154
+ float alpha_inz = expf(qdotk - qdotk_max_tmp) * quad_weights[hi];
155
+ float max_correction = expf(qdotk_max - qdotk_max_tmp);
156
+ alpha_sum = alpha_sum * max_correction + alpha_inz;
157
+ integral = integral * max_correction + alpha_inz * gdotv;
158
+ for (int chan = tidx; chan < num_channels; chan += WARP_SIZE) {
159
+ float kxval = kx[batchId][chan][hi][wip];
160
+ sh_alpha_k[chan] = sh_alpha_k[chan] * max_correction + alpha_inz * kxval;
161
+ sh_alpha_vw[chan] = sh_alpha_vw[chan] * max_correction + alpha_inz * gdotv;
162
+ sh_alpha_kvw[chan] = sh_alpha_kvw[chan] * max_correction + alpha_inz * kxval * gdotv;
163
+ }
164
+ qdotk_max = qdotk_max_tmp;
165
+ }
166
+
167
+ integral /= alpha_sum;
168
+
169
+ // Write dydq
170
+ for (int chan = tidx; chan < num_channels; chan += WARP_SIZE) {
171
+ dydq[batchId][chan][ho][wo]
172
+ = (sh_alpha_kvw[chan] * alpha_sum - sh_alpha_vw[chan] * sh_alpha_k[chan]) / (alpha_sum * alpha_sum);
173
+ }
174
+
175
+ // Third pass: accumulate gradients for k and v
176
+ for (int off = 0; off < rlen; off++) {
177
+ const int64_t col = psi_col_idx[rbeg + off];
178
+ const int hi = col / nlon_in;
179
+ const int wi = col - (hi * nlon_in);
180
+ const int wip = (wi + wo) - ((wi + wo) / nlon_in) * nlon_in;
181
+ float qdotk = 0.0f, gdotv = 0.0f;
182
+ for (int chan = tidx; chan < num_channels; chan += WARP_SIZE) {
183
+ qdotk += qy[batchId][chan][ho][wo] * kx[batchId][chan][hi][wip];
184
+ gdotv += sh_dy[chan] * vx[batchId][chan][hi][wip];
185
+ }
186
+ qdotk = __warp_sum_cub(qdotk);
187
+ gdotv = __warp_sum_cub(gdotv);
188
+ float alpha_inz = expf(qdotk - qdotk_max) * quad_weights[hi];
189
+ for (int chan = tidx; chan < num_channels; chan += WARP_SIZE) {
190
+ float qyval = qy[batchId][chan][ho][wo];
191
+ float dyval = sh_dy[chan];
192
+ atomicAdd(&dydk[batchId][chan][hi][wip], qyval * (alpha_inz / alpha_sum) * (gdotv - integral));
193
+ atomicAdd(&dydv[batchId][chan][hi][wip], (alpha_inz / alpha_sum) * dyval);
194
+ }
195
+ }
196
+ }
197
+ #endif
198
+
199
+ // BEGIN backward kernels and functions
200
+
201
+ // called with (blockDim.x=32 and blockDim.y>1, BDIM=blockDim.x*blockDim.y)
202
+ template<int BDIM_X,
203
+ typename FLOATV_T> // either float or float4
204
+ __global__
205
+ __launch_bounds__(BDIM_X)
206
+ void s2_attn_bwd_generic_vec_k(int nchan, // no. of FLOATV_T elements along channel dim
207
+ int nlat_in,
208
+ int nlon_in,
209
+ int nlat_out,
210
+ int nlon_out,
211
+ const FLOATV_T *__restrict__ kx,
212
+ const FLOATV_T *__restrict__ vx,
213
+ const FLOATV_T *__restrict__ qy,
214
+ const FLOATV_T *__restrict__ dy,
215
+ const int32_t *__restrict__ row_idx,
216
+ const int64_t *__restrict__ row_off,
217
+ const int64_t *__restrict__ col_idx,
218
+ const float *__restrict__ quad_weights,
219
+ FLOATV_T *__restrict__ dkx,
220
+ FLOATV_T *__restrict__ dvx,
221
+ FLOATV_T *__restrict__ dqy) {
222
+
223
+ extern __shared__ __align__(sizeof(float4)) float shext[];
224
+
225
+ // for dqy
226
+ FLOATV_T *sh_alpha_k__ = reinterpret_cast<FLOATV_T *>(shext) + threadIdx.y * nchan*5;
227
+ FLOATV_T *sh_alpha_vw_ = sh_alpha_k__ + nchan;
228
+ FLOATV_T *sh_alpha_kvw = sh_alpha_vw_ + nchan;
229
+
230
+ FLOATV_T *sh_dy = sh_alpha_kvw + nchan;
231
+ FLOATV_T *sh_qy = sh_dy + nchan;
232
+
233
+ const int batch = blockIdx.y;
234
+
235
+ const uint64_t wid = uint64_t(blockIdx.x) * blockDim.y + threadIdx.y;
236
+ if (wid >= uint64_t(nlat_out)*nlon_in) {
237
+ return;
238
+ }
239
+
240
+ const int tidx = threadIdx.x;
241
+
242
+ // use permuted rows
243
+ const int h = wid / nlon_out;
244
+ const int wo = wid - (h*nlon_out);
245
+ const int ho = row_idx[h];
246
+
247
+ // offset input tensors
248
+ kx += int64_t(batch)*nlat_in*nlon_in*nchan;
249
+ vx += int64_t(batch)*nlat_in*nlon_in*nchan;
250
+ qy += int64_t(batch)*nlat_out*nlon_out*nchan + int64_t(ho)*nlon_out*nchan + int64_t(wo)*nchan;
251
+ dy += int64_t(batch)*nlat_out*nlon_out*nchan + int64_t(ho)*nlon_out*nchan + int64_t(wo)*nchan;
252
+
253
+ // offset output tensors
254
+ dkx += int64_t(batch)*nlat_in*nlon_in*nchan;
255
+ dvx += int64_t(batch)*nlat_in*nlon_in*nchan;
256
+ dqy += int64_t(batch)*nlat_out*nlon_out*nchan + int64_t(ho)*nlon_out*nchan + int64_t(wo)*nchan;
257
+
258
+ // zero/init shared memory
259
+ for (int chan = tidx; chan < nchan; chan += WARP_SIZE) {
260
+
261
+ sh_alpha_k__[chan] = __vset<FLOATV_T>(0.0f);
262
+ sh_alpha_vw_[chan] = __vset<FLOATV_T>(0.0f);
263
+ sh_alpha_kvw[chan] = __vset<FLOATV_T>(0.0f);
264
+
265
+ sh_dy[chan] = dy[chan];
266
+ sh_qy[chan] = qy[chan];
267
+ }
268
+
269
+ #if __CUDA_ARCH__ < 900
270
+ // for architectures < 9.0, sh_dy and sh_qy will be read
271
+ // as individual floats at the end of the kernel, which
272
+ // breaks the assumption that each FLOATV_T location is
273
+ // written to and read by the same thread throughout the
274
+ // kernel, in the case FLOATV_T==float4
275
+ if constexpr(std::is_same<FLOATV_T, float4>::value) { __syncwarp(); }
276
+ #endif
277
+
278
+ // for dkx, dvx, dqy
279
+ float alpha_sum = 0.0f;
280
+ float qdotk_max = -FLT_MAX;
281
+
282
+ // for dkx
283
+ float integral = 0.0f;
284
+
285
+ const int64_t rbeg = row_off[ho];
286
+ const int64_t rend = row_off[ho+1];
287
+
288
+ col_idx += rbeg;
289
+
290
+ const int rlen = rend - rbeg;
291
+
292
+ // accumulate alpha_sum, integral, and shared stats,
293
+ // along with a progressively computed qdotk_max.
294
+ for (int off = 0; off < rlen; off++) {
295
+
296
+ const int64_t col = col_idx[off];
297
+
298
+ const int hi = col / nlon_in;
299
+ const int wi = col - (hi * nlon_in);
300
+ const int wip = (wi + wo) - ((wi + wo) / nlon_in) * nlon_in;
301
+
302
+ const FLOATV_T *_kx = kx + int64_t(hi)*nlon_in*nchan + int64_t(wip)*nchan;
303
+ const FLOATV_T *_vx = vx + int64_t(hi)*nlon_in*nchan + int64_t(wip)*nchan;
304
+
305
+ FLOATV_T qdotk_v = __vset<FLOATV_T>(0.0f);
306
+ FLOATV_T gdotv_v = __vset<FLOATV_T>(0.0f);
307
+
308
+ for(int chan = tidx; chan < nchan; chan += WARP_SIZE) {
309
+
310
+ qdotk_v = __vadd(qdotk_v, __vmul(sh_qy[chan], _kx[chan]));
311
+ gdotv_v = __vadd(gdotv_v, __vmul(sh_dy[chan], _vx[chan]));
312
+ }
313
+
314
+ const float qdotk = __warp_sum(__vred(qdotk_v));
315
+ const float gdotv = __warp_sum(__vred(gdotv_v));
316
+
317
+ const float qdotk_max_tmp = max(qdotk_max, qdotk);
318
+ const float alpha_inz = expf(qdotk - qdotk_max_tmp) * quad_weights[hi];
319
+ const float max_correction = expf(qdotk_max - qdotk_max_tmp);
320
+ alpha_sum = alpha_sum * max_correction + alpha_inz;
321
+
322
+ integral = integral * max_correction + alpha_inz * gdotv;
323
+
324
+ const float ainz_gdotv = alpha_inz * gdotv;
325
+
326
+ for (int chan = tidx; chan < nchan; chan += WARP_SIZE) {
327
+
328
+ const FLOATV_T kxval = _kx[chan];
329
+
330
+ sh_alpha_k__[chan] = __vadd(__vscale(max_correction, sh_alpha_k__[chan]), __vscale(alpha_inz, kxval));
331
+ sh_alpha_vw_[chan] = __vadd(__vscale(max_correction, sh_alpha_vw_[chan]), __vset<FLOATV_T>(ainz_gdotv));
332
+ sh_alpha_kvw[chan] = __vadd(__vscale(max_correction, sh_alpha_kvw[chan]), __vscale(ainz_gdotv, kxval));
333
+ }
334
+ qdotk_max = qdotk_max_tmp;
335
+ }
336
+
337
+ const float alpha_sum_inv = 1.0f / alpha_sum;
338
+
339
+ integral *= alpha_sum_inv;
340
+
341
+ // Write dqy
342
+ for (int chan = tidx; chan < nchan; chan += WARP_SIZE) {
343
+
344
+ dqy[chan] = __vscale(alpha_sum_inv * alpha_sum_inv,
345
+ __vsub(__vscale(alpha_sum, sh_alpha_kvw[chan]),
346
+ __vmul(sh_alpha_vw_[chan], sh_alpha_k__[chan])));
347
+ }
348
+
349
+ // accumulate gradients for k and v
350
+ for (int off = 0; off < rlen; off++) {
351
+
352
+ const int64_t col = col_idx[off];
353
+ const int hi = col / nlon_in;
354
+ const int wi = col - (hi * nlon_in);
355
+ const int wip = (wi + wo) - ((wi + wo) / nlon_in) * nlon_in;
356
+
357
+ const FLOATV_T *_kx = kx + int64_t(hi)*nlon_in*nchan + int64_t(wip)*nchan;
358
+ const FLOATV_T *_vx = vx + int64_t(hi)*nlon_in*nchan + int64_t(wip)*nchan;
359
+
360
+ FLOATV_T qdotk_v = __vset<FLOATV_T>(0.0f);
361
+ FLOATV_T gdotv_v = __vset<FLOATV_T>(0.0f);
362
+
363
+ for (int chan = tidx; chan < nchan; chan += WARP_SIZE) {
364
+
365
+ qdotk_v = __vadd(qdotk_v, __vmul(sh_qy[chan], _kx[chan]));
366
+ gdotv_v = __vadd(gdotv_v, __vmul(sh_dy[chan], _vx[chan]));
367
+ }
368
+
369
+ const float qdotk = __warp_sum(__vred(qdotk_v));
370
+ const float gdotv = __warp_sum(__vred(gdotv_v));
371
+
372
+ const float alpha_inz = expf(qdotk - qdotk_max) * quad_weights[hi];
373
+
374
+ FLOATV_T *_dkx = dkx + int64_t(hi)*nlon_in*nchan + int64_t(wip)*nchan;
375
+ FLOATV_T *_dvx = dvx + int64_t(hi)*nlon_in*nchan + int64_t(wip)*nchan;
376
+
377
+ const float alpha_mul = alpha_inz * alpha_sum_inv;
378
+
379
+ const float scale_fact_qy = (gdotv - integral)*alpha_mul;
380
+ const float scale_fact_dy = alpha_mul;
381
+
382
+ // float4, 128-bit atomics are only supported by devices of compute
383
+ // capability 9.x+, so on older devices we resort to 32-bit atomics
384
+
385
+ #if __CUDA_ARCH__ < 900
386
+ // to use 32-bit operations on consecutve addresses
387
+ float *sh_qy_scl = reinterpret_cast<float *>(sh_qy);
388
+ float *sh_dy_scl = reinterpret_cast<float *>(sh_dy);
389
+
390
+ float *_dkx_scl = reinterpret_cast<float *>(_dkx);
391
+ float *_dvx_scl = reinterpret_cast<float *>(_dvx);
392
+
393
+ constexpr int VEC_SIZE = sizeof(FLOATV_T)/sizeof(float);
394
+
395
+ // 32-bit, consecutive atomics to glmem;
396
+ // strided atomics results in a severe slowdown
397
+ for (int chan = tidx; chan < nchan*VEC_SIZE; chan += WARP_SIZE) {
398
+
399
+ atomicAdd(_dkx_scl + chan, scale_fact_qy * sh_qy_scl[chan]);
400
+ atomicAdd(_dvx_scl + chan, scale_fact_dy * sh_dy_scl[chan]);
401
+ }
402
+ #else
403
+ // 128-bit, consecutive atomics to glmem
404
+ for (int chan = tidx; chan < nchan; chan += WARP_SIZE) {
405
+
406
+ atomicAdd(_dkx + chan, __vscale(scale_fact_qy, sh_qy[chan]));
407
+ atomicAdd(_dvx + chan, __vscale(scale_fact_dy, sh_dy[chan]));
408
+ }
409
+ #endif
410
+ }
411
+
412
+ return;
413
+ }
414
+
415
+ // called with either (BDIM_X=32 and BDIM_Y>1) || (2^K=BDIM_X > 32 and BDIM_Y=1)
416
+ template<int BDIM_X,
417
+ int BDIM_Y,
418
+ int NLOC,
419
+ typename FLOATV_T> // either float or float4
420
+ __global__
421
+ __launch_bounds__(BDIM_X*BDIM_Y)
422
+ void s2_attn_bwd_special_vec_k(int nchan, // no. of FLOATV_T elements along channel dim
423
+ int nlat_in,
424
+ int nlon_in,
425
+ int nlat_out,
426
+ int nlon_out,
427
+ const FLOATV_T *__restrict__ kx,
428
+ const FLOATV_T *__restrict__ vx,
429
+ const FLOATV_T *__restrict__ qy,
430
+ const FLOATV_T *__restrict__ dy,
431
+ const int32_t *__restrict__ row_idx,
432
+ const int64_t *__restrict__ row_off,
433
+ const int64_t *__restrict__ col_idx,
434
+ const float *__restrict__ quad_weights,
435
+ FLOATV_T *__restrict__ dkx,
436
+ FLOATV_T *__restrict__ dvx,
437
+ FLOATV_T *__restrict__ dqy) {
438
+
439
+ static_assert(0 == (BDIM_X & (BDIM_X-1)));
440
+ static_assert(0 == (BDIM_Y & (BDIM_Y-1)));
441
+ static_assert((BDIM_X == 32 && BDIM_Y > 1) ||
442
+ (BDIM_X > 32 && BDIM_Y == 1)) ;
443
+
444
+ constexpr int NLOC_M1 = NLOC-1;
445
+
446
+ const int tidx = threadIdx.x;
447
+ const int batch = blockIdx.y;
448
+ const uint64_t ctaid = uint64_t(blockIdx.x) * blockDim.y + threadIdx.y;
449
+
450
+ if (ctaid >= uint64_t(nlat_out)*nlon_in) {
451
+ return;
452
+ }
453
+
454
+ extern __shared__ __align__(sizeof(float4)) float shext[];
455
+
456
+ FLOATV_T *sh_dy = reinterpret_cast<FLOATV_T *>(shext) + threadIdx.y*nchan*2 + tidx;
457
+ FLOATV_T *sh_qy = sh_dy + nchan;
458
+
459
+ // for dqy
460
+ FLOATV_T loc_k__[NLOC];
461
+ FLOATV_T loc_vw_[NLOC];
462
+ FLOATV_T loc_kvw[NLOC];
463
+
464
+ // use permuted rows
465
+ const int h = ctaid / nlon_out;
466
+ const int wo = ctaid - (h*nlon_out);
467
+ const int ho = row_idx[h];
468
+
469
+ // offset input tensors
470
+ kx += int64_t(batch)*nlat_in*nlon_in*nchan + tidx;
471
+ vx += int64_t(batch)*nlat_in*nlon_in*nchan + tidx;
472
+ qy += int64_t(batch)*nlat_out*nlon_out*nchan + int64_t(ho)*nlon_out*nchan + int64_t(wo)*nchan + tidx;
473
+ dy += int64_t(batch)*nlat_out*nlon_out*nchan + int64_t(ho)*nlon_out*nchan + int64_t(wo)*nchan + tidx;
474
+
475
+ // offset output tensors
476
+ dkx += int64_t(batch)*nlat_in*nlon_in*nchan + tidx;
477
+ dvx += int64_t(batch)*nlat_in*nlon_in*nchan + tidx;
478
+ dqy += int64_t(batch)*nlat_out*nlon_out*nchan + int64_t(ho)*nlon_out*nchan + int64_t(wo)*nchan + tidx;
479
+
480
+ #pragma unroll
481
+ for(int i = 0; i < NLOC; i++) {
482
+ loc_k__[i] = __vset<FLOATV_T>(0.0f);
483
+ loc_vw_[i] = __vset<FLOATV_T>(0.0f);
484
+ loc_kvw[i] = __vset<FLOATV_T>(0.0f);
485
+ }
486
+
487
+ #pragma unroll
488
+ for(int i = 0; i < NLOC_M1; i++) {
489
+ sh_dy[i*BDIM_X] = dy[i*BDIM_X];
490
+ sh_qy[i*BDIM_X] = qy[i*BDIM_X];
491
+ }
492
+ if (NLOC_M1*BDIM_X+tidx < nchan) {
493
+ sh_dy[NLOC_M1*BDIM_X] = dy[NLOC_M1*BDIM_X];
494
+ sh_qy[NLOC_M1*BDIM_X] = qy[NLOC_M1*BDIM_X];
495
+ }
496
+
497
+ #if __CUDA_ARCH__ < 900
498
+ // for architectures < 9.0, sh_dy and sh_qy will be read
499
+ // as individual floats at the end of the kernel, which
500
+ // breaks the assumption that each FLOATV_T location is
501
+ // written to and read by the same thread throughout the
502
+ // kernel, in the case FLOATV_T==float4
503
+ if constexpr(std::is_same<FLOATV_T, float4>::value) {
504
+ if constexpr(BDIM_X == 32) { __syncwarp(); }
505
+ else { __syncthreads(); }
506
+ }
507
+ #endif
508
+
509
+ // for dkx, dvx, dqy
510
+ float alpha_sum = 0.0f;
511
+ float qdotk_max = -FLT_MAX;
512
+
513
+ // for dkx
514
+ float integral = 0.0f;
515
+
516
+ const int64_t rbeg = row_off[ho];
517
+ const int64_t rend = row_off[ho+1];
518
+
519
+ col_idx += rbeg;
520
+
521
+ const int rlen = rend - rbeg;
522
+
523
+ // accumulate alpha_sum, integral, and shared stats,
524
+ // along with a progressively computed qdotk_max.
525
+ for (int off = 0; off < rlen; off++) {
526
+
527
+ const int64_t col = col_idx[off];
528
+
529
+ const int hi = col / nlon_in;
530
+ const int wi = col - (hi * nlon_in);
531
+ const int wip = (wi + wo) - ((wi + wo) / nlon_in) * nlon_in;
532
+
533
+ const FLOATV_T *_kx = kx + int64_t(hi)*nlon_in*nchan + int64_t(wip)*nchan;
534
+ const FLOATV_T *_vx = vx + int64_t(hi)*nlon_in*nchan + int64_t(wip)*nchan;
535
+
536
+ FLOATV_T qdotk_v = __vset<FLOATV_T>(0.0f);
537
+ FLOATV_T gdotv_v = __vset<FLOATV_T>(0.0f);
538
+
539
+ #pragma unroll
540
+ for(int i = 0; i < NLOC_M1; i++) {
541
+ qdotk_v = __vadd(qdotk_v, __vmul(sh_qy[i*BDIM_X], _kx[i*BDIM_X]));
542
+ gdotv_v = __vadd(gdotv_v, __vmul(sh_dy[i*BDIM_X], _vx[i*BDIM_X]));
543
+ }
544
+ if (NLOC_M1*BDIM_X+tidx < nchan) {
545
+ qdotk_v = __vadd(qdotk_v, __vmul(sh_qy[NLOC_M1*BDIM_X], _kx[NLOC_M1*BDIM_X]));
546
+ gdotv_v = __vadd(gdotv_v, __vmul(sh_dy[NLOC_M1*BDIM_X], _vx[NLOC_M1*BDIM_X]));
547
+ }
548
+
549
+ float qdotk = __vred(qdotk_v);
550
+ float gdotv = __vred(gdotv_v);
551
+
552
+ if constexpr(BDIM_X == 32) {
553
+ qdotk = __warp_sum(qdotk);
554
+ gdotv = __warp_sum(gdotv);
555
+ } else {
556
+ qdotk = __block_sum<BDIM_X>(qdotk);
557
+ gdotv = __block_sum<BDIM_X>(gdotv);
558
+ }
559
+
560
+ const float qdotk_max_tmp = max(qdotk_max, qdotk);
561
+ const float alpha_inz = expf(qdotk - qdotk_max_tmp) * quad_weights[hi];
562
+ const float max_correction = expf(qdotk_max - qdotk_max_tmp);
563
+
564
+ alpha_sum = alpha_sum * max_correction + alpha_inz;
565
+ integral = integral * max_correction + alpha_inz * gdotv;
566
+
567
+ const float ainz_gdotv = alpha_inz * gdotv;
568
+
569
+ #pragma unroll
570
+ for(int i = 0; i < NLOC_M1; i++) {
571
+ const FLOATV_T kxval = _kx[i*BDIM_X];
572
+ loc_k__[i] = __vadd(__vscale(max_correction, loc_k__[i]), __vscale(alpha_inz, kxval));
573
+ loc_vw_[i] = __vadd(__vscale(max_correction, loc_vw_[i]), __vset<FLOATV_T>(ainz_gdotv));
574
+ loc_kvw[i] = __vadd(__vscale(max_correction, loc_kvw[i]), __vscale(ainz_gdotv, kxval));
575
+ }
576
+ if (NLOC_M1*BDIM_X+tidx < nchan) {
577
+ const FLOATV_T kxval = _kx[NLOC_M1*BDIM_X];
578
+ loc_k__[NLOC_M1] = __vadd(__vscale(max_correction, loc_k__[NLOC_M1]), __vscale(alpha_inz, kxval));
579
+ loc_vw_[NLOC_M1] = __vadd(__vscale(max_correction, loc_vw_[NLOC_M1]), __vset<FLOATV_T>(ainz_gdotv));
580
+ loc_kvw[NLOC_M1] = __vadd(__vscale(max_correction, loc_kvw[NLOC_M1]), __vscale(ainz_gdotv, kxval));
581
+ }
582
+
583
+ qdotk_max = qdotk_max_tmp;
584
+ }
585
+
586
+ const float alpha_sum_inv = 1.0f / alpha_sum;
587
+
588
+ integral *= alpha_sum_inv;
589
+
590
+ // Write dqy
591
+ const float alpha_sum_inv_sq = alpha_sum_inv*alpha_sum_inv;
592
+
593
+ #pragma unroll
594
+ for(int i = 0; i < NLOC_M1; i++) {
595
+ dqy[i*BDIM_X] = __vscale(alpha_sum_inv_sq,
596
+ __vsub(__vscale(alpha_sum, loc_kvw[i]),
597
+ __vmul(loc_vw_[i], loc_k__[i])));
598
+ }
599
+ if (NLOC_M1*BDIM_X+tidx < nchan) {
600
+ dqy[NLOC_M1*BDIM_X] = __vscale(alpha_sum_inv_sq,
601
+ __vsub(__vscale(alpha_sum, loc_kvw[NLOC_M1]),
602
+ __vmul(loc_vw_[NLOC_M1], loc_k__[NLOC_M1])));
603
+ }
604
+
605
+ // accumulate gradients for k and v
606
+ for (int off = 0; off < rlen; off++) {
607
+
608
+ const int64_t col = col_idx[off];
609
+
610
+ const int hi = col / nlon_in;
611
+ const int wi = col - (hi * nlon_in);
612
+ const int wip = (wi + wo) - ((wi + wo) / nlon_in) * nlon_in;
613
+
614
+ const FLOATV_T *_kx = kx + int64_t(hi)*nlon_in*nchan + int64_t(wip)*nchan;
615
+ const FLOATV_T *_vx = vx + int64_t(hi)*nlon_in*nchan + int64_t(wip)*nchan;
616
+
617
+ FLOATV_T qdotk_v = __vset<FLOATV_T>(0.0f);
618
+ FLOATV_T gdotv_v = __vset<FLOATV_T>(0.0f);
619
+
620
+ #pragma unroll
621
+ for(int i = 0; i < NLOC_M1; i++) {
622
+ qdotk_v = __vadd(qdotk_v, __vmul(sh_qy[i*BDIM_X], _kx[i*BDIM_X]));
623
+ gdotv_v = __vadd(gdotv_v, __vmul(sh_dy[i*BDIM_X], _vx[i*BDIM_X]));
624
+ }
625
+ if (NLOC_M1*BDIM_X+tidx < nchan) {
626
+ qdotk_v = __vadd(qdotk_v, __vmul(sh_qy[NLOC_M1*BDIM_X], _kx[NLOC_M1*BDIM_X]));
627
+ gdotv_v = __vadd(gdotv_v, __vmul(sh_dy[NLOC_M1*BDIM_X], _vx[NLOC_M1*BDIM_X]));
628
+ }
629
+
630
+ float qdotk = __vred(qdotk_v);
631
+ float gdotv = __vred(gdotv_v);
632
+
633
+ if constexpr(BDIM_X == 32) {
634
+ qdotk = __warp_sum(qdotk);
635
+ gdotv = __warp_sum(gdotv);
636
+ } else {
637
+ qdotk = __block_sum<BDIM_X>(qdotk);
638
+ gdotv = __block_sum<BDIM_X>(gdotv);
639
+ }
640
+
641
+ const float alpha_inz = expf(qdotk - qdotk_max) * quad_weights[hi];
642
+
643
+ FLOATV_T *_dkx = dkx + int64_t(hi)*nlon_in*nchan + int64_t(wip)*nchan;
644
+ FLOATV_T *_dvx = dvx + int64_t(hi)*nlon_in*nchan + int64_t(wip)*nchan;
645
+
646
+ const float alpha_mul = alpha_inz * alpha_sum_inv;
647
+
648
+ const float scale_fact_qy = (gdotv - integral)*alpha_mul;
649
+ const float scale_fact_dy = alpha_mul;
650
+
651
+ // float4, 128-bit atomics are only supported by devices of compute
652
+ // capability 9.x+, so on older devices we resort to 32-bit atomics
653
+
654
+ #if __CUDA_ARCH__ < 900
655
+ // making the loop count known at compile time doesn't seem
656
+ // to make any difference here so let's keep this (much)
657
+ // simpler version
658
+ float *sh_qy_scl = reinterpret_cast<float *>(sh_qy - tidx);
659
+ float *sh_dy_scl = reinterpret_cast<float *>(sh_dy - tidx);
660
+
661
+ float *_dkx_scl = reinterpret_cast<float *>(_dkx - tidx);
662
+ float *_dvx_scl = reinterpret_cast<float *>(_dvx - tidx);
663
+
664
+ constexpr int VEC_SIZE = sizeof(FLOATV_T)/sizeof(float);
665
+
666
+ // 32-bit, consecutive atomics to glmem
667
+ // strided atomics results in a severe slowdown
668
+ for (int chan = tidx; chan < nchan*VEC_SIZE; chan += BDIM_X) {
669
+
670
+ atomicAdd(_dkx_scl + chan, scale_fact_qy * sh_qy_scl[chan]);
671
+ atomicAdd(_dvx_scl + chan, scale_fact_dy * sh_dy_scl[chan]);
672
+ }
673
+ #else
674
+ #pragma unroll
675
+ for(int i = 0; i < NLOC_M1; i++) {
676
+ atomicAdd(_dkx + i*BDIM_X, __vscale(scale_fact_qy, sh_qy[i*BDIM_X]));
677
+ atomicAdd(_dvx + i*BDIM_X, __vscale(scale_fact_dy, sh_dy[i*BDIM_X]));
678
+ }
679
+ if (NLOC_M1*BDIM_X+tidx < nchan) {
680
+ atomicAdd(_dkx + NLOC_M1*BDIM_X, __vscale(scale_fact_qy, sh_qy[NLOC_M1*BDIM_X]));
681
+ atomicAdd(_dvx + NLOC_M1*BDIM_X, __vscale(scale_fact_dy, sh_dy[NLOC_M1*BDIM_X]));
682
+ }
683
+ #endif
684
+ }
685
+
686
+ return;
687
+ }
688
+
689
+ template<typename FLOATV_T>
690
+ void launch_gen_attn_bwd(int batch_size,
691
+ int nchans,
692
+ int nlat_in,
693
+ int nlon_in,
694
+ int nlat_out,
695
+ int nlon_out,
696
+ FLOATV_T *_kxp,
697
+ FLOATV_T *_vxp,
698
+ FLOATV_T *_qyp,
699
+ FLOATV_T *_dyp,
700
+ int32_t *_row_idx,
701
+ int64_t *_row_off,
702
+ int64_t *_col_idx,
703
+ float *_quad_weights,
704
+ FLOATV_T *_dkxp,
705
+ FLOATV_T *_dvxp,
706
+ FLOATV_T *_dqyp,
707
+ cudaStream_t stream) {
708
+
709
+ dim3 block(WARP_SIZE, THREADS / WARP_SIZE);
710
+ dim3 grid(DIV_UP(nlat_out*nlon_out, block.y), batch_size);
711
+
712
+ size_t shsize = sizeof(FLOATV_T)*nchans*5 * block.y; // 5 arrays per warp
713
+
714
+ s2_attn_bwd_generic_vec_k<THREADS>
715
+ <<<grid, block, shsize, stream>>>(nchans, nlat_in, nlon_in, nlat_out, nlon_out,
716
+ _kxp, _vxp, _qyp, _dyp, _row_idx, _row_off, _col_idx,
717
+ _quad_weights, _dkxp, _dvxp, _dqyp);
718
+ CHECK_ERROR("s2_attn_bwd_generic_vec_k");
719
+
720
+ return;
721
+ }
722
+
723
+ template<int BDIM_X,
724
+ int BDIM_Y,
725
+ int CUR_LOC_SIZE,
726
+ int MAX_LOC_SIZE, // max size of FLOATV_T[] local array
727
+ typename FLOATV_T>
728
+ void launch_spc_attn_bwd(int batch_size,
729
+ int nloc, // "BDIM_X*nloc" >= nchans
730
+ int nchans,
731
+ int nlat_in,
732
+ int nlon_in,
733
+ int nlat_out,
734
+ int nlon_out,
735
+ FLOATV_T *_kxp,
736
+ FLOATV_T *_vxp,
737
+ FLOATV_T *_qyp,
738
+ FLOATV_T *_dyp,
739
+ int32_t *_row_idx,
740
+ int64_t *_row_off,
741
+ int64_t *_col_idx,
742
+ float *_quad_weights,
743
+ FLOATV_T *_dkxp,
744
+ FLOATV_T *_dvxp,
745
+ FLOATV_T *_dqyp,
746
+ cudaStream_t stream) {
747
+
748
+ if (CUR_LOC_SIZE == nloc) {
749
+
750
+ dim3 block(BDIM_X, BDIM_Y);
751
+ dim3 grid(DIV_UP(nlat_out*nlon_out, block.y), batch_size);
752
+
753
+ size_t shsize = sizeof(FLOATV_T)*nchans*2 * block.y; // 2 arrays per cta, block.y > 1 iif block.x==32
754
+
755
+ s2_attn_bwd_special_vec_k<BDIM_X, BDIM_Y, CUR_LOC_SIZE>
756
+ <<<grid, block, shsize, stream>>>(nchans, nlat_in, nlon_in, nlat_out, nlon_out,
757
+ _kxp, _vxp, _qyp, _dyp, _row_idx, _row_off, _col_idx,
758
+ _quad_weights, _dkxp, _dvxp, _dqyp);
759
+ CHECK_ERROR("s2_attn_bwd_special_vec_k");
760
+
761
+ return;
762
+ }
763
+ if constexpr(CUR_LOC_SIZE < MAX_LOC_SIZE) {
764
+ launch_spc_attn_bwd<BDIM_X,
765
+ BDIM_Y,
766
+ CUR_LOC_SIZE+1,
767
+ MAX_LOC_SIZE>(batch_size, nloc, nchans, nlat_in, nlon_in, nlat_out, nlon_out,
768
+ _kxp, _vxp, _qyp, _dyp, _row_idx, _row_off, _col_idx, _quad_weights,
769
+ _dkxp, _dvxp, _dqyp, stream);
770
+ }
771
+ return;
772
+ }
773
+
774
+ static void s2_attn_bwd_dispatch(int batch_size,
775
+ int nchans,
776
+ int nlon_in,
777
+ int nlat_out,
778
+ int nlon_out,
779
+ at::Tensor kxP,
780
+ at::Tensor vxP,
781
+ at::Tensor qyP,
782
+ at::Tensor dyP,
783
+ at::Tensor row_off,
784
+ at::Tensor col_idx,
785
+ at::Tensor quad_weights,
786
+ at::Tensor dkxP,
787
+ at::Tensor dvxP,
788
+ at::Tensor dqyP) {
789
+
790
+ static_assert(0 == (MAX_LOCAL_ARR_LEN & (MAX_LOCAL_ARR_LEN-1)));
791
+
792
+ // get stream
793
+ auto stream = at::cuda::getCurrentCUDAStream().stream();
794
+
795
+ // sort row indices (ho-s) in descending order
796
+ // based on (row_off[ho+1]-row_off[ho])
797
+ at::Tensor row_idx = sortRows(nlat_out, row_off, stream);
798
+
799
+ const int nlat_in = kxP.size(1);
800
+
801
+ // smallest power of two "bdimx" (>=32) s.t. bdimx*MAX_LOCAL_ARR_LEN >= nchans
802
+ int bdimx;
803
+ bdimx = DIV_UP(nchans, MAX_LOCAL_ARR_LEN);
804
+ bdimx = max(bdimx, WARP_SIZE);
805
+ bdimx = next_pow2(bdimx);
806
+
807
+ float *_kxp = reinterpret_cast<float *>(kxP.data_ptr());
808
+ float *_vxp = reinterpret_cast<float *>(vxP.data_ptr());
809
+ float *_qyp = reinterpret_cast<float *>(qyP.data_ptr());
810
+ float *_dyp = reinterpret_cast<float *>(dyP.data_ptr());
811
+
812
+ float *_dkxp = reinterpret_cast<float *>(dkxP.data_ptr());
813
+ float *_dvxp = reinterpret_cast<float *>(dvxP.data_ptr());
814
+ float *_dqyp = reinterpret_cast<float *>(dqyP.data_ptr());
815
+
816
+ int32_t *_row_idx = reinterpret_cast<int32_t *>(row_idx.data_ptr());
817
+ int64_t *_row_off = reinterpret_cast<int64_t *>(row_off.data_ptr());
818
+ int64_t *_col_idx = reinterpret_cast<int64_t *>(col_idx.data_ptr());
819
+ float *_quad_weights = reinterpret_cast<float *>(quad_weights.data_ptr());
820
+
821
+ constexpr int VEC_SIZE = sizeof(float4) / sizeof(float);
822
+
823
+ if (!is_aligned<sizeof(float4)>(_kxp) ||
824
+ !is_aligned<sizeof(float4)>(_vxp) ||
825
+ !is_aligned<sizeof(float4)>(_qyp) ||
826
+ !is_aligned<sizeof(float4)>(_dyp) ||
827
+ !is_aligned<sizeof(float4)>(_dkxp) ||
828
+ !is_aligned<sizeof(float4)>(_dvxp) ||
829
+ !is_aligned<sizeof(float4)>(_dqyp) ||
830
+ (nchans % VEC_SIZE) != 0) {
831
+
832
+ const int nloc = DIV_UP(nchans, bdimx);
833
+
834
+ // to avoid the compilation of unused template instances;
835
+ // we use a block size BDIM_X that is the smallest power of 2
836
+ // such that BDIM_X*MAX_LOCAL_ARR_LEN >= nchans, so
837
+ // BDIM_X > 32 are used only for:
838
+ //
839
+ // (BDIM_X-1)*MAX_LOCAL_ARR_LEN < nchans <= BDIM_X*MAX_LOCAL_ARR_LEN
840
+ constexpr int MIN_LOC_ARR_LEN = MAX_LOCAL_ARR_LEN/2+1;
841
+
842
+ // use 2D blocks only if 32 threads are enough; w.r.t fowrard,
843
+ // we use the special kernel only up to BDIM_X=512 as with 1024
844
+ // each thread cannot use more than 64 registers, resulting in
845
+ // large amounts of registers spills
846
+ switch(bdimx) {
847
+ case 32: launch_spc_attn_bwd< 32, 2, 1, MAX_LOCAL_ARR_LEN>(batch_size, nloc, nchans, nlat_in, nlon_in, nlat_out, nlon_out, _kxp, _vxp, _qyp, _dyp, _row_idx, _row_off, _col_idx, _quad_weights, _dkxp, _dvxp, _dqyp, stream); break;
848
+ case 64: launch_spc_attn_bwd< 64, 1, MIN_LOC_ARR_LEN, MAX_LOCAL_ARR_LEN>(batch_size, nloc, nchans, nlat_in, nlon_in, nlat_out, nlon_out, _kxp, _vxp, _qyp, _dyp, _row_idx, _row_off, _col_idx, _quad_weights, _dkxp, _dvxp, _dqyp, stream); break;
849
+ case 128: launch_spc_attn_bwd<128, 1, MIN_LOC_ARR_LEN, MAX_LOCAL_ARR_LEN>(batch_size, nloc, nchans, nlat_in, nlon_in, nlat_out, nlon_out, _kxp, _vxp, _qyp, _dyp, _row_idx, _row_off, _col_idx, _quad_weights, _dkxp, _dvxp, _dqyp, stream); break;
850
+ case 256: launch_spc_attn_bwd<256, 1, MIN_LOC_ARR_LEN, MAX_LOCAL_ARR_LEN>(batch_size, nloc, nchans, nlat_in, nlon_in, nlat_out, nlon_out, _kxp, _vxp, _qyp, _dyp, _row_idx, _row_off, _col_idx, _quad_weights, _dkxp, _dvxp, _dqyp, stream); break;
851
+ case 512: launch_spc_attn_bwd<512, 1, MIN_LOC_ARR_LEN, MAX_LOCAL_ARR_LEN>(batch_size, nloc, nchans, nlat_in, nlon_in, nlat_out, nlon_out, _kxp, _vxp, _qyp, _dyp, _row_idx, _row_off, _col_idx, _quad_weights, _dkxp, _dvxp, _dqyp, stream); break;
852
+ default: launch_gen_attn_bwd (batch_size, nchans, nlat_in, nlon_in, nlat_out, nlon_out, _kxp, _vxp, _qyp, _dyp, _row_idx, _row_off, _col_idx, _quad_weights, _dkxp, _dvxp, _dqyp, stream); break;
853
+ }
854
+
855
+ } else {
856
+
857
+ float4 *_kxp4 = reinterpret_cast<float4 *>(kxP.data_ptr());
858
+ float4 *_vxp4 = reinterpret_cast<float4 *>(vxP.data_ptr());
859
+ float4 *_qyp4 = reinterpret_cast<float4 *>(qyP.data_ptr());
860
+ float4 *_dyp4 = reinterpret_cast<float4 *>(dyP.data_ptr());
861
+
862
+ float4 *_dkxp4 = reinterpret_cast<float4 *>(dkxP.data_ptr());
863
+ float4 *_dvxp4 = reinterpret_cast<float4 *>(dvxP.data_ptr());
864
+ float4 *_dqyp4 = reinterpret_cast<float4 *>(dqyP.data_ptr());
865
+
866
+ nchans /= VEC_SIZE;
867
+ const int nloc = DIV_UP(nchans, bdimx);
868
+
869
+ constexpr int MAX_LOCAL_VEC_LEN = MAX_LOCAL_ARR_LEN / VEC_SIZE;
870
+
871
+ constexpr int MIN_LOC_VEC_LEN = MAX_LOCAL_VEC_LEN/2+1;
872
+
873
+ // use 2D blocks only if 32 threads are enough
874
+ switch(bdimx) {
875
+ case 32: launch_spc_attn_bwd< 32, 2, 1, MAX_LOCAL_VEC_LEN>(batch_size, nloc, nchans, nlat_in, nlon_in, nlat_out, nlon_out, _kxp4, _vxp4, _qyp4, _dyp4, _row_idx, _row_off, _col_idx, _quad_weights, _dkxp4, _dvxp4, _dqyp4, stream); break;
876
+ case 64: launch_spc_attn_bwd< 64, 1, MIN_LOC_VEC_LEN, MAX_LOCAL_VEC_LEN>(batch_size, nloc, nchans, nlat_in, nlon_in, nlat_out, nlon_out, _kxp4, _vxp4, _qyp4, _dyp4, _row_idx, _row_off, _col_idx, _quad_weights, _dkxp4, _dvxp4, _dqyp4, stream); break;
877
+ case 128: launch_spc_attn_bwd<128, 1, MIN_LOC_VEC_LEN, MAX_LOCAL_VEC_LEN>(batch_size, nloc, nchans, nlat_in, nlon_in, nlat_out, nlon_out, _kxp4, _vxp4, _qyp4, _dyp4, _row_idx, _row_off, _col_idx, _quad_weights, _dkxp4, _dvxp4, _dqyp4, stream); break;
878
+ case 256: launch_spc_attn_bwd<256, 1, MIN_LOC_VEC_LEN, MAX_LOCAL_VEC_LEN>(batch_size, nloc, nchans, nlat_in, nlon_in, nlat_out, nlon_out, _kxp4, _vxp4, _qyp4, _dyp4, _row_idx, _row_off, _col_idx, _quad_weights, _dkxp4, _dvxp4, _dqyp4, stream); break;
879
+ case 512: launch_spc_attn_bwd<512, 1, MIN_LOC_VEC_LEN, MAX_LOCAL_VEC_LEN>(batch_size, nloc, nchans, nlat_in, nlon_in, nlat_out, nlon_out, _kxp4, _vxp4, _qyp4, _dyp4, _row_idx, _row_off, _col_idx, _quad_weights, _dkxp4, _dvxp4, _dqyp4, stream); break;
880
+ default: launch_gen_attn_bwd (batch_size, nchans, nlat_in, nlon_in, nlat_out, nlon_out, _kxp4, _vxp4, _qyp4, _dyp4, _row_idx, _row_off, _col_idx, _quad_weights, _dkxp4, _dvxp4, _dqyp4, stream); break;
881
+ }
882
+ }
883
+
884
+ return;
885
+ }
886
+
887
+ // END backward kernels and functions
888
+
889
+ std::tuple<at::Tensor, at::Tensor, at::Tensor> s2_attention_bwd_dkvq_cuda(at::Tensor kx, at::Tensor vx, at::Tensor qy,
890
+ at::Tensor dy, at::Tensor quad_weights,
891
+ at::Tensor psi_col_idx, at::Tensor psi_row_off,
892
+ int nlon_in, int nlat_out, int nlon_out)
893
+ {
894
+
895
+ CHECK_CUDA_INPUT_TENSOR(kx);
896
+ CHECK_CUDA_INPUT_TENSOR(vx);
897
+ CHECK_CUDA_INPUT_TENSOR(qy);
898
+ CHECK_CUDA_INPUT_TENSOR(dy);
899
+ CHECK_CUDA_TENSOR(quad_weights);
900
+ CHECK_CUDA_TENSOR(psi_col_idx);
901
+ CHECK_CUDA_TENSOR(psi_row_off);
902
+
903
+ const size_t uo_num_channels = kx.size(1);
904
+ const int batch_size = kx.size(0);
905
+
906
+ // extract dtype
907
+ auto kx_type = kx.dtype();
908
+ auto vx_type = vx.dtype();
909
+ auto qy_type = qy.dtype();
910
+ auto dy_type = dy.dtype();
911
+
912
+ torch::Tensor kxP = kx.to(torch::kFloat32);
913
+ torch::Tensor vxP = vx.to(torch::kFloat32);
914
+ torch::Tensor qyP = qy.to(torch::kFloat32);
915
+ torch::Tensor dyP = dy.to(torch::kFloat32);
916
+
917
+ // exract memory format: this is much safer than checking is_contiguous(at::MemoryFormat::ChannelsLast)
918
+ // the former fails for num_channels == 1
919
+ bool kx_is_channels_last = kxP.strides()[1] == 1;
920
+ bool vx_is_channels_last = vxP.strides()[1] == 1;
921
+ bool qy_is_channels_last = qyP.strides()[1] == 1;
922
+ bool dy_is_channels_last = dyP.strides()[1] == 1;
923
+
924
+ // transpose if required
925
+ if (!kx_is_channels_last) { kxP = permute_4D_to0231(kxP); }
926
+ if (!vx_is_channels_last) { vxP = permute_4D_to0231(vxP); }
927
+ if (!qy_is_channels_last) { qyP = permute_4D_to0231(qyP); }
928
+ if (!dy_is_channels_last) { dyP = permute_4D_to0231(dyP); }
929
+
930
+ torch::Tensor dkxP = torch::zeros_like(kxP);
931
+ torch::Tensor dvxP = torch::zeros_like(vxP);
932
+ torch::Tensor dqyP = torch::zeros_like(qyP);
933
+
934
+ s2_attn_bwd_dispatch(batch_size,
935
+ uo_num_channels,
936
+ nlon_in,
937
+ nlat_out,
938
+ nlon_out,
939
+ kxP, vxP, qyP, dyP,
940
+ psi_row_off,
941
+ psi_col_idx,
942
+ quad_weights,
943
+ dkxP, dvxP, dqyP);
944
+
945
+ torch::Tensor dkx = dkxP;
946
+ torch::Tensor dvx = dvxP;
947
+ torch::Tensor dqy = dqyP;
948
+
949
+ if (!kx_is_channels_last) { dkx = permute_4D_to0312(dkx); }
950
+ if (!vx_is_channels_last) { dvx = permute_4D_to0312(dvx); }
951
+ if (!qy_is_channels_last) { dqy = permute_4D_to0312(dqy); }
952
+
953
+ // convert precision back to starting
954
+ dkx = dkx.to(kx_type);
955
+ dvx = dvx.to(vx_type);
956
+ dqy = dqy.to(qy_type);
957
+
958
+ return std::make_tuple(dkx, dvx, dqy);
959
+ // #endif
960
+ }
.deps/torch_harmonics/csrc/attention/attention_fwd_cuda.cu ADDED
@@ -0,0 +1,525 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // coding=utf-8
2
+ //
3
+ // SPDX-FileCopyrightText: Copyright (c) 2025 The torch-harmonics Authors. All rights reserved.
4
+ // SPDX-License-Identifier: BSD-3-Clause
5
+ //
6
+ // Redistribution and use in source and binary forms, with or without
7
+ // modification, are permitted provided that the following conditions are met:
8
+ //
9
+ // 1. Redistributions of source code must retain the above copyright notice, this
10
+ // list of conditions and the following disclaimer.
11
+ //
12
+ // 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ // this list of conditions and the following disclaimer in the documentation
14
+ // and/or other materials provided with the distribution.
15
+ //
16
+ // 3. Neither the name of the copyright holder nor the names of its
17
+ // contributors may be used to endorse or promote products derived from
18
+ // this software without specific prior written permission.
19
+ //
20
+ // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+
31
+ #include "attention.cuh"
32
+ #include <ATen/cuda/detail/TensorInfo.cuh>
33
+ #include <ATen/cuda/detail/KernelUtils.h>
34
+ #include <ATen/cuda/detail/IndexUtils.cuh>
35
+ #include <ATen/cuda/CUDAUtils.h>
36
+
37
+ #include <cuda_runtime.h>
38
+
39
+ #include <cub/cub.cuh>
40
+ #include <limits>
41
+
42
+ #include "cudamacro.h"
43
+ #include "attention_utils.cuh"
44
+
45
+ #define THREADS (64)
46
+
47
+ #define MAX_LOCAL_ARR_LEN (16)
48
+
49
+ // BEGIN - forward kernels and functions
50
+
51
+ // called with (blockDim.x=32 and blockDim.y>1, BDIM_X=blockDim.x*blockDim.y)
52
+ template<int BDIM_X,
53
+ typename FLOATV_T> // either float or float4
54
+ __global__
55
+ __launch_bounds__(BDIM_X)
56
+ void s2_attn_fwd_generic_vec_k(int nchan, // no. of FLOATV_T elements along channel dim
57
+ int nlat_in,
58
+ int nlon_in,
59
+ int nlat_out,
60
+ int nlon_out,
61
+ const FLOATV_T *__restrict__ kx,
62
+ const FLOATV_T *__restrict__ vx,
63
+ const FLOATV_T *__restrict__ qy,
64
+ const int32_t *__restrict__ row_idx,
65
+ const int64_t *__restrict__ row_off,
66
+ const int64_t *__restrict__ col_idx,
67
+ const float *__restrict__ quad_weights,
68
+ FLOATV_T *__restrict__ y) {
69
+
70
+ extern __shared__ __align__(sizeof(float4)) float shext[];
71
+ FLOATV_T *shy = reinterpret_cast<FLOATV_T *>(shext) + threadIdx.y*nchan;
72
+
73
+ const int batch = blockIdx.y;
74
+ const int wid = blockIdx.x*blockDim.y + threadIdx.y;
75
+
76
+ if (wid >= nlat_out*nlon_out) {
77
+ return;
78
+ }
79
+
80
+ const int tidx = threadIdx.x;
81
+
82
+ const int h = wid / nlon_out;
83
+ const int wo = wid - (h*nlon_out);
84
+ const int ho = row_idx[h];
85
+
86
+ for(int chan = tidx; chan < nchan; chan += WARP_SIZE) {
87
+ shy[chan] = __vset<FLOATV_T>(0.f);
88
+ }
89
+
90
+ kx += int64_t(batch)*nlat_in*nlon_in*nchan;
91
+ vx += int64_t(batch)*nlat_in*nlon_in*nchan;
92
+ qy += int64_t(batch)*nlat_out*nlon_out*nchan + int64_t(ho)*nchan*nlon_out + int64_t(wo)*nchan;
93
+ y += int64_t(batch)*nlat_out*nlon_out*nchan + int64_t(ho)*nchan*nlon_out + int64_t(wo)*nchan;
94
+
95
+ float alpha_sum = 0.0f;
96
+ float qdotk_max = -FLT_MAX;
97
+
98
+ const int64_t rbeg = row_off[ho];
99
+ const int64_t rend = row_off[ho+1];
100
+
101
+ col_idx += rbeg;
102
+
103
+ const int rlen = rend-rbeg;
104
+
105
+ for(int off = 0; off < rlen; off++) {
106
+
107
+ const int64_t col = col_idx[off];
108
+
109
+ const int hi = col / nlon_in;
110
+ const int wi = col - (hi*nlon_in);
111
+ const int wip = (wi+wo) - ((wi+wo) / nlon_in) * nlon_in;
112
+
113
+ const FLOATV_T *_kx = kx + int64_t(hi)*nlon_in*nchan + int64_t(wip)*nchan;
114
+ const FLOATV_T *_vx = vx + int64_t(hi)*nlon_in*nchan + int64_t(wip)*nchan;
115
+
116
+ FLOATV_T qdotkv = __vset<FLOATV_T>(0.f);
117
+
118
+ for(int chan = tidx; chan < nchan; chan += WARP_SIZE) {
119
+ qdotkv = __vadd(qdotkv,
120
+ __vmul( qy[chan],
121
+ _kx[chan]));
122
+ }
123
+
124
+ float qdotk = __warp_sum(__vred(qdotkv));
125
+
126
+ float qdotk_max_tmp;
127
+ float alpha;
128
+ float exp_save;
129
+
130
+ qdotk_max_tmp = max(qdotk_max, qdotk);
131
+ alpha = expf(qdotk - qdotk_max_tmp) * quad_weights[hi];
132
+ exp_save = expf(qdotk_max - qdotk_max_tmp);
133
+
134
+ alpha_sum = alpha + alpha_sum*exp_save;
135
+
136
+ for(int chan = tidx; chan < nchan; chan += WARP_SIZE) {
137
+ shy[chan] = __vadd(__vscale(exp_save, shy[chan]),
138
+ __vscale( alpha, _vx[chan]));
139
+ }
140
+ qdotk_max = qdotk_max_tmp;
141
+ }
142
+
143
+ alpha_sum = 1.0f / alpha_sum;
144
+ for(int chan = tidx; chan < nchan; chan += WARP_SIZE) {
145
+ y[chan] = __vscale(alpha_sum, shy[chan]);
146
+ }
147
+
148
+ return;
149
+ }
150
+
151
+ // called with either (BDIM_X=32 and BDIM_Y>1) || (2^K=BDIM_X > 32 and BDIM_Y=1)
152
+ template<int BDIM_X,
153
+ int BDIM_Y,
154
+ int NLOC,
155
+ typename FLOATV_T> // either float or float4
156
+ __global__
157
+ __launch_bounds__(BDIM_X*BDIM_Y)
158
+ void s2_attn_fwd_special_vec_k(int nchan, // no. of FLOATV_T elements along channel dim
159
+ int nlat_in,
160
+ int nlon_in,
161
+ int nlat_out,
162
+ int nlon_out,
163
+ const FLOATV_T *__restrict__ kx,
164
+ const FLOATV_T *__restrict__ vx,
165
+ const FLOATV_T *__restrict__ qy,
166
+ const int32_t *__restrict__ row_idx,
167
+ const int64_t *__restrict__ row_off,
168
+ const int64_t *__restrict__ col_idx,
169
+ const float *__restrict__ quad_weights,
170
+ FLOATV_T *__restrict__ y) {
171
+
172
+ static_assert(0 == (BDIM_X & (BDIM_X-1)));
173
+ static_assert(0 == (BDIM_Y & (BDIM_Y-1)));
174
+ static_assert((BDIM_X == 32 && BDIM_Y > 1) ||
175
+ (BDIM_X > 32 && BDIM_Y == 1)) ;
176
+
177
+ constexpr int NLOC_M1 = NLOC-1;
178
+
179
+ const int tidx = threadIdx.x;
180
+ const int batch = blockIdx.y;
181
+ const int ctaid = blockIdx.x*blockDim.y + threadIdx.y;
182
+
183
+ if (ctaid >= nlat_out*nlon_out) {
184
+ return;
185
+ }
186
+
187
+ FLOATV_T locy[NLOC];
188
+
189
+ extern __shared__ __align__(sizeof(float4)) float shext[];
190
+ FLOATV_T *shq = reinterpret_cast<FLOATV_T *>(shext) + threadIdx.y*nchan + tidx;
191
+
192
+ const int h = ctaid / nlon_out;
193
+ const int wo = ctaid - (h*nlon_out);
194
+ const int ho = row_idx[h];
195
+
196
+ kx += int64_t(batch)*nlat_in*nlon_in*nchan + tidx;
197
+ vx += int64_t(batch)*nlat_in*nlon_in*nchan + tidx;
198
+ qy += int64_t(batch)*nlat_out*nlon_out*nchan + int64_t(ho)*nlon_out*nchan + int64_t(wo)*nchan + tidx;
199
+ y += int64_t(batch)*nlat_out*nlon_out*nchan + int64_t(ho)*nlon_out*nchan + int64_t(wo)*nchan + tidx;
200
+
201
+ #pragma unroll
202
+ for(int i = 0; i < NLOC; i++) {
203
+ locy[i] = __vset<FLOATV_T>(0.f);
204
+ }
205
+
206
+ #pragma unroll
207
+ for(int i = 0; i < NLOC_M1; i++) {
208
+ shq[i*BDIM_X] = qy[i*BDIM_X];
209
+ }
210
+ if (NLOC_M1*BDIM_X+tidx < nchan) {
211
+ shq[NLOC_M1*BDIM_X] = qy[NLOC_M1*BDIM_X];
212
+ }
213
+
214
+ float alpha_sum = 0.0f;
215
+ float qdotk_max = -FLT_MAX;
216
+
217
+ const int64_t rbeg = row_off[ho];
218
+ const int64_t rend = row_off[ho+1];
219
+
220
+ col_idx += rbeg;
221
+
222
+ const int rlen = rend-rbeg;
223
+
224
+ for(int off = 0; off < rlen; off++) {
225
+
226
+ const int64_t col = col_idx[off];
227
+
228
+ const int hi = col / nlon_in;
229
+ const int wi = col - (hi*nlon_in);
230
+ const int wip = (wi+wo) - ((wi+wo) / nlon_in) * nlon_in;
231
+
232
+ const FLOATV_T *_kx = kx + int64_t(hi)*nlon_in*nchan + int64_t(wip)*nchan;
233
+ const FLOATV_T *_vx = vx + int64_t(hi)*nlon_in*nchan + int64_t(wip)*nchan;
234
+
235
+ FLOATV_T qdotkv = __vset<FLOATV_T>(0.f);
236
+
237
+ #pragma unroll
238
+ for(int i = 0; i < NLOC_M1; i++) {
239
+ qdotkv = __vadd(qdotkv,
240
+ __vmul(shq[i*BDIM_X],
241
+ _kx[i*BDIM_X]));
242
+ }
243
+ if (NLOC_M1*BDIM_X+tidx < nchan) {
244
+ qdotkv = __vadd(qdotkv,
245
+ __vmul(shq[NLOC_M1*BDIM_X],
246
+ _kx[NLOC_M1*BDIM_X]));
247
+ }
248
+
249
+ float qdotk = __vred(qdotkv);
250
+ if constexpr(BDIM_X == 32) { qdotk = __warp_sum(qdotk); }
251
+ else { qdotk = __block_sum<BDIM_X>(qdotk); }
252
+
253
+ float qdotk_max_tmp;
254
+ float alpha;
255
+ float exp_save;
256
+
257
+ qdotk_max_tmp = max(qdotk_max, qdotk);
258
+ alpha = expf(qdotk - qdotk_max_tmp) * quad_weights[hi];
259
+ exp_save = expf(qdotk_max - qdotk_max_tmp);
260
+
261
+ alpha_sum = alpha + alpha_sum*exp_save;
262
+
263
+ #pragma unroll
264
+ for(int i = 0; i < NLOC_M1; i++) {
265
+ locy[i] = __vadd(__vscale(exp_save, locy[i]),
266
+ __vscale(alpha, _vx[i*BDIM_X]));
267
+ }
268
+ if (NLOC_M1*BDIM_X+tidx < nchan) {
269
+ locy[NLOC_M1] = __vadd(__vscale(exp_save, locy[NLOC_M1]),
270
+ __vscale(alpha, _vx[NLOC_M1*BDIM_X]));
271
+ }
272
+
273
+ qdotk_max = qdotk_max_tmp;
274
+ }
275
+
276
+ alpha_sum = 1.0f / alpha_sum;
277
+
278
+ #pragma unroll
279
+ for(int i = 0; i < NLOC_M1; i++) {
280
+ y[i*BDIM_X] = __vscale(alpha_sum, locy[i]);
281
+ }
282
+ if (NLOC_M1*BDIM_X+tidx < nchan) {
283
+ y[NLOC_M1*BDIM_X] = __vscale(alpha_sum, locy[NLOC_M1]);
284
+ }
285
+
286
+ return;
287
+ }
288
+
289
+ template<typename FLOATV_T>
290
+ void launch_gen_attn_fwd(int batch_size,
291
+ int nchans,
292
+ int nlat_in,
293
+ int nlon_in,
294
+ int nlat_out,
295
+ int nlon_out,
296
+ FLOATV_T *__restrict__ _kxp,
297
+ FLOATV_T *__restrict__ _vxp,
298
+ FLOATV_T *__restrict__ _qyp,
299
+ int32_t *_row_idx,
300
+ int64_t *_row_off,
301
+ int64_t *_col_idx,
302
+ float *_quad_weights,
303
+ FLOATV_T *__restrict__ _yp,
304
+ cudaStream_t stream) {
305
+
306
+ dim3 block(WARP_SIZE, THREADS/WARP_SIZE);
307
+ dim3 grid(DIV_UP(nlat_out*nlon_out, block.y), batch_size);
308
+
309
+ size_t shsize = sizeof(FLOATV_T)*nchans * block.y;
310
+
311
+ s2_attn_fwd_generic_vec_k<THREADS>
312
+ <<<grid, block, shsize, stream>>>(nchans, nlat_in, nlon_in, nlat_out, nlon_out,
313
+ _kxp, _vxp, _qyp, _row_idx, _row_off, _col_idx, _quad_weights, _yp);
314
+ CHECK_ERROR("s2_attn_fwd_generic_vec_k");
315
+
316
+ return;
317
+ }
318
+
319
+ template<int BDIM_X,
320
+ int BDIM_Y,
321
+ int CUR_LOC_SIZE,
322
+ int MAX_LOC_SIZE, // max size of FLOATV_T[] local array
323
+ typename FLOATV_T>
324
+ void launch_spc_attn_fwd(int batch_size,
325
+ int nloc, // "BDIM_X*nloc" >= nchans
326
+ int nchans,
327
+ int nlat_in,
328
+ int nlon_in,
329
+ int nlat_out,
330
+ int nlon_out,
331
+ FLOATV_T *__restrict__ _kxp,
332
+ FLOATV_T *__restrict__ _vxp,
333
+ FLOATV_T *__restrict__ _qyp,
334
+ int32_t *_row_idx,
335
+ int64_t *_row_off,
336
+ int64_t *_col_idx,
337
+ float *_quad_weights,
338
+ FLOATV_T *__restrict__ _yp,
339
+ cudaStream_t stream) {
340
+
341
+ if (CUR_LOC_SIZE == nloc) {
342
+
343
+ dim3 block(BDIM_X, BDIM_Y);
344
+ dim3 grid(DIV_UP(nlat_out*nlon_out, block.y), batch_size);
345
+
346
+ size_t shsize = sizeof(FLOATV_T)*nchans * block.y; // block.y > 1 iif block.x==32
347
+
348
+ s2_attn_fwd_special_vec_k<BDIM_X, BDIM_Y, CUR_LOC_SIZE>
349
+ <<<grid, block, shsize, stream>>>(nchans, nlat_in, nlon_in, nlat_out, nlon_out,
350
+ _kxp, _vxp, _qyp, _row_idx, _row_off, _col_idx, _quad_weights, _yp);
351
+ CHECK_ERROR("s2_attn_fwd_special_vec_k");
352
+
353
+ return;
354
+ }
355
+ if constexpr(CUR_LOC_SIZE < MAX_LOC_SIZE) {
356
+ launch_spc_attn_fwd<BDIM_X,
357
+ BDIM_Y,
358
+ CUR_LOC_SIZE+1,
359
+ MAX_LOC_SIZE>(batch_size, nloc, nchans, nlat_in, nlon_in, nlat_out, nlon_out,
360
+ _kxp, _vxp, _qyp, _row_idx, _row_off, _col_idx, _quad_weights, _yp,
361
+ stream);
362
+ }
363
+ return;
364
+ }
365
+
366
+ static void s2_attn_fwd_dispatch(int batch_size,
367
+ int nchans,
368
+ int nlon_in,
369
+ int nlat_out,
370
+ int nlon_out,
371
+ at::Tensor kxP,
372
+ at::Tensor vxP,
373
+ at::Tensor qyP,
374
+ at::Tensor row_off,
375
+ at::Tensor col_idx,
376
+ at::Tensor quad_weights,
377
+ at::Tensor yP) {
378
+
379
+ static_assert(0 == (MAX_LOCAL_ARR_LEN & (MAX_LOCAL_ARR_LEN-1)));
380
+
381
+ // get stream
382
+ auto stream = at::cuda::getCurrentCUDAStream().stream();
383
+
384
+ // sort row indices (ho-s) in descending order
385
+ // based on (row_off[ho+1]-row_off[ho])
386
+ at::Tensor row_idx = sortRows(nlat_out, row_off, stream);
387
+
388
+ const int nlat_in = kxP.size(1);
389
+
390
+ // smallest power of two "bdimx" (>=32) s.t. bdimx*MAX_LOCAL_ARR_LEN >= nchans
391
+ int bdimx;
392
+ bdimx = DIV_UP(nchans, MAX_LOCAL_ARR_LEN);
393
+ bdimx = max(bdimx, WARP_SIZE);
394
+ bdimx = next_pow2(bdimx);
395
+
396
+ float *_kxp = reinterpret_cast<float *>(kxP.data_ptr());
397
+ float *_vxp = reinterpret_cast<float *>(vxP.data_ptr());
398
+ float *_qyp = reinterpret_cast<float *>(qyP.data_ptr());
399
+ float *_yp = reinterpret_cast<float *>(yP.data_ptr());
400
+
401
+ int32_t *_row_idx = reinterpret_cast<int32_t *>(row_idx.data_ptr());
402
+ int64_t *_row_off = reinterpret_cast<int64_t *>(row_off.data_ptr());
403
+ int64_t *_col_idx = reinterpret_cast<int64_t *>(col_idx.data_ptr());
404
+ float *_quad_weights = reinterpret_cast<float *>(quad_weights.data_ptr());
405
+
406
+ constexpr int VEC_SIZE = sizeof(float4) / sizeof(float);
407
+
408
+ if (!is_aligned<sizeof(float4)>(_kxp) ||
409
+ !is_aligned<sizeof(float4)>(_vxp) ||
410
+ !is_aligned<sizeof(float4)>(_qyp) ||
411
+ !is_aligned<sizeof(float4)>(_yp) ||
412
+ (nchans % VEC_SIZE) != 0) {
413
+
414
+ const int nloc = DIV_UP(nchans, bdimx);
415
+
416
+ // to avoid the compilation of unused template instances;
417
+ // we use a block size BDIM_X that is the smallest power of 2
418
+ // such that BDIM_X*MAX_LOCAL_ARR_LEN >= nchans, so
419
+ // BDIM_X > 32 are used only for:
420
+ //
421
+ // (BDIM_X-1)*MAX_LOCAL_ARR_LEN < nchans <= BDIM_X*MAX_LOCAL_ARR_LEN
422
+ constexpr int MIN_LOC_ARR_LEN = MAX_LOCAL_ARR_LEN/2+1;
423
+
424
+ // use 2D blocks only if 32 threads are enough
425
+ switch(bdimx) {
426
+ case 32: launch_spc_attn_fwd< 32, 2, 1, MAX_LOCAL_ARR_LEN>(batch_size, nloc, nchans, nlat_in, nlon_in, nlat_out, nlon_out, _kxp, _vxp, _qyp, _row_idx, _row_off, _col_idx, _quad_weights, _yp, stream); break;
427
+ case 64: launch_spc_attn_fwd< 64, 1, MIN_LOC_ARR_LEN, MAX_LOCAL_ARR_LEN>(batch_size, nloc, nchans, nlat_in, nlon_in, nlat_out, nlon_out, _kxp, _vxp, _qyp, _row_idx, _row_off, _col_idx, _quad_weights, _yp, stream); break;
428
+ case 128: launch_spc_attn_fwd< 128, 1, MIN_LOC_ARR_LEN, MAX_LOCAL_ARR_LEN>(batch_size, nloc, nchans, nlat_in, nlon_in, nlat_out, nlon_out, _kxp, _vxp, _qyp, _row_idx, _row_off, _col_idx, _quad_weights, _yp, stream); break;
429
+ case 256: launch_spc_attn_fwd< 256, 1, MIN_LOC_ARR_LEN, MAX_LOCAL_ARR_LEN>(batch_size, nloc, nchans, nlat_in, nlon_in, nlat_out, nlon_out, _kxp, _vxp, _qyp, _row_idx, _row_off, _col_idx, _quad_weights, _yp, stream); break;
430
+ case 512: launch_spc_attn_fwd< 512, 1, MIN_LOC_ARR_LEN, MAX_LOCAL_ARR_LEN>(batch_size, nloc, nchans, nlat_in, nlon_in, nlat_out, nlon_out, _kxp, _vxp, _qyp, _row_idx, _row_off, _col_idx, _quad_weights, _yp, stream); break;
431
+ case 1024: launch_spc_attn_fwd<1024, 1, MIN_LOC_ARR_LEN, MAX_LOCAL_ARR_LEN>(batch_size, nloc, nchans, nlat_in, nlon_in, nlat_out, nlon_out, _kxp, _vxp, _qyp, _row_idx, _row_off, _col_idx, _quad_weights, _yp, stream); break;
432
+ default: launch_gen_attn_fwd (batch_size, nchans, nlat_in, nlon_in, nlat_out, nlon_out, _kxp, _vxp, _qyp, _row_idx, _row_off, _col_idx, _quad_weights, _yp, stream); break;
433
+ }
434
+
435
+ } else {
436
+
437
+ float4 *_kxp4 = reinterpret_cast<float4 *>(_kxp);
438
+ float4 *_vxp4 = reinterpret_cast<float4 *>(_vxp);
439
+ float4 *_qyp4 = reinterpret_cast<float4 *>(_qyp);
440
+ float4 *_yp4 = reinterpret_cast<float4 *>(_yp);
441
+
442
+ nchans /= VEC_SIZE;
443
+ const int nloc = DIV_UP(nchans, bdimx);
444
+
445
+ constexpr int MAX_LOCAL_VEC_LEN = MAX_LOCAL_ARR_LEN / VEC_SIZE;
446
+
447
+ constexpr int MIN_LOC_VEC_LEN = MAX_LOCAL_VEC_LEN/2+1;
448
+
449
+ // use 2D blocks only if 32 threads are enough
450
+ switch(bdimx) {
451
+ case 32: launch_spc_attn_fwd< 32, 2, 1, MAX_LOCAL_VEC_LEN>(batch_size, nloc, nchans, nlat_in, nlon_in, nlat_out, nlon_out, _kxp4, _vxp4, _qyp4, _row_idx, _row_off, _col_idx, _quad_weights, _yp4, stream); break;
452
+ case 64: launch_spc_attn_fwd< 64, 1, MIN_LOC_VEC_LEN, MAX_LOCAL_VEC_LEN>(batch_size, nloc, nchans, nlat_in, nlon_in, nlat_out, nlon_out, _kxp4, _vxp4, _qyp4, _row_idx, _row_off, _col_idx, _quad_weights, _yp4, stream); break;
453
+ case 128: launch_spc_attn_fwd< 128, 1, MIN_LOC_VEC_LEN, MAX_LOCAL_VEC_LEN>(batch_size, nloc, nchans, nlat_in, nlon_in, nlat_out, nlon_out, _kxp4, _vxp4, _qyp4, _row_idx, _row_off, _col_idx, _quad_weights, _yp4, stream); break;
454
+ case 256: launch_spc_attn_fwd< 256, 1, MIN_LOC_VEC_LEN, MAX_LOCAL_VEC_LEN>(batch_size, nloc, nchans, nlat_in, nlon_in, nlat_out, nlon_out, _kxp4, _vxp4, _qyp4, _row_idx, _row_off, _col_idx, _quad_weights, _yp4, stream); break;
455
+ case 512: launch_spc_attn_fwd< 512, 1, MIN_LOC_VEC_LEN, MAX_LOCAL_VEC_LEN>(batch_size, nloc, nchans, nlat_in, nlon_in, nlat_out, nlon_out, _kxp4, _vxp4, _qyp4, _row_idx, _row_off, _col_idx, _quad_weights, _yp4, stream); break;
456
+ case 1024: launch_spc_attn_fwd<1024, 1, MIN_LOC_VEC_LEN, MAX_LOCAL_VEC_LEN>(batch_size, nloc, nchans, nlat_in, nlon_in, nlat_out, nlon_out, _kxp4, _vxp4, _qyp4, _row_idx, _row_off, _col_idx, _quad_weights, _yp4, stream); break;
457
+ default: launch_gen_attn_fwd (batch_size, nchans, nlat_in, nlon_in, nlat_out, nlon_out, _kxp4, _vxp4, _qyp4, _row_idx, _row_off, _col_idx, _quad_weights, _yp4, stream); break;
458
+ }
459
+ }
460
+
461
+ return;
462
+ }
463
+
464
+ // END - forward kernels and functions
465
+
466
+ torch::Tensor s2_attention_fwd_cuda(at::Tensor kx,
467
+ at::Tensor vx,
468
+ at::Tensor qy,
469
+ at::Tensor quad_weights,
470
+ at::Tensor psi_col_idx,
471
+ at::Tensor psi_row_off,
472
+ int nlon_in,
473
+ int nlat_out,
474
+ int nlon_out) {
475
+ CHECK_CUDA_INPUT_TENSOR(kx);
476
+ CHECK_CUDA_INPUT_TENSOR(vx);
477
+ CHECK_CUDA_INPUT_TENSOR(qy);
478
+ CHECK_CUDA_TENSOR(quad_weights);
479
+ CHECK_CUDA_TENSOR(psi_col_idx);
480
+ CHECK_CUDA_TENSOR(psi_row_off);
481
+
482
+ size_t uo_num_channels = kx.size(1);
483
+
484
+ const int batch_size = kx.size(0);
485
+
486
+ // extract dtype
487
+ auto qy_type = qy.dtype();
488
+
489
+ torch::Tensor kxP = kx.to(torch::kFloat32);
490
+ torch::Tensor vxP = vx.to(torch::kFloat32);
491
+ torch::Tensor qyP = qy.to(torch::kFloat32);
492
+
493
+ // these are much safer than checking is_contiguous(at::MemoryFormat::ChannelsLast)
494
+ // the former fails for num_channels == 1
495
+ bool kx_is_channels_last = kxP.strides()[1] == 1;
496
+ bool vx_is_channels_last = vxP.strides()[1] == 1;
497
+ bool qy_is_channels_last = qyP.strides()[1] == 1;
498
+
499
+ if (!kx_is_channels_last) { kxP = permute_4D_to0231(kxP); }
500
+ if (!vx_is_channels_last) { vxP = permute_4D_to0231(vxP); }
501
+ if (!qy_is_channels_last) { qyP = permute_4D_to0231(qyP); }
502
+
503
+ torch::Tensor yP = torch::empty_like(qyP);
504
+
505
+ s2_attn_fwd_dispatch(batch_size,
506
+ uo_num_channels,
507
+ nlon_in,
508
+ nlat_out,
509
+ nlon_out,
510
+ kxP, vxP, qyP,
511
+ psi_row_off,
512
+ psi_col_idx,
513
+ quad_weights,
514
+ yP);
515
+
516
+ torch::Tensor y = yP;
517
+ if (!qy_is_channels_last) { y = permute_4D_to0312(y); }
518
+
519
+ // convert precision back to starting
520
+ y = y.to(qy_type);
521
+
522
+ C10_CUDA_KERNEL_LAUNCH_CHECK();
523
+
524
+ return y;
525
+ }
.deps/torch_harmonics/csrc/attention/attention_interface.cu ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // coding=utf-8
2
+ //
3
+ // SPDX-FileCopyrightText: Copyright (c) 2025 The torch-harmonics Authors. All rights reserved.
4
+ // SPDX-License-Identifier: BSD-3-Clause
5
+ //
6
+ // Redistribution and use in source and binary forms, with or without
7
+ // modification, are permitted provided that the following conditions are met:
8
+ //
9
+ // 1. Redistributions of source code must retain the above copyright notice, this
10
+ // list of conditions and the following disclaimer.
11
+ //
12
+ // 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ // this list of conditions and the following disclaimer in the documentation
14
+ // and/or other materials provided with the distribution.
15
+ //
16
+ // 3. Neither the name of the copyright holder nor the names of its
17
+ // contributors may be used to endorse or promote products derived from
18
+ // this software without specific prior written permission.
19
+ //
20
+ // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+
31
+ #include "attention.cuh"
32
+ #include <torch/extension.h>
33
+
34
+ PYBIND11_MODULE(TORCH_EXTENSION_NAME, m)
35
+ {
36
+ m.def("forward", &s2_attention_fwd_cuda, "(Local) Attention on S2");
37
+ m.def("backward_dkvq", &s2_attention_bwd_dkvq_cuda, "(Local) Attention gradient on S2 (gradient for k,v,&q)");
38
+ }
.deps/torch_harmonics/csrc/attention/attention_utils.cu ADDED
@@ -0,0 +1,180 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // coding=utf-8
2
+ //
3
+ // SPDX-FileCopyrightText: Copyright (c) 2025 The torch-harmonics Authors. All rights reserved.
4
+ // SPDX-License-Identifier: BSD-3-Clause
5
+ //
6
+ // Redistribution and use in source and binary forms, with or without
7
+ // modification, are permitted provided that the following conditions are met:
8
+ //
9
+ // 1. Redistributions of source code must retain the above copyright notice, this
10
+ // list of conditions and the following disclaimer.
11
+ //
12
+ // 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ // this list of conditions and the following disclaimer in the documentation
14
+ // and/or other materials provided with the distribution.
15
+ //
16
+ // 3. Neither the name of the copyright holder nor the names of its
17
+ // contributors may be used to endorse or promote products derived from
18
+ // this software without specific prior written permission.
19
+ //
20
+ // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+
31
+ #include "attention.cuh"
32
+ #include <ATen/cuda/detail/TensorInfo.cuh>
33
+ #include <ATen/cuda/detail/KernelUtils.h>
34
+ #include <ATen/cuda/detail/IndexUtils.cuh>
35
+ #include <ATen/cuda/CUDAUtils.h>
36
+
37
+ #include <cuda_runtime.h>
38
+
39
+ #include <cub/cub.cuh>
40
+ #include <limits>
41
+
42
+ #include "cudamacro.h"
43
+ #include "attention_utils.cuh"
44
+
45
+ #define THREADS (64)
46
+
47
+ #define TRANSP_WARPS_X_TILE_GENERIC (32)
48
+ #define TRANSP_WARPS_X_TILE_SM100 (4)
49
+
50
+ // BEGIN - CSR rows sorting kernels and functions
51
+ __global__ void set_rlen_rids_k(const int n,
52
+ const int64_t *__restrict__ offs,
53
+ int *__restrict__ rids,
54
+ int *__restrict__ rlen) {
55
+
56
+ const int nth = gridDim.x*blockDim.x;
57
+ const int tid = blockIdx.x*blockDim.x + threadIdx.x;
58
+
59
+ for(int i = tid; i < n; i += nth) {
60
+ rids[i] = i;
61
+ rlen[i] = offs[i+1]-offs[i];
62
+ }
63
+
64
+ return;
65
+ }
66
+
67
+ at::Tensor sortRows(int nlat_out, at::Tensor row_off, cudaStream_t stream) {
68
+
69
+ int64_t *_row_off_d = reinterpret_cast<int64_t *>(row_off.data_ptr());
70
+
71
+ auto options = torch::TensorOptions().dtype(torch::kInt32).device(row_off.device());
72
+
73
+ torch::Tensor rids_d = torch::empty({nlat_out}, options);
74
+ torch::Tensor rlen_d = torch::empty({nlat_out}, options);
75
+
76
+ int *_rids_d = reinterpret_cast<int *>(rids_d.data_ptr());
77
+ int *_rlen_d = reinterpret_cast<int *>(rlen_d.data_ptr());
78
+
79
+ const int grid = DIV_UP(nlat_out, THREADS);
80
+ const int block = THREADS;
81
+
82
+ set_rlen_rids_k<<<grid, block, 0, stream>>>(nlat_out,
83
+ _row_off_d,
84
+ _rids_d,
85
+ _rlen_d);
86
+
87
+ torch::Tensor rids_sort_d = torch::empty({nlat_out}, options);
88
+ torch::Tensor rlen_sort_d = torch::empty({nlat_out}, options);
89
+
90
+ int *_rids_sort_d = reinterpret_cast<int *>(rids_sort_d.data_ptr());
91
+ int *_rlen_sort_d = reinterpret_cast<int *>(rlen_sort_d.data_ptr());
92
+
93
+ size_t temp_storage_bytes = 0;
94
+ CHECK_CUDA(cub::DeviceRadixSort::SortPairsDescending(NULL, temp_storage_bytes,
95
+ _rlen_d, _rlen_sort_d,
96
+ _rids_d, _rids_sort_d,
97
+ nlat_out, 0, sizeof(*_rlen_d)*8, stream));
98
+
99
+ options = torch::TensorOptions().dtype(torch::kByte).device(row_off.device());
100
+ torch::Tensor temp_storage_d = torch::empty({int64_t(temp_storage_bytes)}, options);
101
+
102
+ void *_temp_storage_d = reinterpret_cast<void *>(temp_storage_d.data_ptr());
103
+
104
+ CHECK_CUDA(cub::DeviceRadixSort::SortPairsDescending(_temp_storage_d, temp_storage_bytes,
105
+ _rlen_d, _rlen_sort_d,
106
+ _rids_d, _rids_sort_d,
107
+ nlat_out, 0, sizeof(*_rlen_d)*8, stream));
108
+ return rids_sort_d;
109
+ }
110
+ // END - CSR rows sorting kernels and functions
111
+
112
+
113
+ // BEGIN - 4D tensor permutation kernels and functions
114
+ __global__ void empty_k() {}
115
+
116
+ static int getPtxver() {
117
+ cudaFuncAttributes attrs;
118
+ CHECK_CUDA(cudaFuncGetAttributes(&attrs, empty_k));
119
+ return attrs.ptxVersion*10;
120
+ }
121
+
122
+ at::Tensor permute_4D_to0231(at::Tensor src) {
123
+
124
+ auto options = torch::TensorOptions().dtype(src.dtype()).device(src.device());
125
+ torch::Tensor dst = torch::empty({src.size(0), src.size(2), src.size(3), src.size(1)}, options);
126
+
127
+ const int ptxv = getPtxver();
128
+
129
+ // to be further specialized for additional archs, if necessary
130
+ if (ptxv < 100) {
131
+ AT_DISPATCH_FLOATING_TYPES(src.scalar_type(), "permute_to0231_k_tile_generic", ([&] {
132
+ launch_permute_to0231<TRANSP_WARPS_X_TILE_GENERIC, scalar_t>(src, dst);
133
+ }));
134
+ CHECK_ERROR("permute_to0231_k_tile_generic");
135
+ } else {
136
+ AT_DISPATCH_FLOATING_TYPES(src.scalar_type(), "permute_to0231_k_tile_sm100", ([&] {
137
+ launch_permute_to0231<TRANSP_WARPS_X_TILE_SM100, scalar_t>(src, dst);
138
+ }));
139
+ CHECK_ERROR("permute_to0231_k_tile_sm100");
140
+ }
141
+
142
+ return dst;
143
+ }
144
+
145
+ at::Tensor permute_4D_to0312(at::Tensor src) {
146
+
147
+ auto options = torch::TensorOptions().dtype(src.dtype()).device(src.device());
148
+ torch::Tensor dst = torch::empty({src.size(0), src.size(3), src.size(1), src.size(2)}, options);
149
+
150
+ const int ptxv = getPtxver();
151
+
152
+ // to be further specialized for additional archs, if necessary
153
+ if (ptxv < 100) {
154
+ AT_DISPATCH_FLOATING_TYPES(src.scalar_type(), "permute_to0312_k_tile_generic", ([&] {
155
+ launch_permute_to0312<TRANSP_WARPS_X_TILE_GENERIC, scalar_t>(src, dst);
156
+ }));
157
+ CHECK_ERROR("permute_to0312_k_tile_generic");
158
+ } else {
159
+ AT_DISPATCH_FLOATING_TYPES(src.scalar_type(), "permute_to0312_k_tile_sm100", ([&] {
160
+ launch_permute_to0312<TRANSP_WARPS_X_TILE_SM100, scalar_t>(src, dst);
161
+ }));
162
+ CHECK_ERROR("permute_to0312_k_tile_sm100");
163
+ }
164
+
165
+ return dst;
166
+ }
167
+ // END - tensor permutation kernels and functions
168
+
169
+ // BEGIN - general host-side functions
170
+ unsigned int next_pow2(unsigned int x) {
171
+
172
+ x -= 1;
173
+
174
+ #pragma unroll
175
+ for(int i = 1; i <= sizeof(x)*8 / 2; i *= 2) {
176
+ x |= x >> i;
177
+ }
178
+ return x+1;
179
+ }
180
+ // END - general host-side functions
.deps/torch_harmonics/csrc/attention/attention_utils.cuh ADDED
@@ -0,0 +1,373 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // coding=utf-8
2
+ //
3
+ // SPDX-FileCopyrightText: Copyright (c) 2025 The torch-harmonics Authors. All rights reserved.
4
+ // SPDX-License-Identifier: BSD-3-Clause
5
+ //
6
+ // Redistribution and use in source and binary forms, with or without
7
+ // modification, are permitted provided that the following conditions are met:
8
+ //
9
+ // 1. Redistributions of source code must retain the above copyright notice, this
10
+ // list of conditions and the following disclaimer.
11
+ //
12
+ // 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ // this list of conditions and the following disclaimer in the documentation
14
+ // and/or other materials provided with the distribution.
15
+ //
16
+ // 3. Neither the name of the copyright holder nor the names of its
17
+ // contributors may be used to endorse or promote products derived from
18
+ // this software without specific prior written permission.
19
+ //
20
+ // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+
31
+ #pragma once
32
+
33
+ #include <ATen/ATen.h>
34
+
35
+ #define WARP_SIZE (32)
36
+ #define FULL_MASK (0xFFFFFFFF)
37
+ #define DIV_UP(a,b) (((a)+((b)-1))/(b))
38
+
39
+ // CSR rows sorting kernels and functions
40
+ at::Tensor sortRows(int nlat_out, at::Tensor row_off, cudaStream_t stream);
41
+
42
+ // 4D tensor permutation kernels and functions
43
+ at::Tensor permute_4D_to0231(at::Tensor src);
44
+ at::Tensor permute_4D_to0312(at::Tensor src);
45
+
46
+ // Host tensor dump and CSR manipulation functions
47
+ void dump_tensor(const char *fname, at::Tensor t);
48
+ void dump_csr(const char *fname, at::Tensor roff, at::Tensor cols);
49
+
50
+ int part_csr_rows(int *row_perm,
51
+ const at::Tensor roff,
52
+ const at::Tensor cols,
53
+ int **part_off,
54
+ int **part_val);
55
+
56
+ int verify_part(const int npart,
57
+ const int *part_off,
58
+ const int *part_val,
59
+ const at::Tensor roff,
60
+ const at::Tensor cols);
61
+
62
+ void verify_part_new(const int nlon_out,
63
+ const int nlat_in,
64
+ const int nlon_in,
65
+ const int npart, // partitioning data
66
+ const int *part_off,
67
+ const int *part_val,
68
+ const at::Tensor roff,
69
+ const at::Tensor cols);
70
+
71
+ unsigned int next_pow2(unsigned int x);
72
+
73
+
74
+ // utility host functions and templates
75
+
76
+ template<unsigned int ALIGN>
77
+ int is_aligned(const void *ptr) {
78
+
79
+ static_assert(0 == (ALIGN & (ALIGN-1)));
80
+ return (0 == (uintptr_t(ptr) & (ALIGN-1)));
81
+ }
82
+
83
+
84
+ // utility device functions and templates
85
+
86
+ template<typename FLOATV_T>
87
+ __device__ FLOATV_T __vset(float x) {
88
+ static_assert(sizeof(FLOATV_T) == 0, "Unsupported type for __vset");
89
+ return FLOATV_T{};
90
+ }
91
+
92
+ template<>
93
+ __device__ float __forceinline__ __vset<float>(float x) {
94
+ return x;
95
+ }
96
+
97
+ __device__ float __forceinline__ __vmul(float a, float b) {
98
+ return a*b;
99
+ }
100
+
101
+ __device__ float __forceinline__ __vadd(float a, float b) {
102
+ return a+b;
103
+ }
104
+
105
+ __device__ float __forceinline__ __vsub(float a, float b) {
106
+ return a-b;
107
+ }
108
+
109
+ __device__ float __forceinline__ __vred(float a) {
110
+ return a;
111
+ }
112
+
113
+ __device__ float __forceinline__ __vscale(float s, float v) {
114
+ return v*s;
115
+ }
116
+
117
+ __device__ float __forceinline__ __vdiv(float s, float v) {
118
+ return v/s;
119
+ }
120
+
121
+ template<>
122
+ __device__ float4 __forceinline__ __vset<float4>(float x) {
123
+ return make_float4(x, x, x, x);
124
+ }
125
+
126
+ __device__ float4 __forceinline__ __vmul(float4 a, float4 b) {
127
+ return make_float4(a.x*b.x, a.y*b.y, a.z*b.z, a.w*b.w);
128
+ }
129
+
130
+ __device__ float4 __forceinline__ __vadd(float4 a, float4 b) {
131
+ return make_float4(a.x+b.x, a.y+b.y, a.z+b.z, a.w+b.w);
132
+ }
133
+
134
+ __device__ float4 __forceinline__ __vsub(float4 a, float4 b) {
135
+ return make_float4(a.x-b.x, a.y-b.y, a.z-b.z, a.w-b.w);
136
+ }
137
+
138
+ __device__ float __forceinline__ __vred(float4 a) {
139
+ return a.x + a.y + a.z + a.w;
140
+ }
141
+
142
+ __device__ float4 __forceinline__ __vscale(float s, float4 v) {
143
+ return make_float4(s*v.x, s*v.y, s*v.z, s*v.w);
144
+ }
145
+
146
+ __device__ float4 __forceinline__ __vdiv(float s, float4 v) {
147
+ return make_float4(s/v.x, s/v.y, s/v.z, s/v.w);;
148
+ }
149
+
150
+ template<typename VAL_T>
151
+ __device__ VAL_T __warp_sum(VAL_T val) {
152
+
153
+ #pragma unroll
154
+ for(int i = WARP_SIZE/2; i; i /= 2) {
155
+ val += __shfl_xor_sync(FULL_MASK, val, i, WARP_SIZE);
156
+ }
157
+ return val;
158
+ }
159
+
160
+ template<int BDIM_X,
161
+ int BDIM_Y=1,
162
+ int BDIM_Z=1,
163
+ typename VAL_T>
164
+ __device__ VAL_T __block_sum(VAL_T val) {
165
+
166
+ const int NWARP = (BDIM_X*BDIM_Y*BDIM_Z) / WARP_SIZE;
167
+
168
+ val = __warp_sum(val);
169
+
170
+ if constexpr(NWARP > 1) {
171
+
172
+ int tid = threadIdx.x;
173
+ if constexpr(BDIM_Y > 1) { tid += threadIdx.y*BDIM_X; }
174
+ if constexpr(BDIM_Z > 1) { tid += threadIdx.z*BDIM_X*BDIM_Y; }
175
+
176
+ const int lid = tid%WARP_SIZE;
177
+ const int wid = tid/WARP_SIZE;
178
+
179
+ __shared__ VAL_T sh[NWARP];
180
+
181
+ if (lid == 0) {
182
+ sh[wid] = val;
183
+ }
184
+ __syncthreads();
185
+
186
+ if (wid == 0) {
187
+ val = (lid < NWARP) ? sh[lid] : 0;
188
+
189
+ val = __warp_sum(val);
190
+ __syncwarp();
191
+
192
+ if (!lid) {
193
+ sh[0] = val;
194
+ }
195
+ }
196
+ __syncthreads();
197
+
198
+ val = sh[0];
199
+ __syncthreads();
200
+ }
201
+ return val;
202
+ }
203
+
204
+ // transpose utils
205
+ template<int BDIM_X,
206
+ int BDIM_Y,
207
+ typename VAL_T>
208
+ __global__
209
+ __launch_bounds__(BDIM_X*BDIM_Y)
210
+ void permute_to0231_k(const int nchn,
211
+ const int nlat,
212
+ const int nlon,
213
+ const at::PackedTensorAccessor32<VAL_T, 4, at::RestrictPtrTraits> src,
214
+ at::PackedTensorAccessor32<VAL_T, 4, at::RestrictPtrTraits> dst) {
215
+
216
+ static_assert(!(BDIM_X & (BDIM_X-1)));
217
+ static_assert(!(BDIM_Y & (BDIM_Y-1)));
218
+ static_assert(BDIM_X >= BDIM_Y);
219
+
220
+ __shared__ VAL_T sh[BDIM_X][BDIM_X+1];
221
+
222
+ const int tidx = threadIdx.x;
223
+ const int tidy = threadIdx.y;
224
+
225
+ const int coff = blockIdx.x*BDIM_X; // channel offset
226
+ const int woff = blockIdx.y*BDIM_X; // width offset
227
+ const int batch = blockIdx.z / nlat; // batch (same for all block)
228
+ const int h = blockIdx.z - (batch * nlat); // height (same for all block)
229
+
230
+ const int nchn_full = (nchn-coff) >= BDIM_X;
231
+ const int nlon_full = (nlon-woff) >= BDIM_X;
232
+
233
+ if (nchn_full && nlon_full) {
234
+ #pragma unroll
235
+ for(int j = 0; j < BDIM_X; j += BDIM_Y) {
236
+ sh[j+tidy][tidx] = src[batch][coff + j+tidy][h][woff+tidx];
237
+ }
238
+ __syncthreads();
239
+
240
+ #pragma unroll
241
+ for(int j = 0; j < BDIM_X; j += BDIM_Y) {
242
+ dst[batch][h][woff + j+tidy][coff+tidx] = sh[tidx][j+tidy];
243
+ }
244
+ } else {
245
+ if (woff+tidx < nlon) {
246
+ #pragma unroll
247
+ for(int j = 0; j < BDIM_X; j += BDIM_Y) {
248
+ sh[j+tidy][tidx] = (coff + j+tidy < nchn) ? src[batch][coff + j+tidy][h][woff+tidx] : VAL_T(0);
249
+ }
250
+ }
251
+ __syncthreads();
252
+
253
+ if (coff+tidx < nchn) {
254
+ #pragma unroll
255
+ for(int j = 0; j < BDIM_X; j += BDIM_Y) {
256
+ if (woff + j+tidy < nlon) {
257
+ dst[batch][h][woff + j+tidy][coff+tidx] = sh[tidx][j+tidy];
258
+ }
259
+ }
260
+ }
261
+ }
262
+ return;
263
+ }
264
+
265
+ template<int WARPS_X_TILE, typename VAL_T>
266
+ void launch_permute_to0231(at::Tensor src, at::Tensor dst){
267
+ dim3 block;
268
+ dim3 grid;
269
+
270
+ block.x = WARP_SIZE;
271
+ block.y = WARPS_X_TILE;
272
+ grid.x = DIV_UP(src.size(1), block.x);
273
+ grid.y = DIV_UP(src.size(3), block.x);
274
+ grid.z = src.size(2)*src.size(0);
275
+
276
+ assert(grid.y < 65536);
277
+ assert(grid.z < 65536);
278
+
279
+ // get stream
280
+ auto stream = at::cuda::getCurrentCUDAStream().stream();
281
+
282
+ permute_to0231_k<WARP_SIZE, WARPS_X_TILE>
283
+ <<<grid, block, 0, stream>>>(src.size(1),
284
+ src.size(2),
285
+ src.size(3),
286
+ src.packed_accessor32<VAL_T, 4, at::RestrictPtrTraits>(),
287
+ dst.packed_accessor32<VAL_T, 4, at::RestrictPtrTraits>());
288
+ }
289
+
290
+ template<int BDIM_X,
291
+ int BDIM_Y,
292
+ typename VAL_T>
293
+ __global__
294
+ __launch_bounds__(BDIM_X*BDIM_Y)
295
+ void permute_to0312_k(const int nchn,
296
+ const int nlat,
297
+ const int nlon,
298
+ const at::PackedTensorAccessor32<VAL_T, 4, at::RestrictPtrTraits> src,
299
+ at::PackedTensorAccessor32<VAL_T, 4, at::RestrictPtrTraits> dst) {
300
+
301
+ static_assert(!(BDIM_X & (BDIM_X-1)));
302
+ static_assert(!(BDIM_Y & (BDIM_Y-1)));
303
+ static_assert(BDIM_X >= BDIM_Y);
304
+
305
+ __shared__ VAL_T sh[BDIM_X][BDIM_X+1];
306
+
307
+ const int tidx = threadIdx.x;
308
+ const int tidy = threadIdx.y;
309
+
310
+ const int woff = blockIdx.x*BDIM_X; // width offset
311
+ const int coff = blockIdx.y*BDIM_X; // channel offset
312
+ const int batch = blockIdx.z / nlat; // batch (same for all block)
313
+ const int h = blockIdx.z - (batch * nlat); // height (same for all block)
314
+
315
+ const int nchn_full = (nchn-coff) >= BDIM_X;
316
+ const int nlon_full = (nlon-woff) >= BDIM_X;
317
+
318
+ if (nchn_full && nlon_full) {
319
+ #pragma unroll
320
+ for(int j = 0; j < BDIM_X; j += BDIM_Y) {
321
+ sh[j+tidy][tidx] = src[batch][h][woff + j+tidy][coff+tidx];
322
+ }
323
+ __syncthreads();
324
+
325
+ #pragma unroll
326
+ for(int j = 0; j < BDIM_X; j += BDIM_Y) {
327
+ dst[batch][coff + j+tidy][h][woff+tidx] = sh[tidx][j+tidy];
328
+ }
329
+ } else {
330
+ if (coff+tidx < nchn) {
331
+ #pragma unroll
332
+ for(int j = 0; j < BDIM_X; j += BDIM_Y) {
333
+ sh[j+tidy][tidx] = (woff + j+tidy < nlon) ? src[batch][h][woff + j+tidy][coff+tidx] : VAL_T(0);
334
+ }
335
+ }
336
+ __syncthreads();
337
+
338
+ if (woff+tidx < nlon) {
339
+ #pragma unroll
340
+ for(int j = 0; j < BDIM_X; j += BDIM_Y) {
341
+ if (coff + j+tidy < nchn) {
342
+ dst[batch][coff + j+tidy][h][woff+tidx] = sh[tidx][j+tidy];;
343
+ }
344
+ }
345
+ }
346
+ }
347
+ return;
348
+ }
349
+
350
+ template<int WARPS_X_TILE, typename VAL_T>
351
+ void launch_permute_to0312(at::Tensor src, at::Tensor dst){
352
+ dim3 block;
353
+ dim3 grid;
354
+
355
+ block.x = WARP_SIZE;
356
+ block.y = WARPS_X_TILE;
357
+ grid.x = DIV_UP(src.size(2), block.x);
358
+ grid.y = DIV_UP(src.size(3), block.x);
359
+ grid.z = src.size(1)*src.size(0);
360
+
361
+ assert(grid.y < 65536);
362
+ assert(grid.z < 65536);
363
+
364
+ // get stream
365
+ auto stream = at::cuda::getCurrentCUDAStream().stream();
366
+
367
+ permute_to0312_k<WARP_SIZE, WARPS_X_TILE>
368
+ <<<grid, block, 0, stream>>>(src.size(3),
369
+ src.size(1),
370
+ src.size(2),
371
+ src.packed_accessor32<VAL_T, 4, at::RestrictPtrTraits>(),
372
+ dst.packed_accessor32<VAL_T, 4, at::RestrictPtrTraits>());
373
+ }
.deps/torch_harmonics/csrc/attention/cudamacro.h ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // coding=utf-8
2
+ //
3
+ // SPDX-FileCopyrightText: Copyright (c) 2025 The torch-harmonics Authors. All rights reserved.
4
+ // SPDX-License-Identifier: BSD-3-Clause
5
+ //
6
+ // Redistribution and use in source and binary forms, with or without
7
+ // modification, are permitted provided that the following conditions are met:
8
+ //
9
+ // 1. Redistributions of source code must retain the above copyright notice, this
10
+ // list of conditions and the following disclaimer.
11
+ //
12
+ // 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ // this list of conditions and the following disclaimer in the documentation
14
+ // and/or other materials provided with the distribution.
15
+ //
16
+ // 3. Neither the name of the copyright holder nor the names of its
17
+ // contributors may be used to endorse or promote products derived from
18
+ // this software without specific prior written permission.
19
+ //
20
+ // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+
31
+ #pragma once
32
+
33
+ #define CHECK_CUDA(call) { \
34
+ cudaError_t err = call; \
35
+ if( cudaSuccess != err) { \
36
+ fprintf(stderr, "Cuda error in file '%s' in line %i : %s.\n", \
37
+ __FILE__, __LINE__, cudaGetErrorString( err) ); \
38
+ exit(EXIT_FAILURE); \
39
+ }}
40
+
41
+ #define CHECK_ERROR(errorMessage) { \
42
+ cudaError_t err = cudaGetLastError(); \
43
+ if( cudaSuccess != err) { \
44
+ fprintf(stderr, "Cuda error: %s in file '%s' in line %i : %s.\n", \
45
+ errorMessage, __FILE__, __LINE__, cudaGetErrorString( err) );\
46
+ exit(EXIT_FAILURE); \
47
+ }}
.deps/torch_harmonics/csrc/disco/disco.h ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // coding=utf-8
2
+ //
3
+ // SPDX-FileCopyrightText: Copyright (c) 2024 The torch-harmonics Authors. All rights reserved.
4
+ // SPDX-License-Identifier: BSD-3-Clause
5
+ //
6
+ // Redistribution and use in source and binary forms, with or without
7
+ // modification, are permitted provided that the following conditions are met:
8
+ //
9
+ // 1. Redistributions of source code must retain the above copyright notice, this
10
+ // list of conditions and the following disclaimer.
11
+ //
12
+ // 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ // this list of conditions and the following disclaimer in the documentation
14
+ // and/or other materials provided with the distribution.
15
+ //
16
+ // 3. Neither the name of the copyright holder nor the names of its
17
+ // contributors may be used to endorse or promote products derived from
18
+ // this software without specific prior written permission.
19
+ //
20
+ // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+
31
+ #pragma once
32
+
33
+ #include <stdio.h>
34
+ #include <stdlib.h>
35
+ #include <torch/extension.h>
36
+ #include <cassert>
37
+
38
+ #define CHECK_CONTIGUOUS_TENSOR(x) TORCH_CHECK(x.is_contiguous(), #x " must be contiguous")
39
+ #define CHECK_INPUT_TENSOR(x) CHECK_CONTIGUOUS_TENSOR(x)
.deps/torch_harmonics/csrc/disco/disco_cuda.cuh ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // coding=utf-8
2
+ //
3
+ // SPDX-FileCopyrightText: Copyright (c) 2024 The torch-harmonics Authors. All rights reserved.
4
+ // SPDX-License-Identifier: BSD-3-Clause
5
+ //
6
+ // Redistribution and use in source and binary forms, with or without
7
+ // modification, are permitted provided that the following conditions are met:
8
+ //
9
+ // 1. Redistributions of source code must retain the above copyright notice, this
10
+ // list of conditions and the following disclaimer.
11
+ //
12
+ // 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ // this list of conditions and the following disclaimer in the documentation
14
+ // and/or other materials provided with the distribution.
15
+ //
16
+ // 3. Neither the name of the copyright holder nor the names of its
17
+ // contributors may be used to endorse or promote products derived from
18
+ // this software without specific prior written permission.
19
+ //
20
+ // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+
31
+ #pragma once
32
+
33
+ #include "disco.h"
34
+
35
+ #include <cuda_runtime.h>
36
+ #include <c10/cuda/CUDAStream.h>
37
+
38
+ #define CHECK_CUDA_TENSOR(x) TORCH_CHECK(x.device().is_cuda(), #x " must be a CUDA tensor")
39
+ #define CHECK_CUDA_INPUT_TENSOR(x) \
40
+ CHECK_CUDA_TENSOR(x); \
41
+ CHECK_CONTIGUOUS_TENSOR(x)
42
+
43
+ #define DIV_UP(a, b) (((a) + ((b)-1)) / (b))
44
+
45
+ #define MIN_THREADS (64)
46
+ #define ELXTH_MAX (32)
47
+
48
+ // forward kernel
49
+ torch::Tensor disco_cuda_fwd(torch::Tensor inp, torch::Tensor roff_idx, torch::Tensor ker_idx, torch::Tensor row_idx,
50
+ torch::Tensor col_idx, torch::Tensor val, int64_t K, int64_t Ho, int64_t Wo);
51
+
52
+ // backward kernel
53
+ torch::Tensor disco_cuda_bwd(torch::Tensor inp, torch::Tensor roff_idx, torch::Tensor ker_idx, torch::Tensor row_idx,
54
+ torch::Tensor col_idx, torch::Tensor val, int64_t K, int64_t Ho, int64_t Wo);
.deps/torch_harmonics/csrc/disco/disco_cuda_bwd.cu ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // coding=utf-8
2
+ //
3
+ // SPDX-FileCopyrightText: Copyright (c) 2024 The torch-harmonics Authors. All rights reserved.
4
+ // SPDX-License-Identifier: BSD-3-Clause
5
+ //
6
+ // Redistribution and use in source and binary forms, with or without
7
+ // modification, are permitted provided that the following conditions are met:
8
+ //
9
+ // 1. Redistributions of source code must retain the above copyright notice, this
10
+ // list of conditions and the following disclaimer.
11
+ //
12
+ // 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ // this list of conditions and the following disclaimer in the documentation
14
+ // and/or other materials provided with the distribution.
15
+ //
16
+ // 3. Neither the name of the copyright holder nor the names of its
17
+ // contributors may be used to endorse or promote products derived from
18
+ // this software without specific prior written permission.
19
+ //
20
+ // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+
31
+ #include "disco.h"
32
+ #include "disco_cuda.cuh"
33
+
34
+ template <int BDIM_X, int ELXTH, typename REAL_T>
35
+ __device__ void disco_bwd_d(const int Hi, const int Wi, const int K, const int Ho, const int Wo, const int pscale,
36
+ const int64_t *__restrict__ roff, const int64_t *__restrict__ kers,
37
+ const int64_t *__restrict__ rows, const int64_t *__restrict__ cols,
38
+ const REAL_T *__restrict__ vals, const REAL_T *__restrict__ inp, REAL_T *__restrict__ out)
39
+ {
40
+
41
+ const int tid = threadIdx.x;
42
+
43
+ const int64_t bidx = blockIdx.x; // gloabl row
44
+ const int64_t bidy = blockIdx.y; // bc
45
+
46
+ int64_t soff = roff[bidx];
47
+ int64_t eoff = roff[bidx + 1];
48
+
49
+ const int64_t ker = kers[soff];
50
+ const int64_t row = rows[soff];
51
+
52
+ inp += bidy * K * Hi * Wi + ker * Hi * Wi + row * Wi;
53
+ out += bidy * Ho * Wo;
54
+
55
+ // align to larger supported fp type
56
+ extern __shared__ __align__(sizeof(double)) unsigned char __sh_ptr[]; // REAL_T __sh[2*(BDIM_X*ELXTH)*pscale]
57
+
58
+ REAL_T(*__sh)[BDIM_X * ELXTH * 2] = reinterpret_cast<REAL_T(*)[BDIM_X * ELXTH * 2]>(__sh_ptr);
59
+
60
+ // copy current inp row in regs
61
+ REAL_T __reg[ELXTH];
62
+
63
+ #pragma unroll
64
+ for (int i = 0; i < ELXTH; i++) { __reg[i] = (i * BDIM_X + tid < Wi) ? inp[i * BDIM_X + tid] : REAL_T(0); }
65
+
66
+ // reset shared row up to Wo+2, remaining
67
+ // ppscale*(BDIM_X*ELXTH - Wo) locations
68
+ // will be written to but never copied to
69
+ // global mem
70
+ for (int i = 0; i < pscale; i++) {
71
+ #pragma unroll
72
+ for (int j = 0; j < 2 * BDIM_X * ELXTH; j += BDIM_X) { __sh[i][j + tid] = 0; }
73
+ }
74
+ __syncthreads();
75
+
76
+ int col_prev = cols[soff];
77
+
78
+ int h_prev = col_prev / Wo;
79
+ int w_prev = col_prev % Wo;
80
+
81
+ // loops along the colums of CTA's row
82
+ for (int64_t nz = soff; nz < eoff; nz++) {
83
+
84
+ const int col = cols[nz];
85
+ const REAL_T val = vals[nz];
86
+
87
+ // if we are processing a nz with a col value
88
+ // leading to a new row of inp then copy it
89
+ // to shmem;
90
+ // we read a col that points to a new output
91
+ // row if (col / Wo) > (col_prev / Wo)
92
+ if (col >= col_prev - w_prev + Wo) {
93
+ __syncthreads();
94
+ for (int i = 0; i < pscale; i++) {
95
+ for (int j = tid; j < Wi; j += BDIM_X) {
96
+
97
+ const REAL_T v = __sh[i][j] + __sh[i][Wi + j];
98
+
99
+ atomicAdd(&out[h_prev * Wo + j * pscale + i], v);
100
+
101
+ __sh[i][j] = 0;
102
+ __sh[i][Wi + j] = 0;
103
+ }
104
+ }
105
+ __syncthreads();
106
+
107
+ col_prev = col;
108
+ h_prev = col / Wo;
109
+ w_prev = col % Wo;
110
+ }
111
+
112
+ const int w = w_prev + (col - col_prev);
113
+ const int w_mod_ps = w % pscale;
114
+ const int w_div_ps = w / pscale;
115
+
116
+ #pragma unroll
117
+ for (int i = 0; i < ELXTH; i++) {
118
+
119
+ const int pp = i * BDIM_X + tid;
120
+ __sh[w_mod_ps][w_div_ps + pp] += val * __reg[i];
121
+ }
122
+
123
+ // to avoid race conditions on __sh[]
124
+ // among consecutive iterations along nz
125
+ __syncthreads();
126
+ }
127
+ __syncthreads();
128
+
129
+ // write last row
130
+ for (int i = 0; i < pscale; i++) {
131
+
132
+ for (int j = tid; j < Wi; j += BDIM_X) {
133
+
134
+ const REAL_T v = __sh[i][j] + __sh[i][Wi + j];
135
+ atomicAdd(&out[h_prev * Wo + j * pscale + i], v);
136
+ }
137
+ }
138
+ return;
139
+ }
140
+
141
+ template <int BDIM_X, int ELXTH, int PSCALE, typename REAL_T>
142
+ __global__
143
+ __launch_bounds__(BDIM_X) void disco_bwd_blk_k(const int Hi, const int Wi, const int K, const int Ho, const int Wo,
144
+ const int pscale, const int64_t *__restrict__ roff,
145
+ const int64_t *__restrict__ kers, const int64_t *__restrict__ rows,
146
+ const int64_t *__restrict__ cols, const REAL_T *__restrict__ vals,
147
+ const REAL_T *__restrict__ inp, REAL_T *__restrict__ out)
148
+ {
149
+
150
+ if constexpr (PSCALE != 0) {
151
+ disco_bwd_d<BDIM_X, ELXTH>(Hi, Wi, K, Ho, Wo, PSCALE, roff, kers, rows, cols, vals, inp, out);
152
+ } else {
153
+ disco_bwd_d<BDIM_X, ELXTH>(Hi, Wi, K, Ho, Wo, pscale, roff, kers, rows, cols, vals, inp, out);
154
+ }
155
+
156
+ return;
157
+ }
158
+
159
+ template <int NTH, int ELXTH, typename REAL_T>
160
+ static void launch_kernel(int BC, int Hi, int Wi, int K, int Ho, int Wo, int64_t nrows, int64_t *roff_d, int64_t *ker_d,
161
+ int64_t *row_d, int64_t *col_d, REAL_T *val_d, REAL_T *inp_d, REAL_T *out_d,
162
+ cudaStream_t stream)
163
+ {
164
+
165
+ static_assert(sizeof(REAL_T) == 2 || sizeof(REAL_T) == 4 || sizeof(REAL_T) == 8);
166
+
167
+ if constexpr (ELXTH <= ELXTH_MAX) {
168
+ if (NTH * ELXTH >= Wi) {
169
+ dim3 grid(nrows, BC);
170
+
171
+ const int pscale = Wo / Wi;
172
+ size_t shmem = sizeof(*out_d) * (2 * (NTH * ELXTH) * pscale);
173
+
174
+ switch (pscale) {
175
+ case 1:
176
+ disco_bwd_blk_k<NTH, ELXTH, 1><<<grid, NTH, shmem, stream>>>(Hi, Wi, K, Ho, Wo, pscale, roff_d, ker_d,
177
+ row_d, col_d, val_d, inp_d, out_d);
178
+ break;
179
+ case 2:
180
+ disco_bwd_blk_k<NTH, ELXTH, 2><<<grid, NTH, shmem, stream>>>(Hi, Wi, K, Ho, Wo, pscale, roff_d, ker_d,
181
+ row_d, col_d, val_d, inp_d, out_d);
182
+ break;
183
+ case 3:
184
+ disco_bwd_blk_k<NTH, ELXTH, 3><<<grid, NTH, shmem, stream>>>(Hi, Wi, K, Ho, Wo, pscale, roff_d, ker_d,
185
+ row_d, col_d, val_d, inp_d, out_d);
186
+ break;
187
+ default:
188
+ disco_bwd_blk_k<NTH, ELXTH, 0><<<grid, NTH, shmem, stream>>>(Hi, Wi, K, Ho, Wo, pscale, roff_d, ker_d,
189
+ row_d, col_d, val_d, inp_d, out_d);
190
+ }
191
+ } else {
192
+ launch_kernel<NTH, ELXTH + 1>(BC, Hi, Wi, K, Ho, Wo, nrows, roff_d, ker_d, row_d, col_d, val_d, inp_d,
193
+ out_d, stream);
194
+ }
195
+ }
196
+ return;
197
+ }
198
+
199
+ torch::Tensor disco_cuda_bwd(torch::Tensor inp, torch::Tensor roff_idx, torch::Tensor ker_idx, torch::Tensor row_idx,
200
+ torch::Tensor col_idx, torch::Tensor val, int64_t K, int64_t Ho, int64_t Wo)
201
+ {
202
+
203
+ // some sanity checks
204
+ CHECK_CUDA_INPUT_TENSOR(inp);
205
+ CHECK_CUDA_INPUT_TENSOR(roff_idx);
206
+ CHECK_CUDA_INPUT_TENSOR(ker_idx);
207
+ CHECK_CUDA_INPUT_TENSOR(row_idx);
208
+ CHECK_CUDA_INPUT_TENSOR(col_idx);
209
+ CHECK_CUDA_INPUT_TENSOR(val);
210
+
211
+ // extract some shapes
212
+ int64_t B = inp.size(0);
213
+ int64_t C = inp.size(1);
214
+ int64_t BC = B * C;
215
+ int64_t Hi = inp.size(3);
216
+ int64_t Wi = inp.size(4);
217
+ int64_t nrows = roff_idx.size(0) - 1;
218
+
219
+ // allocate output
220
+ int64_t out_dims[] = {B, C, Ho, Wo};
221
+ auto options = torch::TensorOptions().device(inp.device()).dtype(inp.dtype());
222
+ torch::Tensor out = torch::zeros(out_dims, options);
223
+
224
+ // get stream
225
+ auto stream = at::cuda::getCurrentCUDAStream().stream();
226
+
227
+ // assert
228
+ static_assert(0 == (ELXTH_MAX % 2));
229
+
230
+ if (Wo <= 64 * ELXTH_MAX) {
231
+ AT_DISPATCH_FLOATING_TYPES(inp.scalar_type(), "disco_backward_cuda", ([&] {
232
+ launch_kernel<64, 1, scalar_t>(
233
+ BC, Hi, Wi, K, Ho, Wo, nrows, roff_idx.data_ptr<int64_t>(),
234
+ ker_idx.data_ptr<int64_t>(), row_idx.data_ptr<int64_t>(),
235
+ col_idx.data_ptr<int64_t>(), val.data_ptr<scalar_t>(),
236
+ inp.data_ptr<scalar_t>(), out.data_ptr<scalar_t>(), stream);
237
+ }));
238
+ } else if (Wo <= 128 * ELXTH_MAX) {
239
+ AT_DISPATCH_FLOATING_TYPES(inp.scalar_type(), "disco_backward_cuda", ([&] {
240
+ launch_kernel<128, (ELXTH_MAX / 2) + 1, scalar_t>(
241
+ BC, Hi, Wi, K, Ho, Wo, nrows, roff_idx.data_ptr<int64_t>(),
242
+ ker_idx.data_ptr<int64_t>(), row_idx.data_ptr<int64_t>(),
243
+ col_idx.data_ptr<int64_t>(), val.data_ptr<scalar_t>(),
244
+ inp.data_ptr<scalar_t>(), out.data_ptr<scalar_t>(), stream);
245
+ }));
246
+ } else if (Wo <= 256 * ELXTH_MAX) {
247
+ AT_DISPATCH_FLOATING_TYPES(inp.scalar_type(), "disco_backward_cuda", ([&] {
248
+ launch_kernel<256, (ELXTH_MAX / 2) + 1, scalar_t>(
249
+ BC, Hi, Wi, K, Ho, Wo, nrows, roff_idx.data_ptr<int64_t>(),
250
+ ker_idx.data_ptr<int64_t>(), row_idx.data_ptr<int64_t>(),
251
+ col_idx.data_ptr<int64_t>(), val.data_ptr<scalar_t>(),
252
+ inp.data_ptr<scalar_t>(), out.data_ptr<scalar_t>(), stream);
253
+ }));
254
+ } else if (Wo <= 512 * ELXTH_MAX) {
255
+ AT_DISPATCH_FLOATING_TYPES(inp.scalar_type(), "disco_backward_cuda", ([&] {
256
+ launch_kernel<512, (ELXTH_MAX / 2) + 1, scalar_t>(
257
+ BC, Hi, Wi, K, Ho, Wo, nrows, roff_idx.data_ptr<int64_t>(),
258
+ ker_idx.data_ptr<int64_t>(), row_idx.data_ptr<int64_t>(),
259
+ col_idx.data_ptr<int64_t>(), val.data_ptr<scalar_t>(),
260
+ inp.data_ptr<scalar_t>(), out.data_ptr<scalar_t>(), stream);
261
+ }));
262
+ } else if (Wo <= 1024 * ELXTH_MAX) {
263
+ AT_DISPATCH_FLOATING_TYPES(inp.scalar_type(), "disco_backward_cuda", ([&] {
264
+ launch_kernel<1024, (ELXTH_MAX / 2) + 1, scalar_t>(
265
+ BC, Hi, Wi, K, Ho, Wo, nrows, roff_idx.data_ptr<int64_t>(),
266
+ ker_idx.data_ptr<int64_t>(), row_idx.data_ptr<int64_t>(),
267
+ col_idx.data_ptr<int64_t>(), val.data_ptr<scalar_t>(),
268
+ inp.data_ptr<scalar_t>(), out.data_ptr<scalar_t>(), stream);
269
+ }));
270
+ } else {
271
+ fprintf(stderr, "%s:%d: error, unsupported Wo value (%ld), max supported is %d\n", __FILE__, __LINE__, Wo,
272
+ 1024 * ELXTH_MAX);
273
+ exit(EXIT_FAILURE);
274
+ }
275
+
276
+ return out;
277
+ }
278
+
279
+ // PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
280
+ // m.def("backward", &disco_cuda_bwd, "DISCO backward (CUDA)");
281
+ //}
.deps/torch_harmonics/csrc/disco/disco_cuda_fwd.cu ADDED
@@ -0,0 +1,265 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // coding=utf-8
2
+ //
3
+ // SPDX-FileCopyrightText: Copyright (c) 2024 The torch-harmonics Authors. All rights reserved.
4
+ // SPDX-License-Identifier: BSD-3-Clause
5
+ //
6
+ // Redistribution and use in source and binary forms, with or without
7
+ // modification, are permitted provided that the following conditions are met:
8
+ //
9
+ // 1. Redistributions of source code must retain the above copyright notice, this
10
+ // list of conditions and the following disclaimer.
11
+ //
12
+ // 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ // this list of conditions and the following disclaimer in the documentation
14
+ // and/or other materials provided with the distribution.
15
+ //
16
+ // 3. Neither the name of the copyright holder nor the names of its
17
+ // contributors may be used to endorse or promote products derived from
18
+ // this software without specific prior written permission.
19
+ //
20
+ // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+
31
+ #include "disco.h"
32
+ #include "disco_cuda.cuh"
33
+
34
+ template <int BDIM_X, int ELXTH, typename REAL_T>
35
+ __device__ void disco_fwd_d(const int Hi, const int Wi, const int K, const int Ho, const int Wo, const int pscale,
36
+ const int64_t *__restrict__ roff, const int64_t *__restrict__ kers,
37
+ const int64_t *__restrict__ rows, const int64_t *__restrict__ cols,
38
+ const REAL_T *__restrict__ vals, const REAL_T *__restrict__ inp, REAL_T *__restrict__ out)
39
+ {
40
+
41
+ const int tid = threadIdx.x;
42
+
43
+ const int64_t bidx = blockIdx.x; // gloabl row
44
+ const int64_t bidy = blockIdx.y; // bc
45
+
46
+ int64_t soff = roff[bidx];
47
+ int64_t eoff = roff[bidx + 1];
48
+
49
+ const int64_t ker = kers[soff];
50
+ const int64_t row = rows[soff];
51
+
52
+ inp += bidy * Hi * Wi;
53
+ out += bidy * K * Ho * Wo + ker * Ho * Wo + row * Wo;
54
+
55
+ REAL_T __reg[ELXTH] = {0};
56
+
57
+ // align to larger supported fp type
58
+ extern __shared__ __align__(
59
+ sizeof(double)) unsigned char __sh_ptr[]; // REAL_T __sh[2*Wi + ppscale*(BDIM_X*ELXTH - Wo)]
60
+ REAL_T *__sh = reinterpret_cast<REAL_T *>(__sh_ptr);
61
+
62
+ int col_prev = cols[soff];
63
+
64
+ int h_prev = col_prev / Wi;
65
+ int w_prev = col_prev % Wi;
66
+
67
+ // copy current inp row in shmem
68
+ for (int i = tid; i < Wi; i += BDIM_X) {
69
+ const REAL_T v = inp[h_prev * Wi + i];
70
+ __sh[i] = v;
71
+ __sh[Wi + i] = v;
72
+ }
73
+ // locations __sh[2*Wi : ppscale*(BDIM_X*ELXTH-Wo)] are not used
74
+ __syncthreads();
75
+
76
+ // loops along the colums of CTA's row
77
+ for (int64_t nz = soff; nz < eoff; nz++) {
78
+
79
+ const int col = cols[nz];
80
+ const REAL_T val = vals[nz];
81
+
82
+ // if we are processing a nz with a col value
83
+ // leading to a new row of inp then copy it
84
+ // to shmem;
85
+ // checks whether (h_prev < h) with:
86
+ // (col >= col_prev - (col_prev % Wi) + Wi)
87
+ if (col >= col_prev - w_prev + Wi) {
88
+
89
+ col_prev = col;
90
+ h_prev = col / Wi;
91
+ w_prev = col % Wi;
92
+
93
+ __syncthreads();
94
+ for (int i = tid; i < Wi; i += BDIM_X) {
95
+ const REAL_T v = inp[h_prev * Wi + i];
96
+ __sh[i] = v;
97
+ __sh[Wi + i] = v;
98
+ }
99
+ __syncthreads();
100
+ }
101
+
102
+ const int w = w_prev + (col - col_prev);
103
+
104
+ #pragma unroll
105
+ for (int i = 0; i < ELXTH; i++) {
106
+
107
+ const int pp = i * BDIM_X + tid;
108
+
109
+ // original lines:
110
+ //
111
+ // if (pp >= Wo) break;
112
+ // const int wpp = (w + pscale*pp) % Wi;
113
+ //
114
+ // value of (w + pscale*pp) < (Wi + (Wi/Wo)*Wo) = 2*Wi
115
+ // so we can allocate twice the amount of shmem,
116
+ // replicate the current inp row and avoid the costly mod
117
+ //
118
+ // also, to avoid the conditional, sh can be extended to
119
+ // cover the maximum location accessed during this loop
120
+ //
121
+ // REAL_T __sh[2*Wi + ppscale*NUM_REM]
122
+ //
123
+ // Wi + (Wi/Wo)*BDIM_X*ELXTH = (since BDIM_X*ELXTH >= Wo) =
124
+ // = Wi + (Wi/Wo)*(Wo + (BDIM_X*ELXTH - Wo)) =
125
+ // = 2*Wi + ppscale*NUM_REM
126
+ //
127
+ // with NUM_REM = BDIM_X*ELXTH - Wo
128
+
129
+ const int wpp = w + pscale * pp;
130
+
131
+ __reg[i] += val * __sh[wpp];
132
+ }
133
+ }
134
+
135
+ #pragma unroll
136
+ for (int i = 0; i < ELXTH; i++) {
137
+
138
+ const int pp = i * BDIM_X + tid;
139
+ if (pp >= Wo) break;
140
+
141
+ out[pp] = __reg[i];
142
+ }
143
+
144
+ return;
145
+ }
146
+
147
+ template <int BDIM_X, int ELXTH, typename REAL_T>
148
+ __global__
149
+ __launch_bounds__(BDIM_X) void disco_fwd_blk_k(const int Hi, const int Wi, const int K, const int Ho, const int Wo,
150
+ const int pscale, const int64_t *__restrict__ roff,
151
+ const int64_t *__restrict__ kers, const int64_t *__restrict__ rows,
152
+ const int64_t *__restrict__ cols, const REAL_T *__restrict__ vals,
153
+ const REAL_T *__restrict__ inp, REAL_T *__restrict__ out)
154
+ {
155
+
156
+ disco_fwd_d<BDIM_X, ELXTH>(Hi, Wi, K, Ho, Wo, pscale, roff, kers, rows, cols, vals, inp, out);
157
+
158
+ return;
159
+ }
160
+
161
+ template <int NTH, int ELXTH, typename REAL_T>
162
+ static void launch_kernel(int BC, int Hi, int Wi, int K, int Ho, int Wo, int64_t nrows, int64_t *roff_d, int64_t *ker_d,
163
+ int64_t *row_d, int64_t *col_d, REAL_T *val_d, REAL_T *inp_d, REAL_T *out_d,
164
+ cudaStream_t stream)
165
+ {
166
+
167
+ static_assert(sizeof(REAL_T) == 2 || sizeof(REAL_T) == 4 || sizeof(REAL_T) == 8);
168
+
169
+ if constexpr (ELXTH <= ELXTH_MAX) {
170
+ if (NTH * ELXTH >= Wo) {
171
+ dim3 grid(nrows, BC);
172
+
173
+ const int pscale = Wi / Wo;
174
+ size_t shmem = sizeof(*out_d) * (Wi * 2 + pscale * (NTH * ELXTH - Wo));
175
+
176
+ disco_fwd_blk_k<NTH, ELXTH><<<grid, NTH, shmem, stream>>>(Hi, Wi, K, Ho, Wo, pscale, roff_d, ker_d, row_d,
177
+ col_d, val_d, inp_d, out_d);
178
+ } else {
179
+ launch_kernel<NTH, ELXTH + 1>(BC, Hi, Wi, K, Ho, Wo, nrows, roff_d, ker_d, row_d, col_d, val_d, inp_d,
180
+ out_d, stream);
181
+ }
182
+ }
183
+ return;
184
+ }
185
+
186
+ torch::Tensor disco_cuda_fwd(torch::Tensor inp, torch::Tensor roff_idx, torch::Tensor ker_idx, torch::Tensor row_idx,
187
+ torch::Tensor col_idx, torch::Tensor val, int64_t K, int64_t Ho, int64_t Wo)
188
+ {
189
+
190
+ // some sanity checks
191
+ CHECK_CUDA_INPUT_TENSOR(inp);
192
+ CHECK_CUDA_INPUT_TENSOR(roff_idx);
193
+ CHECK_CUDA_INPUT_TENSOR(ker_idx);
194
+ CHECK_CUDA_INPUT_TENSOR(row_idx);
195
+ CHECK_CUDA_INPUT_TENSOR(col_idx);
196
+ CHECK_CUDA_INPUT_TENSOR(val);
197
+
198
+ // extract some shapes
199
+ int64_t B = inp.size(0);
200
+ int64_t C = inp.size(1);
201
+ int64_t BC = B * C;
202
+ int64_t Hi = inp.size(2);
203
+ int64_t Wi = inp.size(3);
204
+ int64_t nrows = roff_idx.size(0) - 1;
205
+
206
+ // allocate output
207
+ int64_t out_dims[] = {B, C, K, Ho, Wo};
208
+ auto options = torch::TensorOptions().device(inp.device()).dtype(inp.dtype());
209
+ torch::Tensor out = torch::zeros(out_dims, options);
210
+
211
+ // get stream
212
+ auto stream = at::cuda::getCurrentCUDAStream().stream();
213
+
214
+ // assert
215
+ static_assert(0 == (ELXTH_MAX % 2));
216
+
217
+ // pick the correct launch config
218
+ if (Wo <= 64 * ELXTH_MAX) {
219
+ AT_DISPATCH_FLOATING_TYPES(inp.scalar_type(), "disco_forward_cuda", ([&] {
220
+ launch_kernel<64, 1, scalar_t>(
221
+ BC, Hi, Wi, K, Ho, Wo, nrows, roff_idx.data_ptr<int64_t>(),
222
+ ker_idx.data_ptr<int64_t>(), row_idx.data_ptr<int64_t>(),
223
+ col_idx.data_ptr<int64_t>(), val.data_ptr<scalar_t>(),
224
+ inp.data_ptr<scalar_t>(), out.data_ptr<scalar_t>(), stream);
225
+ }));
226
+ } else if (Wo <= 128 * ELXTH_MAX) {
227
+ AT_DISPATCH_FLOATING_TYPES(inp.scalar_type(), "disco_forward_cuda", ([&] {
228
+ launch_kernel<128, (ELXTH_MAX / 2) + 1, scalar_t>(
229
+ BC, Hi, Wi, K, Ho, Wo, nrows, roff_idx.data_ptr<int64_t>(),
230
+ ker_idx.data_ptr<int64_t>(), row_idx.data_ptr<int64_t>(),
231
+ col_idx.data_ptr<int64_t>(), val.data_ptr<scalar_t>(),
232
+ inp.data_ptr<scalar_t>(), out.data_ptr<scalar_t>(), stream);
233
+ }));
234
+ } else if (Wo <= 256 * ELXTH_MAX) {
235
+ AT_DISPATCH_FLOATING_TYPES(inp.scalar_type(), "disco_forward_cuda", ([&] {
236
+ launch_kernel<256, (ELXTH_MAX / 2) + 1, scalar_t>(
237
+ BC, Hi, Wi, K, Ho, Wo, nrows, roff_idx.data_ptr<int64_t>(),
238
+ ker_idx.data_ptr<int64_t>(), row_idx.data_ptr<int64_t>(),
239
+ col_idx.data_ptr<int64_t>(), val.data_ptr<scalar_t>(),
240
+ inp.data_ptr<scalar_t>(), out.data_ptr<scalar_t>(), stream);
241
+ }));
242
+ } else if (Wo <= 512 * ELXTH_MAX) {
243
+ AT_DISPATCH_FLOATING_TYPES(inp.scalar_type(), "disco_forward_cuda", ([&] {
244
+ launch_kernel<512, (ELXTH_MAX / 2) + 1, scalar_t>(
245
+ BC, Hi, Wi, K, Ho, Wo, nrows, roff_idx.data_ptr<int64_t>(),
246
+ ker_idx.data_ptr<int64_t>(), row_idx.data_ptr<int64_t>(),
247
+ col_idx.data_ptr<int64_t>(), val.data_ptr<scalar_t>(),
248
+ inp.data_ptr<scalar_t>(), out.data_ptr<scalar_t>(), stream);
249
+ }));
250
+ } else if (Wo <= 1024 * ELXTH_MAX) {
251
+ AT_DISPATCH_FLOATING_TYPES(inp.scalar_type(), "disco_forward_cuda", ([&] {
252
+ launch_kernel<1024, (ELXTH_MAX / 2) + 1, scalar_t>(
253
+ BC, Hi, Wi, K, Ho, Wo, nrows, roff_idx.data_ptr<int64_t>(),
254
+ ker_idx.data_ptr<int64_t>(), row_idx.data_ptr<int64_t>(),
255
+ col_idx.data_ptr<int64_t>(), val.data_ptr<scalar_t>(),
256
+ inp.data_ptr<scalar_t>(), out.data_ptr<scalar_t>(), stream);
257
+ }));
258
+ } else {
259
+ fprintf(stderr, "%s:%d: error, unsupported Wo value (%ld), max supported is %d\n", __FILE__, __LINE__, Wo,
260
+ 1024 * ELXTH_MAX);
261
+ exit(EXIT_FAILURE);
262
+ }
263
+
264
+ return out;
265
+ }
.deps/torch_harmonics/csrc/disco/disco_helpers.cpp ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // coding=utf-8
2
+ //
3
+ // SPDX-FileCopyrightText: Copyright (c) 2024 The torch-harmonics Authors. All rights reserved.
4
+ // SPDX-License-Identifier: BSD-3-Clause
5
+ //
6
+ // Redistribution and use in source and binary forms, with or without
7
+ // modification, are permitted provided that the following conditions are met:
8
+ //
9
+ // 1. Redistributions of source code must retain the above copyright notice, this
10
+ // list of conditions and the following disclaimer.
11
+ //
12
+ // 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ // this list of conditions and the following disclaimer in the documentation
14
+ // and/or other materials provided with the distribution.
15
+ //
16
+ // 3. Neither the name of the copyright holder nor the names of its
17
+ // contributors may be used to endorse or promote products derived from
18
+ // this software without specific prior written permission.
19
+ //
20
+ // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+
31
+ #include "disco.h"
32
+
33
+ template <typename REAL_T>
34
+ void preprocess_psi_kernel(int64_t nnz, int64_t K, int64_t Ho, int64_t *ker_h, int64_t *row_h, int64_t *col_h,
35
+ int64_t *roff_h, REAL_T *val_h, int64_t &nrows)
36
+ {
37
+
38
+ int64_t *Koff = new int64_t[K];
39
+ for (int i = 0; i < K; i++) { Koff[i] = 0; }
40
+
41
+ for (int64_t i = 0; i < nnz; i++) { Koff[ker_h[i]]++; }
42
+
43
+ int64_t prev = Koff[0];
44
+ Koff[0] = 0;
45
+ for (int i = 1; i < K; i++) {
46
+ int64_t save = Koff[i];
47
+ Koff[i] = prev + Koff[i - 1];
48
+ prev = save;
49
+ }
50
+
51
+ int64_t *ker_sort = new int64_t[nnz];
52
+ int64_t *row_sort = new int64_t[nnz];
53
+ int64_t *col_sort = new int64_t[nnz];
54
+ float *val_sort = new float[nnz];
55
+
56
+ for (int64_t i = 0; i < nnz; i++) {
57
+
58
+ const int64_t ker = ker_h[i];
59
+ const int64_t off = Koff[ker]++;
60
+
61
+ ker_sort[off] = ker;
62
+ row_sort[off] = row_h[i];
63
+ col_sort[off] = col_h[i];
64
+ val_sort[off] = val_h[i];
65
+ }
66
+ for (int64_t i = 0; i < nnz; i++) {
67
+ ker_h[i] = ker_sort[i];
68
+ row_h[i] = row_sort[i];
69
+ col_h[i] = col_sort[i];
70
+ val_h[i] = val_sort[i];
71
+ }
72
+
73
+ delete[] Koff;
74
+ delete[] ker_sort;
75
+ delete[] row_sort;
76
+ delete[] col_sort;
77
+ delete[] val_sort;
78
+
79
+ // compute rows offsets
80
+ nrows = 1;
81
+ roff_h[0] = 0;
82
+ for (int64_t i = 1; i < nnz; i++) {
83
+
84
+ if (row_h[i - 1] == row_h[i]) continue;
85
+ roff_h[nrows++] = i;
86
+
87
+ if (nrows > Ho * K) {
88
+ fprintf(stderr, "%s:%d: error, found more rows in the K COOs than Ho*K (%ld)\n", __FILE__, __LINE__,
89
+ int64_t(Ho) * K);
90
+ exit(EXIT_FAILURE);
91
+ }
92
+ }
93
+ roff_h[nrows] = nnz;
94
+
95
+ return;
96
+ }
97
+
98
+ torch::Tensor preprocess_psi(const int64_t K, const int64_t Ho, torch::Tensor ker_idx, torch::Tensor row_idx,
99
+ torch::Tensor col_idx, torch::Tensor val)
100
+ {
101
+
102
+ CHECK_INPUT_TENSOR(ker_idx);
103
+ CHECK_INPUT_TENSOR(row_idx);
104
+ CHECK_INPUT_TENSOR(col_idx);
105
+ CHECK_INPUT_TENSOR(val);
106
+
107
+ // get the input device and make sure all tensors are on the same device
108
+ auto device = ker_idx.device();
109
+ TORCH_INTERNAL_ASSERT(device.type() == row_idx.device().type() && (device.type() == col_idx.device().type()) && (device.type() == val.device().type()));
110
+
111
+ // move to cpu
112
+ ker_idx = ker_idx.to(torch::kCPU);
113
+ row_idx = row_idx.to(torch::kCPU);
114
+ col_idx = col_idx.to(torch::kCPU);
115
+ val = val.to(torch::kCPU);
116
+
117
+ int64_t nnz = val.size(0);
118
+ int64_t *ker_h = ker_idx.data_ptr<int64_t>();
119
+ int64_t *row_h = row_idx.data_ptr<int64_t>();
120
+ int64_t *col_h = col_idx.data_ptr<int64_t>();
121
+ int64_t *roff_h = new int64_t[Ho * K + 1];
122
+ int64_t nrows;
123
+
124
+ AT_DISPATCH_FLOATING_TYPES(val.scalar_type(), "preprocess_psi", ([&] {
125
+ preprocess_psi_kernel<scalar_t>(nnz, K, Ho, ker_h, row_h, col_h, roff_h,
126
+ val.data_ptr<scalar_t>(), nrows);
127
+ }));
128
+
129
+ // create output tensor
130
+ auto roff_idx = torch::empty({nrows + 1}, row_idx.options());
131
+ int64_t *roff_out_h = roff_idx.data_ptr<int64_t>();
132
+
133
+ for (int64_t i = 0; i < (nrows + 1); i++) { roff_out_h[i] = roff_h[i]; }
134
+ delete[] roff_h;
135
+
136
+ // move to original device
137
+ ker_idx = ker_idx.to(device);
138
+ row_idx = row_idx.to(device);
139
+ col_idx = col_idx.to(device);
140
+ val = val.to(device);
141
+ roff_idx = roff_idx.to(device);
142
+
143
+ return roff_idx;
144
+ }
145
+
146
+ PYBIND11_MODULE(TORCH_EXTENSION_NAME, m)
147
+ {
148
+ m.def("preprocess_psi", &preprocess_psi, "Sort psi matrix, required for using disco_cuda.");
149
+ }
.deps/torch_harmonics/csrc/disco/disco_interface.cu ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // coding=utf-8
2
+ //
3
+ // SPDX-FileCopyrightText: Copyright (c) 2024 The torch-harmonics Authors. All rights reserved.
4
+ // SPDX-License-Identifier: BSD-3-Clause
5
+ //
6
+ // Redistribution and use in source and binary forms, with or without
7
+ // modification, are permitted provided that the following conditions are met:
8
+ //
9
+ // 1. Redistributions of source code must retain the above copyright notice, this
10
+ // list of conditions and the following disclaimer.
11
+ //
12
+ // 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ // this list of conditions and the following disclaimer in the documentation
14
+ // and/or other materials provided with the distribution.
15
+ //
16
+ // 3. Neither the name of the copyright holder nor the names of its
17
+ // contributors may be used to endorse or promote products derived from
18
+ // this software without specific prior written permission.
19
+ //
20
+ // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+
31
+ #include "disco.h"
32
+ #include "disco_cuda.cuh"
33
+
34
+ PYBIND11_MODULE(TORCH_EXTENSION_NAME, m)
35
+ {
36
+ m.def("forward", &disco_cuda_fwd, "DISCO forward (CUDA)");
37
+ m.def("backward", &disco_cuda_bwd, "DISCO backward (CUDA)");
38
+ }
.deps/torch_harmonics/distributed/__init__.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2022 The torch-harmonics Authors. All rights reserved.
4
+ # SPDX-License-Identifier: BSD-3-Clause
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are met:
8
+ #
9
+ # 1. Redistributions of source code must retain the above copyright notice, this
10
+ # list of conditions and the following disclaimer.
11
+ #
12
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ # this list of conditions and the following disclaimer in the documentation
14
+ # and/or other materials provided with the distribution.
15
+ #
16
+ # 3. Neither the name of the copyright holder nor the names of its
17
+ # contributors may be used to endorse or promote products derived from
18
+ # this software without specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+ #
31
+
32
+ # we need this in order to enable distributed
33
+ from .utils import init, finalize, is_initialized, polar_group, azimuth_group
34
+ from .utils import polar_group_size, azimuth_group_size, polar_group_rank, azimuth_group_rank
35
+ from .primitives import compute_split_shapes, split_tensor_along_dim
36
+ from .primitives import (
37
+ distributed_transpose_azimuth,
38
+ distributed_transpose_polar,
39
+ reduce_from_polar_region,
40
+ reduce_from_azimuth_region,
41
+ scatter_to_polar_region,
42
+ gather_from_polar_region,
43
+ copy_to_polar_region,
44
+ copy_to_azimuth_region,
45
+ reduce_from_scatter_to_polar_region,
46
+ gather_from_copy_to_polar_region
47
+ )
48
+
49
+ # import the sht
50
+ from .distributed_sht import DistributedRealSHT, DistributedInverseRealSHT
51
+ from .distributed_sht import DistributedRealVectorSHT, DistributedInverseRealVectorSHT
52
+
53
+ # import DISCO
54
+ from .distributed_convolution import DistributedDiscreteContinuousConvS2
55
+ from .distributed_convolution import DistributedDiscreteContinuousConvTransposeS2
56
+
57
+ # import resampling
58
+ from .distributed_resample import DistributedResampleS2
.deps/torch_harmonics/distributed/distributed_convolution.py ADDED
@@ -0,0 +1,474 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2022 The torch-harmonics Authors. All rights reserved.
4
+ # SPDX-License-Identifier: BSD-3-Clause
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are met:
8
+ #
9
+ # 1. Redistributions of source code must retain the above copyright notice, this
10
+ # list of conditions and the following disclaimer.
11
+ #
12
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ # this list of conditions and the following disclaimer in the documentation
14
+ # and/or other materials provided with the distribution.
15
+ #
16
+ # 3. Neither the name of the copyright holder nor the names of its
17
+ # contributors may be used to endorse or promote products derived from
18
+ # this software without specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+ #
31
+
32
+ import abc
33
+ from typing import List, Tuple, Union, Optional
34
+ from itertools import accumulate
35
+ from warnings import warn
36
+
37
+ import math
38
+
39
+ import torch
40
+ import torch.nn as nn
41
+
42
+ from functools import partial
43
+
44
+ from torch_harmonics.quadrature import _precompute_grid, _precompute_latitudes, _precompute_longitudes
45
+ from torch_harmonics._disco_convolution import _get_psi, _disco_s2_contraction_torch, _disco_s2_transpose_contraction_torch
46
+ from torch_harmonics._disco_convolution import _disco_s2_contraction_cuda, _disco_s2_transpose_contraction_cuda
47
+ from torch_harmonics.filter_basis import get_filter_basis
48
+ from torch_harmonics.convolution import (
49
+ _precompute_convolution_tensor_s2,
50
+ DiscreteContinuousConv,
51
+ )
52
+
53
+
54
+ from torch_harmonics.distributed import polar_group_size, azimuth_group_size
55
+ from torch_harmonics.distributed import distributed_transpose_azimuth, distributed_transpose_polar
56
+ from torch_harmonics.distributed import reduce_from_polar_region, scatter_to_polar_region, gather_from_polar_region, copy_to_polar_region
57
+ from torch_harmonics.distributed import polar_group_rank, azimuth_group_rank
58
+ from torch_harmonics.distributed import compute_split_shapes, split_tensor_along_dim
59
+
60
+ # import custom C++/CUDA extensions if available
61
+ try:
62
+ from disco_helpers import preprocess_psi
63
+ import disco_cuda_extension
64
+
65
+ _cuda_extension_available = True
66
+ except ImportError as err:
67
+ disco_cuda_extension = None
68
+ _cuda_extension_available = False
69
+
70
+
71
+ def _split_distributed_convolution_tensor_s2(
72
+ idx: torch.Tensor,
73
+ vals: torch.Tensor,
74
+ in_shape: Tuple[int],
75
+ out_shape: Tuple[int],
76
+ ):
77
+ """
78
+ Splits a pre-computed convolution tensor along the latitude dimension for distributed processing.
79
+
80
+ This function takes a convolution tensor that was generated by the serial routine and filters
81
+ it to only include entries corresponding to the local latitude slice assigned to this process.
82
+ The filtering is done based on the polar group rank and the computed split shapes.
83
+
84
+ Parameters
85
+ ----------
86
+ idx: torch.Tensor
87
+ Indices of the pre-computed convolution tensor
88
+ vals: torch.Tensor
89
+ Values of the pre-computed convolution tensor
90
+ in_shape: Tuple[int]
91
+ Shape of the input tensor (nlat_in, nlon_in)
92
+ out_shape: Tuple[int]
93
+ Shape of the output tensor (nlat_out, nlon_out)
94
+
95
+ Returns
96
+ -------
97
+ idx: torch.Tensor
98
+ Filtered indices corresponding to the local latitude slice
99
+ vals: torch.Tensor
100
+ Filtered values corresponding to the local latitude slice
101
+ """
102
+
103
+ nlat_in, nlon_in = in_shape
104
+ nlat_out, nlon_out = out_shape
105
+
106
+ comm_size_polar = polar_group_size()
107
+ comm_rank_polar = polar_group_rank()
108
+ split_shapes = compute_split_shapes(nlat_in, num_chunks=comm_size_polar)
109
+ offsets = [0] + list(accumulate(split_shapes))
110
+ start_idx = offsets[comm_rank_polar]
111
+ end_idx = offsets[comm_rank_polar + 1]
112
+
113
+ # once normalization is done we can throw away the entries which correspond to input latitudes we do not care about
114
+ lats = idx[2] // nlon_in
115
+ lons = idx[2] % nlon_in
116
+ ilats = torch.argwhere((lats < end_idx) & (lats >= start_idx)).squeeze()
117
+ vals = vals[ilats]
118
+ # for the indices we need to recompute them to refer to local indices of the input tenor
119
+ idx = torch.stack([idx[0, ilats], idx[1, ilats], (lats[ilats] - start_idx) * nlon_in + lons[ilats]], dim=0)
120
+
121
+ # make results contiguous
122
+ idx = idx.contiguous()
123
+ vals = vals.to(dtype=torch.float32).contiguous()
124
+
125
+ return idx, vals
126
+
127
+
128
+ class DistributedDiscreteContinuousConvS2(DiscreteContinuousConv):
129
+ """
130
+ Distributed version of Discrete-continuous convolutions (DISCO) on the 2-Sphere as described in [1].
131
+ We assume the data can be splitted in polar and azimuthal directions.
132
+
133
+ Parameters
134
+ ----------
135
+ in_channels: int
136
+ Number of input channels
137
+ out_channels: int
138
+ Number of output channels
139
+ in_shape: Tuple[int]
140
+ Shape of the input tensor
141
+ out_shape: Tuple[int]
142
+ Shape of the output tensor
143
+ kernel_shape: Union[int, Tuple[int], Tuple[int, int]]
144
+ Shape of the kernel
145
+ basis_type: Optional[str]
146
+ Type of basis to use
147
+ basis_norm_mode: Optional[str]
148
+ Normalization mode for the filter basis
149
+ groups: Optional[int]
150
+ Number of groups
151
+ grid_in: Optional[str]
152
+ Grid type for the input tensor
153
+ grid_out: Optional[str]
154
+ Grid type for the output tensor
155
+ bias: Optional[bool]
156
+ Whether to use bias
157
+ theta_cutoff: Optional[float]
158
+ Theta cutoff for the filter basis
159
+
160
+ Returns
161
+ -------
162
+ out: torch.Tensor
163
+ Output tensor
164
+
165
+ References
166
+ ----------
167
+ [1] Ocampo, Price, McEwen, Scalable and equivariant spherical CNNs by discrete-continuous (DISCO) convolutions, ICLR (2023), arXiv:2209.13603
168
+ """
169
+
170
+ def __init__(
171
+ self,
172
+ in_channels: int,
173
+ out_channels: int,
174
+ in_shape: Tuple[int],
175
+ out_shape: Tuple[int],
176
+ kernel_shape: Union[int, Tuple[int], Tuple[int, int]],
177
+ basis_type: Optional[str] = "piecewise linear",
178
+ basis_norm_mode: Optional[str] = "mean",
179
+ groups: Optional[int] = 1,
180
+ grid_in: Optional[str] = "equiangular",
181
+ grid_out: Optional[str] = "equiangular",
182
+ bias: Optional[bool] = True,
183
+ theta_cutoff: Optional[float] = None,
184
+ ):
185
+ super().__init__(in_channels, out_channels, kernel_shape, basis_type, groups, bias)
186
+
187
+ self.nlat_in, self.nlon_in = in_shape
188
+ self.nlat_out, self.nlon_out = out_shape
189
+
190
+ # get the comms grid:
191
+ self.comm_size_polar = polar_group_size()
192
+ self.comm_rank_polar = polar_group_rank()
193
+ self.comm_size_azimuth = azimuth_group_size()
194
+ self.comm_rank_azimuth = azimuth_group_rank()
195
+
196
+ # we need those shapes:
197
+ self.lat_in_shapes = compute_split_shapes(self.nlat_in, self.comm_size_polar)
198
+ self.lon_in_shapes = compute_split_shapes(self.nlon_in, self.comm_size_azimuth)
199
+ self.lat_out_shapes = compute_split_shapes(self.nlat_out, self.comm_size_polar)
200
+ self.lon_out_shapes = compute_split_shapes(self.nlon_out, self.comm_size_azimuth)
201
+
202
+ # compute theta cutoff based on the bandlimit of the input field
203
+ if theta_cutoff is None:
204
+ theta_cutoff = torch.pi / float(self.nlat_out - 1)
205
+
206
+ if theta_cutoff <= 0.0:
207
+ raise ValueError("Error, theta_cutoff has to be positive.")
208
+
209
+ # Note that the psi matrix is of shape nlat_out x nlat_in * nlon_in. Since the contraction in nlon direction is a convolution,
210
+ # we will keep local to all nodes and split the computation up along nlat. We further split the input dim because this reduces the number
211
+ # of atomic reduction calls inside the actual kernel
212
+
213
+ # set local shapes according to distributed mode:
214
+ self.nlat_in_local = self.lat_in_shapes[self.comm_rank_polar]
215
+ self.nlat_out_local = self.nlat_out
216
+
217
+ # compute global convolution tensor
218
+ idx, vals, _ = _precompute_convolution_tensor_s2(
219
+ in_shape,
220
+ out_shape,
221
+ self.filter_basis,
222
+ grid_in=grid_in,
223
+ grid_out=grid_out,
224
+ theta_cutoff=theta_cutoff,
225
+ transpose_normalization=False,
226
+ basis_norm_mode=basis_norm_mode,
227
+ merge_quadrature=True,
228
+ )
229
+
230
+ # split the convolution tensor along latitude
231
+ idx, vals = _split_distributed_convolution_tensor_s2(idx, vals, in_shape, out_shape)
232
+
233
+ # sort the values
234
+ ker_idx = idx[0, ...].contiguous()
235
+ row_idx = idx[1, ...].contiguous()
236
+ col_idx = idx[2, ...].contiguous()
237
+ vals = vals.contiguous()
238
+
239
+ if _cuda_extension_available:
240
+ # preprocessed data-structure for GPU kernel
241
+ roff_idx = preprocess_psi(self.kernel_size, self.nlat_out_local, ker_idx, row_idx, col_idx, vals).contiguous()
242
+ self.register_buffer("psi_roff_idx", roff_idx, persistent=False)
243
+
244
+ # save all datastructures
245
+ self.register_buffer("psi_ker_idx", ker_idx, persistent=False)
246
+ self.register_buffer("psi_row_idx", row_idx, persistent=False)
247
+ self.register_buffer("psi_col_idx", col_idx, persistent=False)
248
+ self.register_buffer("psi_vals", vals, persistent=False)
249
+
250
+ # store psi jic:
251
+ self.psi = _get_psi(self.kernel_size, self.psi_idx, self.psi_vals, self.nlat_in, self.nlon_in, self.nlat_out, self.nlon_out, self.nlat_in_local, self.nlat_out_local)
252
+
253
+ def extra_repr(self):
254
+ return f"in_shape={(self.nlat_in, self.nlon_in)}, out_shape={(self.nlat_out, self.nlon_out)}, in_chans={self.groupsize * self.groups}, out_chans={self.weight.shape[0]}, filter_basis={self.filter_basis}, kernel_shape={self.kernel_shape}, groups={self.groups}"
255
+
256
+ @property
257
+ def psi_idx(self):
258
+ return torch.stack([self.psi_ker_idx, self.psi_row_idx, self.psi_col_idx], dim=0).contiguous()
259
+
260
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
261
+
262
+ # store number of channels
263
+ num_chans = x.shape[1]
264
+
265
+ # h and w is split. First we make w local by transposing into channel dim
266
+ if self.comm_size_azimuth > 1:
267
+ x = distributed_transpose_azimuth.apply(x, (1, -1), self.lon_in_shapes)
268
+
269
+ if x.is_cuda and _cuda_extension_available:
270
+ x = _disco_s2_contraction_cuda(
271
+ x, self.psi_roff_idx, self.psi_ker_idx, self.psi_row_idx, self.psi_col_idx, self.psi_vals, self.kernel_size, self.nlat_out_local, self.nlon_out
272
+ )
273
+ else:
274
+ if x.is_cuda:
275
+ warn("couldn't find CUDA extension, falling back to slow PyTorch implementation")
276
+
277
+ x = _disco_s2_contraction_torch(x, self.psi.to(x.device), self.nlon_out)
278
+
279
+ # perform reduce scatter in polar region
280
+ x = reduce_from_polar_region(x)
281
+ x = scatter_to_polar_region(x, -2)
282
+
283
+ # now we can transpose back the result, so that lon is split and channels are local
284
+ if self.comm_size_azimuth > 1:
285
+ chan_shapes = compute_split_shapes(num_chans, self.comm_size_azimuth)
286
+ x = distributed_transpose_azimuth.apply(x, (-1, 1), chan_shapes)
287
+
288
+ # extract shape
289
+ B, C, K, H, W = x.shape
290
+ x = x.reshape(B, self.groups, self.groupsize, K, H, W)
291
+
292
+ # do weight multiplication
293
+ out = torch.einsum("bgckxy,gock->bgoxy", x, self.weight.reshape(self.groups, -1, self.weight.shape[1], self.weight.shape[2])).contiguous()
294
+ out = out.reshape(out.shape[0], -1, H, W)
295
+
296
+ if self.bias is not None:
297
+ out = out + self.bias.reshape(1, -1, 1, 1)
298
+
299
+ return out
300
+
301
+
302
+ class DistributedDiscreteContinuousConvTransposeS2(DiscreteContinuousConv):
303
+ """
304
+ Discrete-continuous transpose convolutions (DISCO) on the 2-Sphere as described in [1].
305
+
306
+ Parameters
307
+ ----------
308
+ in_channels: int
309
+ Number of input channels
310
+ out_channels: int
311
+ Number of output channels
312
+ in_shape: Tuple[int]
313
+ Shape of the input tensor
314
+ out_shape: Tuple[int]
315
+ Shape of the output tensor
316
+ kernel_shape: Union[int, Tuple[int], Tuple[int, int]]
317
+ Shape of the kernel
318
+ basis_type: Optional[str]
319
+ Type of basis to use
320
+ basis_norm_mode: Optional[str]
321
+ Normalization mode for the filter basis
322
+ groups: Optional[int]
323
+ Number of groups
324
+ grid_in: Optional[str]
325
+ Grid type for the input tensor
326
+ grid_out: Optional[str]
327
+ Grid type for the output tensor
328
+ bias: Optional[bool]
329
+ Whether to use bias
330
+ theta_cutoff: Optional[float]
331
+ Theta cutoff for the filter basis
332
+
333
+ Returns
334
+ -------
335
+ out: torch.Tensor
336
+ Output tensor
337
+
338
+ References
339
+ ----------
340
+ [1] Ocampo, Price, McEwen, Scalable and equivariant spherical CNNs by discrete-continuous (DISCO) convolutions, ICLR (2023), arXiv:2209.13603
341
+
342
+ We assume the data can be splitted in polar and azimuthal directions.
343
+ """
344
+
345
+ def __init__(
346
+ self,
347
+ in_channels: int,
348
+ out_channels: int,
349
+ in_shape: Tuple[int],
350
+ out_shape: Tuple[int],
351
+ kernel_shape: Union[int, Tuple[int], Tuple[int, int]],
352
+ basis_type: Optional[str] = "piecewise linear",
353
+ basis_norm_mode: Optional[str] = "mean",
354
+ groups: Optional[int] = 1,
355
+ grid_in: Optional[str] = "equiangular",
356
+ grid_out: Optional[str] = "equiangular",
357
+ bias: Optional[bool] = True,
358
+ theta_cutoff: Optional[float] = None,
359
+ ):
360
+ super().__init__(in_channels, out_channels, kernel_shape, basis_type, groups, bias)
361
+
362
+ self.nlat_in, self.nlon_in = in_shape
363
+ self.nlat_out, self.nlon_out = out_shape
364
+
365
+ # get the comms grid:
366
+ self.comm_size_polar = polar_group_size()
367
+ self.comm_rank_polar = polar_group_rank()
368
+ self.comm_size_azimuth = azimuth_group_size()
369
+ self.comm_rank_azimuth = azimuth_group_rank()
370
+
371
+ # we need those shapes:
372
+ self.lat_in_shapes = compute_split_shapes(self.nlat_in, self.comm_size_polar)
373
+ self.lon_in_shapes = compute_split_shapes(self.nlon_in, self.comm_size_azimuth)
374
+ self.lat_out_shapes = compute_split_shapes(self.nlat_out, self.comm_size_polar)
375
+ self.lon_out_shapes = compute_split_shapes(self.nlon_out, self.comm_size_azimuth)
376
+
377
+ # bandlimit
378
+ if theta_cutoff is None:
379
+ theta_cutoff = torch.pi / float(self.nlat_in - 1)
380
+
381
+ if theta_cutoff <= 0.0:
382
+ raise ValueError("Error, theta_cutoff has to be positive.")
383
+
384
+ # Note that the psi matrix is of shape nlat_out x nlat_in * nlon_in. Since the contraction in nlon direction is a convolution,
385
+ # we will keep local to all nodes and split the computation up along nlat. We further split the input dim because this reduces the number
386
+ # of atomic reduction calls inside the actual kernel
387
+
388
+ # set local shapes according to distributed mode:
389
+ self.nlat_in_local = self.nlat_in
390
+ self.nlat_out_local = self.lat_out_shapes[self.comm_rank_polar]
391
+
392
+ # compute global convolution tensor
393
+ # switch in_shape and out_shape since we want transpose conv
394
+ # distributed mode here is swapped because of the transpose
395
+ idx, vals, _ = _precompute_convolution_tensor_s2(
396
+ out_shape,
397
+ in_shape,
398
+ self.filter_basis,
399
+ grid_in=grid_out,
400
+ grid_out=grid_in,
401
+ theta_cutoff=theta_cutoff,
402
+ transpose_normalization=True,
403
+ basis_norm_mode=basis_norm_mode,
404
+ merge_quadrature=True,
405
+ )
406
+
407
+ # split the convolution tensor along latitude, again, we need to swap the meaning
408
+ # of in_shape and out_shape
409
+ idx, vals = _split_distributed_convolution_tensor_s2(idx, vals, out_shape, in_shape)
410
+
411
+ # sort the values
412
+ ker_idx = idx[0, ...].contiguous()
413
+ row_idx = idx[1, ...].contiguous()
414
+ col_idx = idx[2, ...].contiguous()
415
+ vals = vals.contiguous()
416
+
417
+ if _cuda_extension_available:
418
+ # preprocessed data-structure for GPU kernel
419
+ roff_idx = preprocess_psi(self.kernel_size, self.nlat_in_local, ker_idx, row_idx, col_idx, vals).contiguous()
420
+ self.register_buffer("psi_roff_idx", roff_idx, persistent=False)
421
+
422
+ # save all datastructures
423
+ self.register_buffer("psi_ker_idx", ker_idx, persistent=False)
424
+ self.register_buffer("psi_row_idx", row_idx, persistent=False)
425
+ self.register_buffer("psi_col_idx", col_idx, persistent=False)
426
+ self.register_buffer("psi_vals", vals, persistent=False)
427
+
428
+ # store psi as COO
429
+ self.psi_st = _get_psi(self.kernel_size, self.psi_idx, self.psi_vals, self.nlat_in, self.nlon_in, self.nlat_out, self.nlon_out, self.nlat_in_local, self.nlat_out_local, semi_transposed=True)
430
+
431
+ def extra_repr(self):
432
+ return f"in_shape={(self.nlat_in, self.nlon_in)}, out_shape={(self.nlat_out, self.nlon_out)}, in_chans={self.groupsize * self.groups}, out_chans={self.weight.shape[0]}, filter_basis={self.filter_basis}, kernel_shape={self.kernel_shape}, groups={self.groups}"
433
+
434
+ @property
435
+ def psi_idx(self):
436
+ return torch.stack([self.psi_ker_idx, self.psi_row_idx, self.psi_col_idx], dim=0).contiguous()
437
+
438
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
439
+
440
+ # extract shape
441
+ B, C, H, W = x.shape
442
+ x = x.reshape(B, self.groups, self.groupsize, H, W)
443
+
444
+ # do weight multiplication
445
+ x = torch.einsum("bgcxy,gock->bgokxy", x, self.weight.reshape(self.groups, -1, self.weight.shape[1], self.weight.shape[2])).contiguous()
446
+ x = x.reshape(B, -1, x.shape[-3], H, W)
447
+ num_chans = x.shape[1]
448
+
449
+ # transpose such that lon is local, channels are split
450
+ if self.comm_size_azimuth > 1:
451
+ x = distributed_transpose_azimuth.apply(x, (1, -1), self.lon_in_shapes)
452
+
453
+ # gather input tensor and set up backward reduction hooks
454
+ x = gather_from_polar_region(x, -2, self.lat_in_shapes)
455
+ x = copy_to_polar_region(x)
456
+
457
+ if x.is_cuda and _cuda_extension_available:
458
+ out = _disco_s2_transpose_contraction_cuda(
459
+ x, self.psi_roff_idx, self.psi_ker_idx, self.psi_row_idx, self.psi_col_idx, self.psi_vals, self.kernel_size, self.nlat_out_local, self.nlon_out
460
+ )
461
+ else:
462
+ if x.is_cuda:
463
+ warn("couldn't find CUDA extension, falling back to slow PyTorch implementation")
464
+ out = _disco_s2_transpose_contraction_torch(x, self.psi_st.to(x.device), self.nlon_out)
465
+
466
+ # now we can transpose back the result, so that lon is split and channels are local
467
+ if self.comm_size_azimuth > 1:
468
+ chan_shapes = compute_split_shapes(num_chans, self.comm_size_azimuth)
469
+ out = distributed_transpose_azimuth.apply(out, (-1, 1), chan_shapes)
470
+
471
+ if self.bias is not None:
472
+ out = out + self.bias.reshape(1, -1, 1, 1)
473
+
474
+ return out
.deps/torch_harmonics/distributed/distributed_resample.py ADDED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2022 The torch-harmonics Authors. All rights reserved.
4
+ # SPDX-License-Identifier: BSD-3-Clause
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are met:
8
+ #
9
+ # 1. Redistributions of source code must retain the above copyright notice, this
10
+ # list of conditions and the following disclaimer.
11
+ #
12
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ # this list of conditions and the following disclaimer in the documentation
14
+ # and/or other materials provided with the distribution.
15
+ #
16
+ # 3. Neither the name of the copyright holder nor the names of its
17
+ # contributors may be used to endorse or promote products derived from
18
+ # this software without specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+ #
31
+
32
+ from typing import List, Tuple, Union, Optional
33
+ import math
34
+
35
+ import torch
36
+ import torch.nn as nn
37
+
38
+ from torch_harmonics.quadrature import _precompute_latitudes, _precompute_longitudes
39
+ from torch_harmonics.distributed import polar_group_size, azimuth_group_size, distributed_transpose_azimuth, distributed_transpose_polar
40
+ from torch_harmonics.distributed import reduce_from_azimuth_region, copy_to_azimuth_region
41
+ from torch_harmonics.distributed import polar_group_rank, azimuth_group_rank
42
+ from torch_harmonics.distributed import compute_split_shapes
43
+
44
+
45
+ class DistributedResampleS2(nn.Module):
46
+ """
47
+ Distributed resampling module for spherical data on the 2-sphere.
48
+
49
+ This module performs distributed resampling of spherical data across multiple processes,
50
+ supporting both upscaling and downscaling operations. The data is distributed across
51
+ polar and azimuthal directions, and the module handles the necessary communication
52
+ and interpolation operations.
53
+
54
+ Parameters
55
+ -----------
56
+ nlat_in : int
57
+ Number of input latitude points
58
+ nlon_in : int
59
+ Number of input longitude points
60
+ nlat_out : int
61
+ Number of output latitude points
62
+ nlon_out : int
63
+ Number of output longitude points
64
+ grid_in : str, optional
65
+ Input grid type, by default "equiangular"
66
+ grid_out : str, optional
67
+ Output grid type, by default "equiangular"
68
+ mode : str, optional
69
+ Interpolation mode ("bilinear" or "bilinear-spherical"), by default "bilinear"
70
+ """
71
+
72
+ def __init__(
73
+ self,
74
+ nlat_in: int,
75
+ nlon_in: int,
76
+ nlat_out: int,
77
+ nlon_out: int,
78
+ grid_in: Optional[str] = "equiangular",
79
+ grid_out: Optional[str] = "equiangular",
80
+ mode: Optional[str] = "bilinear",
81
+ ):
82
+
83
+ super().__init__()
84
+
85
+ # currently only bilinear is supported
86
+ if mode in ["bilinear", "bilinear-spherical"]:
87
+ self.mode = mode
88
+ else:
89
+ raise NotImplementedError(f"unknown interpolation mode {mode}")
90
+
91
+ self.nlat_in, self.nlon_in = nlat_in, nlon_in
92
+ self.nlat_out, self.nlon_out = nlat_out, nlon_out
93
+
94
+ self.grid_in = grid_in
95
+ self.grid_out = grid_out
96
+
97
+ # get the comms grid:
98
+ self.comm_size_polar = polar_group_size()
99
+ self.comm_rank_polar = polar_group_rank()
100
+ self.comm_size_azimuth = azimuth_group_size()
101
+ self.comm_rank_azimuth = azimuth_group_rank()
102
+
103
+ # compute splits: is this correct even when expanding the poles?
104
+ self.lat_in_shapes = compute_split_shapes(self.nlat_in, self.comm_size_polar)
105
+ self.lon_in_shapes = compute_split_shapes(self.nlon_in, self.comm_size_azimuth)
106
+ self.lat_out_shapes = compute_split_shapes(self.nlat_out, self.comm_size_polar)
107
+ self.lon_out_shapes = compute_split_shapes(self.nlon_out, self.comm_size_azimuth)
108
+
109
+ # for upscaling the latitudes we will use interpolation
110
+ self.lats_in, _ = _precompute_latitudes(nlat_in, grid=grid_in)
111
+ self.lons_in = _precompute_longitudes(nlon_in)
112
+ self.lats_out, _ = _precompute_latitudes(nlat_out, grid=grid_out)
113
+ self.lons_out = _precompute_longitudes(nlon_out)
114
+
115
+ # in the case where some points lie outside of the range spanned by lats_in,
116
+ # we need to expand the solution to the poles before interpolating
117
+ self.expand_poles = (self.lats_out > self.lats_in[-1]).any() or (self.lats_out < self.lats_in[0]).any()
118
+ if self.expand_poles:
119
+ self.lats_in = torch.cat([torch.tensor([0.], dtype=torch.float64),
120
+ self.lats_in,
121
+ torch.tensor([math.pi], dtype=torch.float64)]).contiguous()
122
+
123
+ # prepare the interpolation by computing indices to the left and right of each output latitude
124
+ lat_idx = torch.searchsorted(self.lats_in, self.lats_out, side="right") - 1
125
+ # make sure that we properly treat the last point if they coincide with the pole
126
+ lat_idx = torch.where(self.lats_out == self.lats_in[-1], lat_idx - 1, lat_idx)
127
+
128
+ # lat_idx = np.where(self.lats_out > self.lats_in[-1], lat_idx - 1, lat_idx)
129
+ # lat_idx = np.where(self.lats_out < self.lats_in[0], 0, lat_idx)
130
+
131
+ # compute the interpolation weights along the latitude
132
+ lat_weights = ((self.lats_out - self.lats_in[lat_idx]) / torch.diff(self.lats_in)[lat_idx]).to(torch.float32)
133
+ lat_weights = lat_weights.unsqueeze(-1)
134
+
135
+ # register buffers
136
+ self.register_buffer("lat_idx", lat_idx, persistent=False)
137
+ self.register_buffer("lat_weights", lat_weights, persistent=False)
138
+
139
+ # get left and right indices but this time make sure periodicity in the longitude is handled
140
+ lon_idx_left = torch.searchsorted(self.lons_in, self.lons_out, side="right") - 1
141
+ lon_idx_right = torch.where(self.lons_out >= self.lons_in[-1], torch.zeros_like(lon_idx_left), lon_idx_left + 1)
142
+
143
+ # get the difference
144
+ diff = self.lons_in[lon_idx_right] - self.lons_in[lon_idx_left]
145
+ diff = torch.where(diff < 0.0, diff + 2 * math.pi, diff)
146
+ lon_weights = ((self.lons_out - self.lons_in[lon_idx_left]) / diff).to(torch.float32)
147
+
148
+ # register buffers
149
+ self.register_buffer("lon_idx_left", lon_idx_left, persistent=False)
150
+ self.register_buffer("lon_idx_right", lon_idx_right, persistent=False)
151
+ self.register_buffer("lon_weights", lon_weights, persistent=False)
152
+
153
+ self.skip_resampling = (nlon_in == nlon_out) and (nlat_in == nlat_out) and (grid_in == grid_out)
154
+
155
+ def extra_repr(self):
156
+ return f"in_shape={(self.nlat_in, self.nlon_in)}, out_shape={(self.nlat_out, self.nlon_out)}"
157
+
158
+ def _upscale_longitudes(self, x: torch.Tensor):
159
+ """Upscale the longitude dimension using interpolation."""
160
+ # do the interpolation
161
+ lwgt = self.lon_weights.to(x.dtype)
162
+ if self.mode == "bilinear":
163
+ x = torch.lerp(x[..., self.lon_idx_left], x[..., self.lon_idx_right], lwgt)
164
+ else:
165
+ omega = x[..., self.lon_idx_right] - x[..., self.lon_idx_left]
166
+ somega = torch.sin(omega)
167
+ start_prefac = torch.where(somega > 1e-4, torch.sin((1.0 - lwgt) * omega) / somega, (1.0 - lwgt))
168
+ end_prefac = torch.where(somega > 1e-4, torch.sin(lwgt * omega) / somega, lwgt)
169
+ x = start_prefac * x[..., self.lon_idx_left] + end_prefac * x[..., self.lon_idx_right]
170
+
171
+ return x
172
+
173
+ def _expand_poles(self, x: torch.Tensor):
174
+ """Expand the data to include pole values for interpolation."""
175
+ x_north = x[..., 0, :].sum(dim=-1, keepdims=True)
176
+ x_south = x[..., -1, :].sum(dim=-1, keepdims=True)
177
+ x_count = torch.tensor([x.shape[-1]], dtype=torch.long, device=x.device, requires_grad=False)
178
+
179
+ if self.comm_size_azimuth > 1:
180
+ x_north = reduce_from_azimuth_region(x_north.contiguous())
181
+ x_south = reduce_from_azimuth_region(x_south.contiguous())
182
+ x_count = reduce_from_azimuth_region(x_count)
183
+ x_north = x_north / x_count
184
+ x_south = x_south / x_count
185
+
186
+ if self.comm_size_azimuth > 1:
187
+ x_north = copy_to_azimuth_region(x_north)
188
+ x_south = copy_to_azimuth_region(x_south)
189
+
190
+ x = nn.functional.pad(x, pad=[0, 0, 1, 1], mode='constant')
191
+ x[..., 0, :] = x_north[...]
192
+ x[..., -1, :] = x_south[...]
193
+
194
+ return x
195
+
196
+ def _upscale_latitudes(self, x: torch.Tensor):
197
+ """Upscale the latitude dimension using interpolation."""
198
+ # do the interpolation
199
+ lwgt = self.lat_weights.to(x.dtype)
200
+ if self.mode == "bilinear":
201
+ x = torch.lerp(x[..., self.lat_idx, :], x[..., self.lat_idx + 1, :], lwgt)
202
+ else:
203
+ omega = x[..., self.lat_idx + 1, :] - x[..., self.lat_idx, :]
204
+ somega = torch.sin(omega)
205
+ start_prefac = torch.where(somega > 1e-4, torch.sin((1.0 - lwgt) * omega) / somega, (1.0 - lwgt))
206
+ end_prefac = torch.where(somega > 1e-4, torch.sin(lwgt * omega) / somega, lwgt)
207
+ x = start_prefac * x[..., self.lat_idx, :] + end_prefac * x[..., self.lat_idx + 1, :]
208
+
209
+ return x
210
+
211
+ def forward(self, x: torch.Tensor):
212
+
213
+ if self.skip_resampling:
214
+ return x
215
+
216
+ # transpose data so that h is local, and channels are split
217
+ num_chans = x.shape[-3]
218
+
219
+ # h and w is split. First we make w local by transposing into channel dim
220
+ if self.comm_size_polar > 1:
221
+ channels_shapes = compute_split_shapes(num_chans, self.comm_size_polar)
222
+ x = distributed_transpose_polar.apply(x, (-3, -2), self.lat_in_shapes)
223
+
224
+ # expand poles if requested
225
+ if self.expand_poles:
226
+ x = self._expand_poles(x)
227
+
228
+ # upscaling
229
+ x = self._upscale_latitudes(x)
230
+
231
+ # now, transpose back
232
+ if self.comm_size_polar > 1:
233
+ x = distributed_transpose_polar.apply(x, (-2, -3), channels_shapes)
234
+
235
+ # now, transpose in w:
236
+ if self.comm_size_azimuth > 1:
237
+ channels_shapes = compute_split_shapes(num_chans, self.comm_size_azimuth)
238
+ x = distributed_transpose_azimuth.apply(x, (-3, -1), self.lon_in_shapes)
239
+
240
+ # upscale
241
+ x = self._upscale_longitudes(x)
242
+
243
+ # transpose back
244
+ if self.comm_size_azimuth > 1:
245
+ x = distributed_transpose_azimuth.apply(x, (-1, -3), channels_shapes)
246
+
247
+ return x
.deps/torch_harmonics/distributed/distributed_sht.py ADDED
@@ -0,0 +1,612 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2022 The torch-harmonics Authors. All rights reserved.
4
+ # SPDX-License-Identifier: BSD-3-Clause
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are met:
8
+ #
9
+ # 1. Redistributions of source code must retain the above copyright notice, this
10
+ # list of conditions and the following disclaimer.
11
+ #
12
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ # this list of conditions and the following disclaimer in the documentation
14
+ # and/or other materials provided with the distribution.
15
+ #
16
+ # 3. Neither the name of the copyright holder nor the names of its
17
+ # contributors may be used to endorse or promote products derived from
18
+ # this software without specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+ #
31
+
32
+ import os
33
+ import torch
34
+ import torch.nn as nn
35
+ import torch.fft
36
+ import torch.nn.functional as F
37
+
38
+ from torch_harmonics.quadrature import legendre_gauss_weights, lobatto_weights, clenshaw_curtiss_weights
39
+ from torch_harmonics.legendre import _precompute_legpoly, _precompute_dlegpoly
40
+ from torch_harmonics.distributed import polar_group_size, azimuth_group_size, distributed_transpose_azimuth, distributed_transpose_polar
41
+ from torch_harmonics.distributed import polar_group_rank, azimuth_group_rank
42
+ from torch_harmonics.distributed import compute_split_shapes, split_tensor_along_dim
43
+
44
+
45
+ class DistributedRealSHT(nn.Module):
46
+ """
47
+ Defines a module for computing the forward (real-valued) SHT.
48
+ Precomputes Legendre Gauss nodes, weights and associated Legendre polynomials on these nodes.
49
+ The SHT is applied to the last two dimensions of the input
50
+
51
+ Parameters
52
+ ----------
53
+ nlat: int
54
+ Number of latitude points
55
+ nlon: int
56
+ Number of longitude points
57
+ lmax: int
58
+ Maximum spherical harmonic degree
59
+ mmax: int
60
+ Maximum spherical harmonic order
61
+ grid: str
62
+ Grid type ("equiangular", "legendre-gauss", "lobatto", "equidistant"), by default "equiangular"
63
+ norm: str
64
+ Normalization type ("ortho", "schmidt", "unnorm"), by default "ortho"
65
+ csphase: bool
66
+ Whether to apply the Condon-Shortley phase factor, by default True
67
+
68
+ Returns
69
+ -------
70
+ x: torch.Tensor
71
+ Tensor of shape (..., lmax, mmax)
72
+
73
+ References
74
+ ----------
75
+ [1] Schaeffer, N. Efficient spherical harmonic transforms aimed at pseudospectral numerical simulations, G3: Geochemistry, Geophysics, Geosystems.
76
+ [2] Wang, B., Wang, L., Xie, Z.; Accurate calculation of spherical and vector spherical harmonic expansions via spectral element grids; Adv Comput Math.
77
+ """
78
+
79
+ def __init__(self, nlat, nlon, lmax=None, mmax=None, grid="equiangular", norm="ortho", csphase=True):
80
+
81
+ super().__init__()
82
+
83
+ self.nlat = nlat
84
+ self.nlon = nlon
85
+ self.grid = grid
86
+ self.norm = norm
87
+ self.csphase = csphase
88
+
89
+ # TODO: include assertions regarding the dimensions
90
+
91
+ # compute quadrature points
92
+ if self.grid == "legendre-gauss":
93
+ cost, weights = legendre_gauss_weights(nlat, -1, 1)
94
+ self.lmax = lmax or self.nlat
95
+ elif self.grid == "lobatto":
96
+ cost, weights = lobatto_weights(nlat, -1, 1)
97
+ self.lmax = lmax or self.nlat-1
98
+ elif self.grid == "equiangular":
99
+ cost, weights = clenshaw_curtiss_weights(nlat, -1, 1)
100
+ # cost, w = fejer2_weights(nlat, -1, 1)
101
+ self.lmax = lmax or self.nlat
102
+ else:
103
+ raise(ValueError("Unknown quadrature mode"))
104
+
105
+ # get the comms grid:
106
+ self.comm_size_polar = polar_group_size()
107
+ self.comm_rank_polar = polar_group_rank()
108
+ self.comm_size_azimuth = azimuth_group_size()
109
+ self.comm_rank_azimuth = azimuth_group_rank()
110
+
111
+ # apply cosine transform and flip them
112
+ tq = torch.flip(torch.arccos(cost), dims=(0,))
113
+
114
+ # determine the dimensions
115
+ self.mmax = mmax or self.nlon // 2 + 1
116
+
117
+ # compute splits
118
+ self.lat_shapes = compute_split_shapes(self.nlat, self.comm_size_polar)
119
+ self.lon_shapes = compute_split_shapes(self.nlon, self.comm_size_azimuth)
120
+ self.l_shapes = compute_split_shapes(self.lmax, self.comm_size_polar)
121
+ self.m_shapes = compute_split_shapes(self.mmax, self.comm_size_azimuth)
122
+
123
+ # combine quadrature weights with the legendre weights
124
+ pct = _precompute_legpoly(self.mmax, self.lmax, tq, norm=self.norm, csphase=self.csphase)
125
+ weights = torch.einsum('mlk,k->mlk', pct, weights)
126
+
127
+ # split weights
128
+ weights = split_tensor_along_dim(weights, dim=0, num_chunks=self.comm_size_azimuth)[self.comm_rank_azimuth].contiguous()
129
+
130
+ # remember quadrature weights
131
+ self.register_buffer('weights', weights, persistent=False)
132
+
133
+ def extra_repr(self):
134
+ return f'nlat={self.nlat}, nlon={self.nlon},\n lmax={self.lmax}, mmax={self.mmax},\n grid={self.grid}, csphase={self.csphase}'
135
+
136
+ def forward(self, x: torch.Tensor):
137
+
138
+ if x.dim() < 3:
139
+ raise ValueError(f"Expected tensor with at least 3 dimensions but got {x.dim()} instead")
140
+
141
+ # we need to ensure that we can split the channels evenly
142
+ num_chans = x.shape[-3]
143
+
144
+ # h and w is split. First we make w local by transposing into channel dim
145
+ if self.comm_size_azimuth > 1:
146
+ x = distributed_transpose_azimuth.apply(x, (-3, -1), self.lon_shapes)
147
+
148
+ # apply real fft in the longitudinal direction: make sure to truncate to nlon
149
+ x = 2.0 * torch.pi * torch.fft.rfft(x, n=self.nlon, dim=-1, norm="forward")
150
+
151
+ # truncate
152
+ x = x[..., :self.mmax]
153
+
154
+ # transpose: after this, m is split and c is local
155
+ if self.comm_size_azimuth > 1:
156
+ chan_shapes = compute_split_shapes(num_chans, self.comm_size_azimuth)
157
+ x = distributed_transpose_azimuth.apply(x, (-1, -3), chan_shapes)
158
+
159
+ # transpose: after this, c is split and h is local
160
+ if self.comm_size_polar > 1:
161
+ x = distributed_transpose_polar.apply(x, (-3, -2), self.lat_shapes)
162
+
163
+ # do the Legendre-Gauss quadrature
164
+ x = torch.view_as_real(x)
165
+
166
+ # contraction
167
+ xs = torch.einsum('...kmr,mlk->...lmr', x, self.weights.to(x.dtype)).contiguous()
168
+
169
+ # cast to complex
170
+ x = torch.view_as_complex(xs)
171
+
172
+ # transpose: after this, l is split and c is local
173
+ if self.comm_size_polar > 1:
174
+ chan_shapes = compute_split_shapes(num_chans, self.comm_size_polar)
175
+ x = distributed_transpose_polar.apply(x, (-2, -3), chan_shapes)
176
+
177
+ return x
178
+
179
+
180
+ class DistributedInverseRealSHT(nn.Module):
181
+ """
182
+ Defines a module for computing the inverse (real-valued) SHT.
183
+ Precomputes Legendre Gauss nodes, weights and associated Legendre polynomials on these nodes.
184
+
185
+ Parameters
186
+ ----------
187
+ nlat: int
188
+ Number of latitude points
189
+ nlon: int
190
+ Number of longitude points
191
+ lmax: int
192
+ Maximum spherical harmonic degree
193
+ mmax: int
194
+ Maximum spherical harmonic order
195
+ grid: str
196
+ Grid type ("equiangular", "legendre-gauss", "lobatto", "equidistant"), by default "equiangular"
197
+ norm: str
198
+ Normalization type ("ortho", "schmidt", "unnorm"), by default "ortho"
199
+ csphase: bool
200
+ Whether to apply the Condon-Shortley phase factor, by default True
201
+
202
+ Returns
203
+ -------
204
+ x: torch.Tensor
205
+ Tensor of shape (..., lmax, mmax)
206
+
207
+ References
208
+ ----------
209
+ [1] Schaeffer, N. Efficient spherical harmonic transforms aimed at pseudospectral numerical simulations, G3: Geochemistry, Geophysics, Geosystems.
210
+ [2] Wang, B., Wang, L., Xie, Z.; Accurate calculation of spherical and vector spherical harmonic expansions via spectral element grids; Adv Comput Math.
211
+ """
212
+
213
+ def __init__(self, nlat, nlon, lmax=None, mmax=None, grid="equiangular", norm="ortho", csphase=True):
214
+
215
+ super().__init__()
216
+
217
+ self.nlat = nlat
218
+ self.nlon = nlon
219
+ self.grid = grid
220
+ self.norm = norm
221
+ self.csphase = csphase
222
+
223
+ # compute quadrature points
224
+ if self.grid == "legendre-gauss":
225
+ cost, _ = legendre_gauss_weights(nlat, -1, 1)
226
+ self.lmax = lmax or self.nlat
227
+ elif self.grid == "lobatto":
228
+ cost, _ = lobatto_weights(nlat, -1, 1)
229
+ self.lmax = lmax or self.nlat-1
230
+ elif self.grid == "equiangular":
231
+ cost, _ = clenshaw_curtiss_weights(nlat, -1, 1)
232
+ self.lmax = lmax or self.nlat
233
+ else:
234
+ raise(ValueError("Unknown quadrature mode"))
235
+
236
+ # get the comms grid:
237
+ self.comm_size_polar = polar_group_size()
238
+ self.comm_rank_polar = polar_group_rank()
239
+ self.comm_size_azimuth = azimuth_group_size()
240
+ self.comm_rank_azimuth = azimuth_group_rank()
241
+
242
+ # apply cosine transform and flip them
243
+ t = torch.flip(torch.arccos(cost), dims=(0,))
244
+
245
+ # determine the dimensions
246
+ self.mmax = mmax or self.nlon // 2 + 1
247
+
248
+ # compute splits
249
+ self.lat_shapes = compute_split_shapes(self.nlat, self.comm_size_polar)
250
+ self.lon_shapes = compute_split_shapes(self.nlon, self.comm_size_azimuth)
251
+ self.l_shapes = compute_split_shapes(self.lmax, self.comm_size_polar)
252
+ self.m_shapes = compute_split_shapes(self.mmax, self.comm_size_azimuth)
253
+
254
+ # compute legende polynomials
255
+ pct = _precompute_legpoly(self.mmax, self.lmax, t, norm=self.norm, inverse=True, csphase=self.csphase)
256
+
257
+ # split in m
258
+ pct = split_tensor_along_dim(pct, dim=0, num_chunks=self.comm_size_azimuth)[self.comm_rank_azimuth].contiguous()
259
+
260
+ # register
261
+ self.register_buffer('pct', pct, persistent=False)
262
+
263
+ def extra_repr(self):
264
+ return f'nlat={self.nlat}, nlon={self.nlon},\n lmax={self.lmax}, mmax={self.mmax},\n grid={self.grid}, csphase={self.csphase}'
265
+
266
+ def forward(self, x: torch.Tensor):
267
+
268
+ if x.dim() < 3:
269
+ raise ValueError(f"Expected tensor with at least 3 dimensions but got {x.dim()} instead")
270
+
271
+ # we need to ensure that we can split the channels evenly
272
+ num_chans = x.shape[-3]
273
+
274
+ # transpose: after that, channels are split, l is local:
275
+ if self.comm_size_polar > 1:
276
+ x = distributed_transpose_polar.apply(x, (-3, -2), self.l_shapes)
277
+
278
+ # Evaluate associated Legendre functions on the output nodes
279
+ x = torch.view_as_real(x)
280
+
281
+ # einsum
282
+ xs = torch.einsum('...lmr, mlk->...kmr', x, self.pct.to(x.dtype)).contiguous()
283
+
284
+ # inverse FFT
285
+ x = torch.view_as_complex(xs)
286
+
287
+ if self.comm_size_polar > 1:
288
+ chan_shapes = compute_split_shapes(num_chans, self.comm_size_polar)
289
+ x = distributed_transpose_polar.apply(x, (-2, -3), chan_shapes)
290
+
291
+ # transpose: after this, channels are split and m is local
292
+ if self.comm_size_azimuth > 1:
293
+ x = distributed_transpose_azimuth.apply(x, (-3, -1), self.m_shapes)
294
+
295
+ # set DCT and nyquist frequencies to 0:
296
+ x[..., 0].imag = 0.0
297
+ if (self.nlon % 2 == 0) and (self.nlon // 2 < x.shape[-1]):
298
+ x[..., self.nlon // 2].imag = 0.0
299
+
300
+ # apply the inverse (real) FFT
301
+ x = torch.fft.irfft(x, n=self.nlon, dim=-1, norm="forward")
302
+
303
+ # transpose: after this, m is split and channels are local
304
+ if self.comm_size_azimuth > 1:
305
+ chan_shapes = compute_split_shapes(num_chans, self.comm_size_azimuth)
306
+ x = distributed_transpose_azimuth.apply(x, (-1, -3), chan_shapes)
307
+
308
+ return x
309
+
310
+
311
+ class DistributedRealVectorSHT(nn.Module):
312
+ """
313
+ Defines a module for computing the forward (real) vector SHT.
314
+ Precomputes Legendre Gauss nodes, weights and associated Legendre polynomials on these nodes.
315
+ The SHT is applied to the last three dimensions of the input.
316
+
317
+ Parameters
318
+ ----------
319
+ nlat: int
320
+ Number of latitude points
321
+ nlon: int
322
+ Number of longitude points
323
+ lmax: int
324
+ Maximum spherical harmonic degree
325
+ mmax: int
326
+ Maximum spherical harmonic order
327
+ grid: str
328
+ Grid type ("equiangular", "legendre-gauss", "lobatto", "equidistant"), by default "equiangular"
329
+ norm: str
330
+ Normalization type ("ortho", "schmidt", "unnorm"), by default "ortho"
331
+ csphase: bool
332
+ Whether to apply the Condon-Shortley phase factor, by default True
333
+
334
+ Returns
335
+ -------
336
+ x: torch.Tensor
337
+ Tensor of shape (..., lmax, mmax)
338
+
339
+ References
340
+ ----------
341
+ [1] Schaeffer, N. Efficient spherical harmonic transforms aimed at pseudospectral numerical simulations, G3: Geochemistry, Geophysics, Geosystems.
342
+ [2] Wang, B., Wang, L., Xie, Z.; Accurate calculation of spherical and vector spherical harmonic expansions via spectral element grids; Adv Comput Math.
343
+ """
344
+
345
+ def __init__(self, nlat, nlon, lmax=None, mmax=None, grid="equiangular", norm="ortho", csphase=True):
346
+
347
+ super().__init__()
348
+
349
+ self.nlat = nlat
350
+ self.nlon = nlon
351
+ self.grid = grid
352
+ self.norm = norm
353
+ self.csphase = csphase
354
+
355
+ # compute quadrature points
356
+ if self.grid == "legendre-gauss":
357
+ cost, weights = legendre_gauss_weights(nlat, -1, 1)
358
+ self.lmax = lmax or self.nlat
359
+ elif self.grid == "lobatto":
360
+ cost, weights = lobatto_weights(nlat, -1, 1)
361
+ self.lmax = lmax or self.nlat-1
362
+ elif self.grid == "equiangular":
363
+ cost, weights = clenshaw_curtiss_weights(nlat, -1, 1)
364
+ # cost, w = fejer2_weights(nlat, -1, 1)
365
+ self.lmax = lmax or self.nlat
366
+ else:
367
+ raise(ValueError("Unknown quadrature mode"))
368
+
369
+ # get the comms grid:
370
+ self.comm_size_polar = polar_group_size()
371
+ self.comm_rank_polar = polar_group_rank()
372
+ self.comm_size_azimuth = azimuth_group_size()
373
+ self.comm_rank_azimuth = azimuth_group_rank()
374
+
375
+ # apply cosine transform and flip them
376
+ tq = torch.flip(torch.arccos(cost), dims=(0,))
377
+
378
+ # determine the dimensions
379
+ self.mmax = mmax or self.nlon // 2 + 1
380
+
381
+ # compute splits
382
+ self.lat_shapes = compute_split_shapes(self.nlat, self.comm_size_polar)
383
+ self.lon_shapes = compute_split_shapes(self.nlon, self.comm_size_azimuth)
384
+ self.l_shapes = compute_split_shapes(self.lmax, self.comm_size_polar)
385
+ self.m_shapes = compute_split_shapes(self.mmax, self.comm_size_azimuth)
386
+
387
+ # compute weights
388
+ dpct = _precompute_dlegpoly(self.mmax, self.lmax, tq, norm=self.norm, csphase=self.csphase)
389
+
390
+ # combine integration weights, normalization factor in to one:
391
+ l = torch.arange(0, self.lmax)
392
+ norm_factor = 1. / l / (l+1)
393
+ norm_factor[0] = 1.
394
+ weights = torch.einsum('dmlk,k,l->dmlk', dpct, weights, norm_factor)
395
+ # since the second component is imaginary, we need to take complex conjugation into account
396
+ weights[1] = -1 * weights[1]
397
+
398
+ # we need to split in m, pad before:
399
+ weights = split_tensor_along_dim(weights, dim=1, num_chunks=self.comm_size_azimuth)[self.comm_rank_azimuth].contiguous()
400
+
401
+ # remember quadrature weights
402
+ self.register_buffer('weights', weights, persistent=False)
403
+
404
+
405
+ def extra_repr(self):
406
+ return f'nlat={self.nlat}, nlon={self.nlon},\n lmax={self.lmax}, mmax={self.mmax},\n grid={self.grid}, csphase={self.csphase}'
407
+
408
+ def forward(self, x: torch.Tensor):
409
+
410
+ if x.dim() < 4:
411
+ raise ValueError(f"Expected tensor with at least 4 dimensions but got {x.dim()} instead")
412
+
413
+ # we need to ensure that we can split the channels evenly
414
+ num_chans = x.shape[-4]
415
+
416
+ # h and w is split. First we make w local by transposing into channel dim
417
+ if self.comm_size_azimuth > 1:
418
+ x = distributed_transpose_azimuth.apply(x, (-4, -1), self.lon_shapes)
419
+
420
+ # apply real fft in the longitudinal direction: make sure to truncate to nlon
421
+ x = 2.0 * torch.pi * torch.fft.rfft(x, n=self.nlon, dim=-1, norm="forward")
422
+
423
+ # truncate
424
+ x = x[..., :self.mmax]
425
+
426
+ # transpose: after this, m is split and c is local
427
+ if self.comm_size_azimuth > 1:
428
+ chan_shapes = compute_split_shapes(num_chans, self.comm_size_azimuth)
429
+ x = distributed_transpose_azimuth.apply(x, (-1, -4), chan_shapes)
430
+
431
+ # transpose: after this, c is split and h is local
432
+ if self.comm_size_polar > 1:
433
+ x = distributed_transpose_polar.apply(x, (-4, -2), self.lat_shapes)
434
+
435
+ # do the Legendre-Gauss quadrature
436
+ x = torch.view_as_real(x)
437
+
438
+ # create output array
439
+ xs = torch.zeros_like(x, dtype=x.dtype, device=x.device)
440
+
441
+ # contraction - spheroidal component
442
+ # real component
443
+ xs[..., 0, :, :, 0] = torch.einsum('...km,mlk->...lm', x[..., 0, :, :, 0], self.weights[0].to(xs.dtype)) \
444
+ - torch.einsum('...km,mlk->...lm', x[..., 1, :, :, 1], self.weights[1].to(xs.dtype))
445
+ # imag component
446
+ xs[..., 0, :, :, 1] = torch.einsum('...km,mlk->...lm', x[..., 0, :, :, 1], self.weights[0].to(xs.dtype)) \
447
+ + torch.einsum('...km,mlk->...lm', x[..., 1, :, :, 0], self.weights[1].to(xs.dtype))
448
+
449
+ # contraction - toroidal component
450
+ # real component
451
+ xs[..., 1, :, :, 0] = - torch.einsum('...km,mlk->...lm', x[..., 0, :, :, 1], self.weights[1].to(xs.dtype)) \
452
+ - torch.einsum('...km,mlk->...lm', x[..., 1, :, :, 0], self.weights[0].to(xs.dtype))
453
+ # imag component
454
+ xs[..., 1, :, :, 1] = torch.einsum('...km,mlk->...lm', x[..., 0, :, :, 0], self.weights[1].to(xs.dtype)) \
455
+ - torch.einsum('...km,mlk->...lm', x[..., 1, :, :, 1], self.weights[0].to(xs.dtype))
456
+
457
+ # pad if required
458
+ x = torch.view_as_complex(xs)
459
+
460
+ # transpose: after this, l is split and c is local
461
+ if self.comm_size_polar > 1:
462
+ chan_shapes = compute_split_shapes(num_chans, self.comm_size_polar)
463
+ x = distributed_transpose_polar.apply(x, (-2, -4), chan_shapes)
464
+
465
+ return x
466
+
467
+
468
+ class DistributedInverseRealVectorSHT(nn.Module):
469
+ """
470
+ Defines a module for computing the inverse (real-valued) vector SHT.
471
+ Precomputes Legendre Gauss nodes, weights and associated Legendre polynomials on these nodes.
472
+
473
+ Parameters
474
+ ----------
475
+ nlat: int
476
+ Number of latitude points
477
+ nlon: int
478
+ Number of longitude points
479
+ lmax: int
480
+ Maximum spherical harmonic degree
481
+ mmax: int
482
+ Maximum spherical harmonic order
483
+ grid: str
484
+ Grid type ("equiangular", "legendre-gauss", "lobatto", "equidistant"), by default "equiangular"
485
+ norm: str
486
+ Normalization type ("ortho", "schmidt", "unnorm"), by default "ortho"
487
+ csphase: bool
488
+ Whether to apply the Condon-Shortley phase factor, by default True
489
+
490
+ Returns
491
+ -------
492
+ x: torch.Tensor
493
+ Tensor of shape (..., lmax, mmax)
494
+
495
+ References
496
+ ----------
497
+ [1] Schaeffer, N. Efficient spherical harmonic transforms aimed at pseudospectral numerical simulations, G3: Geochemistry, Geophysics, Geosystems.
498
+ [2] Wang, B., Wang, L., Xie, Z.; Accurate calculation of spherical and vector spherical harmonic expansions via spectral element grids; Adv Comput Math.
499
+ """
500
+ def __init__(self, nlat, nlon, lmax=None, mmax=None, grid="equiangular", norm="ortho", csphase=True):
501
+
502
+ super().__init__()
503
+
504
+ self.nlat = nlat
505
+ self.nlon = nlon
506
+ self.grid = grid
507
+ self.norm = norm
508
+ self.csphase = csphase
509
+
510
+ # compute quadrature points
511
+ if self.grid == "legendre-gauss":
512
+ cost, _ = legendre_gauss_weights(nlat, -1, 1)
513
+ self.lmax = lmax or self.nlat
514
+ elif self.grid == "lobatto":
515
+ cost, _ = lobatto_weights(nlat, -1, 1)
516
+ self.lmax = lmax or self.nlat-1
517
+ elif self.grid == "equiangular":
518
+ cost, _ = clenshaw_curtiss_weights(nlat, -1, 1)
519
+ self.lmax = lmax or self.nlat
520
+ else:
521
+ raise(ValueError("Unknown quadrature mode"))
522
+
523
+ self.comm_size_polar = polar_group_size()
524
+ self.comm_rank_polar = polar_group_rank()
525
+ self.comm_size_azimuth = azimuth_group_size()
526
+ self.comm_rank_azimuth = azimuth_group_rank()
527
+
528
+ # apply cosine transform and flip them
529
+ t = torch.flip(torch.arccos(cost), dims=(0,))
530
+
531
+ # determine the dimensions
532
+ self.mmax = mmax or self.nlon // 2 + 1
533
+
534
+ # compute splits
535
+ self.lat_shapes = compute_split_shapes(self.nlat, self.comm_size_polar)
536
+ self.lon_shapes = compute_split_shapes(self.nlon, self.comm_size_azimuth)
537
+ self.l_shapes = compute_split_shapes(self.lmax, self.comm_size_polar)
538
+ self.m_shapes = compute_split_shapes(self.mmax, self.comm_size_azimuth)
539
+
540
+ # compute legende polynomials
541
+ dpct = _precompute_dlegpoly(self.mmax, self.lmax, t, norm=self.norm, inverse=True, csphase=self.csphase)
542
+
543
+ # split in m
544
+ dpct = split_tensor_along_dim(dpct, dim=1, num_chunks=self.comm_size_azimuth)[self.comm_rank_azimuth].contiguous()
545
+
546
+ # register buffer
547
+ self.register_buffer('dpct', dpct, persistent=False)
548
+
549
+ def extra_repr(self):
550
+ return f'nlat={self.nlat}, nlon={self.nlon},\n lmax={self.lmax}, mmax={self.mmax},\n grid={self.grid}, csphase={self.csphase}'
551
+
552
+ def forward(self, x: torch.Tensor):
553
+
554
+ if x.dim() < 4:
555
+ raise ValueError(f"Expected tensor with at least 4 dimensions but got {x.dim()} instead")
556
+
557
+ # store num channels
558
+ num_chans = x.shape[-4]
559
+
560
+ # transpose: after that, channels are split, l is local:
561
+ if self.comm_size_polar > 1:
562
+ x = distributed_transpose_polar.apply(x, (-4, -2), self.l_shapes)
563
+
564
+ # Evaluate associated Legendre functions on the output nodes
565
+ x = torch.view_as_real(x)
566
+
567
+ # contraction - spheroidal component
568
+ # real component
569
+ srl = torch.einsum('...lm,mlk->...km', x[..., 0, :, :, 0], self.dpct[0].to(x.dtype)) \
570
+ - torch.einsum('...lm,mlk->...km', x[..., 1, :, :, 1], self.dpct[1].to(x.dtype))
571
+ # imag component
572
+ sim = torch.einsum('...lm,mlk->...km', x[..., 0, :, :, 1], self.dpct[0].to(x.dtype)) \
573
+ + torch.einsum('...lm,mlk->...km', x[..., 1, :, :, 0], self.dpct[1].to(x.dtype))
574
+
575
+ # contraction - toroidal component
576
+ # real component
577
+ trl = - torch.einsum('...lm,mlk->...km', x[..., 0, :, :, 1], self.dpct[1].to(x.dtype)) \
578
+ - torch.einsum('...lm,mlk->...km', x[..., 1, :, :, 0], self.dpct[0].to(x.dtype))
579
+ # imag component
580
+ tim = torch.einsum('...lm,mlk->...km', x[..., 0, :, :, 0], self.dpct[1].to(x.dtype)) \
581
+ - torch.einsum('...lm,mlk->...km', x[..., 1, :, :, 1], self.dpct[0].to(x.dtype))
582
+
583
+ # reassemble
584
+ s = torch.stack((srl, sim), -1)
585
+ t = torch.stack((trl, tim), -1)
586
+ xs = torch.stack((s, t), -4)
587
+
588
+ # convert to complex
589
+ x = torch.view_as_complex(xs)
590
+
591
+ if self.comm_size_polar > 1:
592
+ chan_shapes = compute_split_shapes(num_chans, self.comm_size_polar)
593
+ x = distributed_transpose_polar.apply(x, (-2, -4), chan_shapes)
594
+
595
+ # transpose: after this, channels are split and m is local
596
+ if self.comm_size_azimuth > 1:
597
+ x = distributed_transpose_azimuth.apply(x, (-4, -1), self.m_shapes)
598
+
599
+ # set DCT and nyquist frequencies to zero
600
+ x[..., 0].imag = 0.0
601
+ if (self.nlon % 2 == 0) and (self.nlon // 2 < x.shape[-1]):
602
+ x[..., self.nlon // 2].imag = 0.0
603
+
604
+ # apply the inverse (real) FFT
605
+ x = torch.fft.irfft(x, n=self.nlon, dim=-1, norm="forward")
606
+
607
+ # transpose: after this, m is split and channels are local
608
+ if self.comm_size_azimuth > 1:
609
+ chan_shapes = compute_split_shapes(num_chans, self.comm_size_azimuth)
610
+ x = distributed_transpose_azimuth.apply(x, (-1, -4), chan_shapes)
611
+
612
+ return x
.deps/torch_harmonics/distributed/primitives.py ADDED
@@ -0,0 +1,470 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2022 The torch-harmonics Authors. All rights reserved.
4
+ # SPDX-License-Identifier: BSD-3-Clause
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are met:
8
+ #
9
+ # 1. Redistributions of source code must retain the above copyright notice, this
10
+ # list of conditions and the following disclaimer.
11
+ #
12
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ # this list of conditions and the following disclaimer in the documentation
14
+ # and/or other materials provided with the distribution.
15
+ #
16
+ # 3. Neither the name of the copyright holder nor the names of its
17
+ # contributors may be used to endorse or promote products derived from
18
+ # this software without specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+ #
31
+ from typing import List
32
+
33
+ import torch
34
+ import torch.distributed as dist
35
+ from torch.amp import custom_fwd, custom_bwd
36
+
37
+ from .utils import polar_group, azimuth_group, polar_group_size
38
+ from .utils import is_initialized, is_distributed_polar, is_distributed_azimuth
39
+
40
+ # helper routine to compute uneven splitting in balanced way:
41
+ def compute_split_shapes(size: int, num_chunks: int) -> List[int]:
42
+ """Compute the split shapes for a given size and number of chunks."""
43
+
44
+ # treat trivial case first
45
+ if num_chunks == 1:
46
+ return [size]
47
+
48
+ # first, check if we can split using div-up to balance the load:
49
+ chunk_size = (size + num_chunks - 1) // num_chunks
50
+ last_chunk_size = max(0, size - chunk_size * (num_chunks - 1))
51
+ if last_chunk_size == 0:
52
+ # in this case, the last shard would be empty, split with floor instead:
53
+ chunk_size = size // num_chunks
54
+ last_chunk_size = size - chunk_size * (num_chunks-1)
55
+
56
+ # generate sections list
57
+ sections = [chunk_size for _ in range(num_chunks - 1)] + [last_chunk_size]
58
+
59
+ return sections
60
+
61
+
62
+ def split_tensor_along_dim(tensor, dim, num_chunks):
63
+ """Split a tensor along a given dimension into a given number of chunks."""
64
+
65
+ assert dim < tensor.dim(), f"Error, tensor dimension is {tensor.dim()} which cannot be split along {dim}"
66
+ assert (tensor.shape[dim] >= num_chunks), f"Error, cannot split dim {dim} of size {tensor.shape[dim]} into \
67
+ {num_chunks} chunks. Empty slices are currently not supported."
68
+
69
+ # get split
70
+ sections = compute_split_shapes(tensor.shape[dim], num_chunks)
71
+ tensor_list = torch.split(tensor, sections, dim=dim)
72
+
73
+ return tensor_list
74
+
75
+
76
+ def _transpose(tensor, dim0, dim1, dim1_split_sizes, group=None, async_op=False):
77
+
78
+ # get comm params
79
+ comm_size = dist.get_world_size(group=group)
80
+ comm_rank = dist.get_rank(group=group)
81
+
82
+ # split and local transposition
83
+ tsplit = split_tensor_along_dim(tensor, num_chunks=comm_size, dim=dim0)
84
+ x_send = [y.contiguous() for y in tsplit]
85
+ x_send_shapes = [x.shape for x in x_send]
86
+ x_recv = []
87
+ x_shape = list(x_send_shapes[comm_rank])
88
+ for dim1_len in dim1_split_sizes:
89
+ x_shape[dim1] = dim1_len
90
+ x_recv.append(torch.empty(x_shape, dtype=tensor.dtype, device=tensor.device))
91
+
92
+ # global transposition
93
+ req = dist.all_to_all(x_recv, x_send, group=group, async_op=async_op)
94
+
95
+ # get dim0 split sizes
96
+ dim0_split_sizes = [x[dim0] for x in x_send_shapes]
97
+
98
+ return x_recv, dim0_split_sizes, req
99
+
100
+
101
+ class distributed_transpose_azimuth(torch.autograd.Function):
102
+
103
+ @staticmethod
104
+ @custom_fwd(device_type="cuda")
105
+ def forward(ctx, x, dims, dim1_split_sizes):
106
+
107
+ # WAR for a potential contig check torch bug for channels last contig tensors
108
+ xlist, dim0_split_sizes, _ = _transpose(x, dims[0], dims[1], dim1_split_sizes, group=azimuth_group())
109
+ x = torch.cat(xlist, dim=dims[1])
110
+ ctx.dims = dims
111
+ ctx.dim0_split_sizes = dim0_split_sizes
112
+
113
+ return x
114
+
115
+ @staticmethod
116
+ @custom_bwd(device_type="cuda")
117
+ def backward(ctx, go):
118
+ dims = ctx.dims
119
+ dim0_split_sizes = ctx.dim0_split_sizes
120
+ # WAR for a potential contig check torch bug for channels last contig tensors
121
+ gilist, _, _ = _transpose(go, dims[1], dims[0], dim0_split_sizes, group=azimuth_group())
122
+ gi = torch.cat(gilist, dim=dims[0])
123
+
124
+ return gi, None, None
125
+
126
+
127
+ class distributed_transpose_polar(torch.autograd.Function):
128
+
129
+ @staticmethod
130
+ @custom_fwd(device_type="cuda")
131
+ def forward(ctx, x, dim, dim1_split_sizes):
132
+
133
+ # WAR for a potential contig check torch bug for channels last contig tensors
134
+ xlist, dim0_split_sizes, _ = _transpose(x, dim[0], dim[1], dim1_split_sizes, group=polar_group())
135
+ x = torch.cat(xlist, dim=dim[1])
136
+ ctx.dim = dim
137
+ ctx.dim0_split_sizes = dim0_split_sizes
138
+ return x
139
+
140
+ @staticmethod
141
+ @custom_bwd(device_type="cuda")
142
+ def backward(ctx, go):
143
+
144
+ dim = ctx.dim
145
+ dim0_split_sizes = ctx.dim0_split_sizes
146
+ # WAR for a potential contig check torch bug for channels last contig tensors
147
+ gilist, _, _ = _transpose(go, dim[1], dim[0], dim0_split_sizes, group=polar_group())
148
+ gi = torch.cat(gilist, dim=dim[0])
149
+ return gi, None, None
150
+
151
+
152
+ # we need those additional primitives for distributed matrix multiplications
153
+ def _reduce(input_, use_fp32=True, group=None):
154
+
155
+ # Bypass the function if we are using only 1 GPU.
156
+ if dist.get_world_size(group=group) == 1:
157
+ return input_
158
+
159
+ # All-reduce.
160
+ if use_fp32:
161
+ dtype = input_.dtype
162
+ inputf_ = input_.float()
163
+ inputf_ = inputf_.contiguous()
164
+ dist.all_reduce(inputf_, group=group)
165
+ input_ = inputf_.to(dtype)
166
+ else:
167
+ input_ = input_.contiguous()
168
+ dist.all_reduce(input_, group=group)
169
+
170
+ return input_
171
+
172
+
173
+ def _split(input_, dim_, group=None):
174
+ # Bypass the function if we are using only 1 GPU.
175
+ comm_size = dist.get_world_size(group=group)
176
+ if comm_size == 1:
177
+ return input_
178
+
179
+ # Split along last dimension.
180
+ input_list = split_tensor_along_dim(input_, dim_, comm_size)
181
+
182
+ # Note: torch.split does not create contiguous tensors by default.
183
+ rank = dist.get_rank(group=group)
184
+ output = input_list[rank]
185
+
186
+ return output
187
+
188
+
189
+ def _gather(input_, dim_, shapes_, group=None):
190
+
191
+ comm_size = dist.get_world_size(group=group)
192
+
193
+ if (shapes_ is not None) and (len(shapes_) != comm_size):
194
+ raise ValueError()
195
+ if dim_ >= input_.dim():
196
+ raise ValueError()
197
+
198
+ if comm_size == 1:
199
+ return input_
200
+
201
+ # make contiguous:
202
+ input_ = input_.contiguous()
203
+ input_shape = list(input_.shape)
204
+
205
+ if shapes_ is not None:
206
+ input_list = []
207
+ for src in range(comm_size):
208
+ input_shape[dim_] = shapes_[src]
209
+ input_list.append(torch.empty(input_shape, dtype=input_.dtype, device=input_.device))
210
+ else:
211
+ # assume equal shape on all ranks
212
+ input_list = [torch.empty_like(input_) for _ in range(comm_size)]
213
+
214
+ dist.all_gather(input_list, input_, group=group)
215
+
216
+ output = torch.cat(input_list, dim=dim_)
217
+
218
+ return output
219
+
220
+
221
+ def _reduce_scatter(input_, dim_, use_fp32=True, group=None):
222
+
223
+ # Bypass the function if we are using only 1 GPU.
224
+ if dist.get_world_size(group=group) == 1:
225
+ return input_
226
+
227
+ # make input contiguous
228
+ comm_size = dist.get_world_size(group=group)
229
+ comm_rank = dist.get_rank(group=group)
230
+ input_list = split_tensor_along_dim(input_, dim_, comm_size)
231
+
232
+ dtype = input_.dtype
233
+ if (use_fp32 and (dtype != torch.float32)):
234
+ input_list = [x.to(torch.float32) for x in input_list]
235
+
236
+ input_list = [x.contiguous() for x in input_list]
237
+
238
+ # perform reduce_scatter
239
+ output = torch.empty_like(input_list[comm_rank])
240
+ dist.reduce_scatter(output, input_list, group=group)
241
+
242
+ # convert dtype if necessary
243
+ if use_fp32:
244
+ output = output.to(dtype=dtype)
245
+
246
+ return output
247
+
248
+
249
+ class _CopyToPolarRegion(torch.autograd.Function):
250
+
251
+ @staticmethod
252
+ def symbolic(graph, input_):
253
+ return input_
254
+
255
+ @staticmethod
256
+ @custom_fwd(device_type="cuda")
257
+ def forward(ctx, input_):
258
+
259
+ return input_
260
+
261
+ @staticmethod
262
+ @custom_bwd(device_type="cuda")
263
+ def backward(ctx, grad_output):
264
+
265
+ if is_distributed_polar():
266
+ return _reduce(grad_output, group=polar_group())
267
+ else:
268
+ return grad_output, None
269
+
270
+
271
+ class _CopyToAzimuthRegion(torch.autograd.Function):
272
+
273
+ @staticmethod
274
+ def symbolic(graph, input_):
275
+ return input_
276
+
277
+ @staticmethod
278
+ @custom_fwd(device_type="cuda")
279
+ def forward(ctx, input_):
280
+
281
+ return input_
282
+
283
+ @staticmethod
284
+ @custom_bwd(device_type="cuda")
285
+ def backward(ctx, grad_output):
286
+
287
+ if is_distributed_azimuth():
288
+ return _reduce(grad_output, group=azimuth_group())
289
+ else:
290
+ return grad_output, None
291
+
292
+
293
+ class _ScatterToPolarRegion(torch.autograd.Function):
294
+
295
+ @staticmethod
296
+ def symbolic(graph, input_, dim_):
297
+ return _split(input_, dim_, group=polar_group())
298
+
299
+ @staticmethod
300
+ @custom_fwd(device_type="cuda")
301
+ def forward(ctx, input_, dim_):
302
+ if is_distributed_polar():
303
+ ctx.dim = dim_
304
+ ctx.split_shapes = compute_split_shapes(
305
+ input_.shape[dim_], polar_group_size()
306
+ )
307
+ return _split(input_, dim_, group=polar_group())
308
+ else:
309
+ return input_
310
+
311
+ @staticmethod
312
+ @custom_bwd(device_type="cuda")
313
+ def backward(ctx, grad_output):
314
+ if is_distributed_polar():
315
+ return _gather(grad_output, ctx.dim, ctx.split_shapes, polar_group()), None
316
+ else:
317
+ return grad_output, None
318
+
319
+
320
+ class _GatherFromPolarRegion(torch.autograd.Function):
321
+
322
+ @staticmethod
323
+ def symbolic(graph, input_, dim_, shapes_):
324
+ return _gather(input_, dim_, shapes_, polar_group())
325
+
326
+ @staticmethod
327
+ @custom_fwd(device_type="cuda")
328
+ def forward(ctx, input_, dim_, shapes_):
329
+ if is_distributed_polar():
330
+ ctx.dim = dim_
331
+ return _gather(input_, dim_, shapes_, group=polar_group())
332
+ else:
333
+ return input_
334
+
335
+ @staticmethod
336
+ @custom_bwd(device_type="cuda")
337
+ def backward(ctx, grad_output):
338
+ if is_distributed_polar():
339
+ return _split(grad_output, ctx.dim, group=polar_group()), None, None
340
+ else:
341
+ return grad_output, None, None
342
+
343
+
344
+ class _ReduceFromPolarRegion(torch.autograd.Function):
345
+
346
+ @staticmethod
347
+ def symbolic(graph, input_):
348
+ if is_distributed_polar():
349
+ return _reduce(input_, group=polar_group())
350
+ else:
351
+ return input_
352
+
353
+ @staticmethod
354
+ @custom_fwd(device_type="cuda")
355
+ def forward(ctx, input_):
356
+ if is_distributed_polar():
357
+ return _reduce(input_, group=polar_group())
358
+ else:
359
+ return input_
360
+
361
+ @staticmethod
362
+ @custom_bwd(device_type="cuda")
363
+ def backward(ctx, grad_output):
364
+ return grad_output
365
+
366
+
367
+ class _ReduceFromAzimuthRegion(torch.autograd.Function):
368
+
369
+ @staticmethod
370
+ def symbolic(graph, input_):
371
+ if is_distributed_azimuth():
372
+ return _reduce(input_, group=azimuth_group())
373
+ else:
374
+ return input_
375
+
376
+ @staticmethod
377
+ @custom_fwd(device_type="cuda")
378
+ def forward(ctx, input_):
379
+ if is_distributed_azimuth():
380
+ return _reduce(input_, group=azimuth_group())
381
+ else:
382
+ return input_
383
+
384
+ @staticmethod
385
+ @custom_bwd(device_type="cuda")
386
+ def backward(ctx, grad_output):
387
+ return grad_output
388
+
389
+
390
+ class _ReduceFromScatterToPolarRegion(torch.autograd.Function):
391
+
392
+ @staticmethod
393
+ def symbolic(graph, input_, dim_):
394
+ if is_distributed_polar():
395
+ return _reduce_scatter(input_, dim_, group=polar_group())
396
+ else:
397
+ return input_
398
+
399
+ @staticmethod
400
+ @custom_fwd(device_type="cuda")
401
+ def forward(ctx, input_, dim_):
402
+ if is_distributed_polar():
403
+ ctx.dim = dim_
404
+ ctx.split_shapes = compute_split_shapes(
405
+ input_.shape[dim_], polar_group_size()
406
+ )
407
+ return _reduce_scatter(input_, dim_, group=polar_group())
408
+ else:
409
+ return input_
410
+
411
+ @staticmethod
412
+ @custom_bwd(device_type="cuda")
413
+ def backward(ctx, grad_output):
414
+ if is_distributed_polar():
415
+ return _gather(grad_output, ctx.dim, ctx.split_shapes, polar_group()), None
416
+ else:
417
+ return grad_output, None
418
+
419
+
420
+ class _GatherFromCopyToPolarRegion(torch.autograd.Function):
421
+
422
+ @staticmethod
423
+ def symbolic(graph, input_, dim_, shapes_):
424
+ if is_distributed_polar():
425
+ return _gather(input_, dim_, shapes_, polar_group())
426
+ else:
427
+ return input_
428
+
429
+ @staticmethod
430
+ @custom_fwd(device_type="cuda")
431
+ def forward(ctx, input_, dim_, shapes_):
432
+ if is_distributed_polar():
433
+ ctx.dim = dim_
434
+ return _gather(input_, dim_, shapes_, group=polar_group())
435
+ else:
436
+ return input_
437
+
438
+ @staticmethod
439
+ @custom_bwd(device_type="cuda")
440
+ def backward(ctx, grad_output):
441
+ if is_distributed_polar():
442
+ return _reduce_scatter(grad_output, ctx.dim, use_fp32=True, group=polar_group()), None, None
443
+ else:
444
+ return grad_output, None, None
445
+
446
+
447
+
448
+ def copy_to_polar_region(input_):
449
+ return _CopyToPolarRegion.apply(input_)
450
+
451
+ def copy_to_azimuth_region(input_):
452
+ return _CopyToAzimuthRegion.apply(input_)
453
+
454
+ def reduce_from_polar_region(input_):
455
+ return _ReduceFromPolarRegion.apply(input_)
456
+
457
+ def reduce_from_azimuth_region(input_):
458
+ return _ReduceFromAzimuthRegion.apply(input_)
459
+
460
+ def scatter_to_polar_region(input_, dim_):
461
+ return _ScatterToPolarRegion.apply(input_, dim_)
462
+
463
+ def gather_from_polar_region(input_, dim_, shapes_):
464
+ return _GatherFromPolarRegion.apply(input_, dim_, shapes_)
465
+
466
+ def reduce_from_scatter_to_polar_region(input_, dim_):
467
+ return _ReduceFromScatterToPolarRegion.apply(input_, dim_)
468
+
469
+ def gather_from_copy_to_polar_region(input_, dim_, shapes_):
470
+ return _GatherFromCopyToPolarRegion.apply(input_, dim_, shapes_)
.deps/torch_harmonics/distributed/utils.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2022 The torch-harmonics Authors. All rights reserved.
4
+ # SPDX-License-Identifier: BSD-3-Clause
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are met:
8
+ #
9
+ # 1. Redistributions of source code must retain the above copyright notice, this
10
+ # list of conditions and the following disclaimer.
11
+ #
12
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ # this list of conditions and the following disclaimer in the documentation
14
+ # and/or other materials provided with the distribution.
15
+ #
16
+ # 3. Neither the name of the copyright holder nor the names of its
17
+ # contributors may be used to endorse or promote products derived from
18
+ # this software without specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+ #
31
+
32
+ # we need this in order to enable distributed
33
+ import torch
34
+ import torch.distributed as dist
35
+
36
+ # those need to be global
37
+ _POLAR_PARALLEL_GROUP = None
38
+ _AZIMUTH_PARALLEL_GROUP = None
39
+ _IS_INITIALIZED = False
40
+
41
+ def polar_group():
42
+ return _POLAR_PARALLEL_GROUP
43
+
44
+ def azimuth_group():
45
+ return _AZIMUTH_PARALLEL_GROUP
46
+
47
+ def init(polar_process_group, azimuth_process_group):
48
+ global _POLAR_PARALLEL_GROUP
49
+ global _AZIMUTH_PARALLEL_GROUP
50
+ _POLAR_PARALLEL_GROUP = polar_process_group
51
+ _AZIMUTH_PARALLEL_GROUP = azimuth_process_group
52
+ _IS_INITIALIZED = True
53
+
54
+ def finalize():
55
+ if is_initialized():
56
+ if is_distributed_polar():
57
+ dist.destroy_process_group(_POLAR_PARALLEL_GROUP)
58
+ if is_distributed_azimuth():
59
+ ist.destroy_process_group(_AZIMUTH_PARALLEL_GROUP)
60
+
61
+ def is_initialized() -> bool:
62
+ return _IS_INITIALIZED
63
+
64
+ def is_distributed_polar() -> bool:
65
+ return (_POLAR_PARALLEL_GROUP is not None)
66
+
67
+ def is_distributed_azimuth() -> bool:
68
+ return (_AZIMUTH_PARALLEL_GROUP is not None)
69
+
70
+ def polar_group_size() -> int:
71
+ if not is_distributed_polar():
72
+ return 1
73
+ else:
74
+ return dist.get_world_size(group = _POLAR_PARALLEL_GROUP)
75
+
76
+ def azimuth_group_size() -> int:
77
+ if not is_distributed_azimuth():
78
+ return 1
79
+ else:
80
+ return dist.get_world_size(group = _AZIMUTH_PARALLEL_GROUP)
81
+
82
+ def polar_group_rank() -> int:
83
+ if not is_distributed_polar():
84
+ return 0
85
+ else:
86
+ return dist.get_rank(group = _POLAR_PARALLEL_GROUP)
87
+
88
+ def azimuth_group_rank() -> int:
89
+ if not is_distributed_azimuth():
90
+ return 0
91
+ else:
92
+ return dist.get_rank(group = _AZIMUTH_PARALLEL_GROUP)
.deps/torch_harmonics/examples/__init__.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2022 The torch-harmonics Authors. All rights reserved.
4
+ # SPDX-License-Identifier: BSD-3-Clause
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are met:
8
+ #
9
+ # 1. Redistributions of source code must retain the above copyright notice, this
10
+ # list of conditions and the following disclaimer.
11
+ #
12
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ # this list of conditions and the following disclaimer in the documentation
14
+ # and/or other materials provided with the distribution.
15
+ #
16
+ # 3. Neither the name of the copyright holder nor the names of its
17
+ # contributors may be used to endorse or promote products derived from
18
+ # this software without specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+ #
31
+
32
+ from .pde_sphere import SphereSolver
33
+ from .shallow_water_equations import ShallowWaterSolver
34
+
35
+ from .pde_dataset import PdeDataset
36
+ from .stanford_2d3ds_dataset import StanfordSegmentationDataset, StanfordDepthDataset, Stanford2D3DSDownloader, compute_stats_s2, StanfordDatasetSubset
.deps/torch_harmonics/examples/losses.py ADDED
@@ -0,0 +1,417 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2025 The torch-harmonics Authors. All rights reserved.
4
+ # SPDX-License-Identifier: BSD-3-Clause
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are met:
8
+ #
9
+ # 1. Redistributions of source code must retain the above copyright notice, this
10
+ # list of conditions and the following disclaimer.
11
+ #
12
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ # this list of conditions and the following disclaimer in the documentation
14
+ # and/or other materials provided with the distribution.
15
+ #
16
+ # 3. Neither the name of the copyright holder nor the names of its
17
+ # contributors may be used to endorse or promote products derived from
18
+ # this software without specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+ #
31
+
32
+ import torch
33
+ import torch.nn as nn
34
+ import torch.amp as amp
35
+ import torch.nn.functional as F
36
+ from typing import Optional
37
+ from abc import ABC, abstractmethod
38
+
39
+ from torch_harmonics.quadrature import _precompute_latitudes
40
+
41
+
42
+ def get_quadrature_weights(nlat: int, nlon: int, grid: str, tile: bool = False, normalized: bool = True) -> torch.Tensor:
43
+ # area weights
44
+ _, q = _precompute_latitudes(nlat=nlat, grid=grid)
45
+ q = q.reshape(-1, 1) * 2 * torch.pi / nlon
46
+
47
+ # numerical precision can be an issue here, make sure it sums to 1:
48
+ if normalized:
49
+ q = q / torch.sum(q) / float(nlon)
50
+
51
+ if tile:
52
+ q = torch.tile(q, (1, nlon)).contiguous()
53
+
54
+ return q.to(torch.float32)
55
+
56
+
57
+ class DiceLossS2(nn.Module):
58
+ """
59
+ Dice loss for spherical segmentation tasks.
60
+
61
+ Parameters
62
+ -----------
63
+ nlat : int
64
+ Number of latitude points
65
+ nlon : int
66
+ Number of longitude points
67
+ grid : str, optional
68
+ Grid type, by default "equiangular"
69
+ weight : torch.Tensor, optional
70
+ Class weights, by default None
71
+ smooth : float, optional
72
+ Smoothing factor, by default 0
73
+ ignore_index : int, optional
74
+ Index to ignore in loss computation, by default -100
75
+ mode : str, optional
76
+ Aggregation mode ("micro" or "macro"), by default "micro"
77
+ """
78
+
79
+ def __init__(self, nlat: int, nlon: int, grid: str = "equiangular", weight: torch.Tensor = None, smooth: float = 0, ignore_index: int = -100, mode: str = "micro"):
80
+
81
+ super().__init__()
82
+
83
+ self.smooth = smooth
84
+ self.ignore_index = ignore_index
85
+ self.mode = mode
86
+
87
+ # area weights
88
+ q = get_quadrature_weights(nlat=nlat, nlon=nlon, grid=grid)
89
+ self.register_buffer("quad_weights", q)
90
+
91
+ if weight is None:
92
+ self.weight = None
93
+ else:
94
+ self.register_buffer("weight", weight.unsqueeze(0))
95
+
96
+ def forward(self, prd: torch.Tensor, tar: torch.Tensor) -> torch.Tensor:
97
+ prd = nn.functional.softmax(prd, dim=1)
98
+
99
+ # mask values
100
+ if self.ignore_index is not None:
101
+ mask = torch.where(tar == self.ignore_index, 0, 1)
102
+ prd = prd * mask.unsqueeze(1)
103
+ tar = tar * mask
104
+
105
+ # one hot encode
106
+ taroh = nn.functional.one_hot(tar, num_classes=prd.shape[1]).permute(0, 3, 1, 2)
107
+
108
+ # compute numerator and denominator
109
+ intersection = torch.sum((prd * taroh) * self.quad_weights, dim=(-2, -1))
110
+ union = torch.sum((prd + taroh) * self.quad_weights, dim=(-2, -1))
111
+
112
+ if self.mode == "micro":
113
+ if self.weight is not None:
114
+ intersection = torch.sum(intersection * self.weight, dim=1)
115
+ union = torch.sum(union * self.weight, dim=1)
116
+ else:
117
+ intersection = torch.mean(intersection, dim=1)
118
+ union = torch.mean(union, dim=1)
119
+
120
+ # compute score
121
+ dice = (2 * intersection + self.smooth) / (union + self.smooth)
122
+
123
+ # compute average over classes
124
+ if self.mode == "macro":
125
+ if self.weight is not None:
126
+ dice = torch.sum(dice * self.weight, dim=1)
127
+ else:
128
+ dice = torch.mean(dice, dim=1)
129
+
130
+ # average over batch
131
+ dice = torch.mean(dice)
132
+
133
+ return 1 - dice
134
+
135
+
136
+ class CrossEntropyLossS2(nn.Module):
137
+ """
138
+ Cross-entropy loss for spherical classification tasks.
139
+
140
+ Parameters
141
+ -----------
142
+ nlat : int
143
+ Number of latitude points
144
+ nlon : int
145
+ Number of longitude points
146
+ grid : str, optional
147
+ Grid type, by default "equiangular"
148
+ weight : torch.Tensor, optional
149
+ Class weights, by default None
150
+ smooth : float, optional
151
+ Label smoothing factor, by default 0
152
+ ignore_index : int, optional
153
+ Index to ignore in loss computation, by default -100
154
+ """
155
+
156
+ def __init__(self, nlat: int, nlon: int, grid: str = "equiangular", weight: torch.Tensor = None, smooth: float = 0, ignore_index: int = -100):
157
+
158
+ super().__init__()
159
+
160
+ self.smooth = smooth
161
+ self.ignore_index = ignore_index
162
+
163
+ if weight is None:
164
+ self.weight = None
165
+ else:
166
+ self.register_buffer("weight", weight)
167
+
168
+ q = get_quadrature_weights(nlat=nlat, nlon=nlon, grid=grid)
169
+ self.register_buffer("quad_weights", q)
170
+
171
+ def forward(self, prd: torch.Tensor, tar: torch.Tensor) -> torch.Tensor:
172
+
173
+ # compute log softmax
174
+ logits = nn.functional.log_softmax(prd, dim=1)
175
+ ce = nn.functional.cross_entropy(logits, tar, weight=self.weight, reduction="none", ignore_index=self.ignore_index, label_smoothing=self.smooth)
176
+ ce = (ce * self.quad_weights).sum(dim=(-1, -2))
177
+ ce = torch.mean(ce)
178
+
179
+ return ce
180
+
181
+
182
+ class FocalLossS2(nn.Module):
183
+ """
184
+ Focal loss for spherical classification tasks.
185
+
186
+ Parameters
187
+ -----------
188
+ nlat : int
189
+ Number of latitude points
190
+ nlon : int
191
+ Number of longitude points
192
+ grid : str, optional
193
+ Grid type, by default "equiangular"
194
+ weight : torch.Tensor, optional
195
+ Class weights, by default None
196
+ smooth : float, optional
197
+ Label smoothing factor, by default 0
198
+ ignore_index : int, optional
199
+ Index to ignore in loss computation, by default -100
200
+ """
201
+
202
+ def __init__(self, nlat: int, nlon: int, grid: str = "equiangular", weight: torch.Tensor = None, smooth: float = 0, ignore_index: int = -100):
203
+
204
+ super().__init__()
205
+
206
+ self.smooth = smooth
207
+ self.ignore_index = ignore_index
208
+
209
+ if weight is None:
210
+ self.weight = None
211
+ else:
212
+ self.register_buffer("weight", weight)
213
+
214
+ q = get_quadrature_weights(nlat=nlat, nlon=nlon, grid=grid)
215
+ self.register_buffer("quad_weights", q)
216
+
217
+ def forward(self, prd: torch.Tensor, tar: torch.Tensor, alpha: float = 0.25, gamma: float = 2):
218
+
219
+ # compute logits
220
+ logits = nn.functional.log_softmax(prd, dim=1)
221
+
222
+ # w = (1.0 - nn.functional.softmax(prd, dim=-3)).pow(gamma)
223
+ # w = torch.where(tar == self.ignore_index, 0.0, w.gather(-3, tar.unsqueeze(-3)).squeeze(-3))
224
+ ce = nn.functional.cross_entropy(logits, tar, weight=self.weight, reduction="none", ignore_index=self.ignore_index, label_smoothing=self.smooth)
225
+ fl = alpha * (1 - torch.exp(-ce)) ** gamma * ce
226
+ # fl = w * ce
227
+ fl = (fl * self.quad_weights).sum(dim=(-1, -2))
228
+ fl = fl.mean()
229
+
230
+ return fl
231
+
232
+
233
+ class SphericalLossBase(nn.Module, ABC):
234
+ """Abstract base class for spherical losses that handles common initialization and integration."""
235
+
236
+ def __init__(self, nlat: int, nlon: int, grid: str = "equiangular", normalized: bool = True):
237
+ super().__init__()
238
+
239
+ self.nlat = nlat
240
+ self.nlon = nlon
241
+ self.grid = grid
242
+
243
+ # get quadrature weights - these sum to 1!
244
+ q = get_quadrature_weights(nlat=nlat, nlon=nlon, grid=grid, normalized=normalized)
245
+ self.register_buffer("quad_weights", q)
246
+
247
+ def _integrate_sphere(self, ugrid, mask=None):
248
+ if mask is None:
249
+ out = torch.sum(ugrid * self.quad_weights, dim=(-2, -1))
250
+ elif mask is not None:
251
+ out = torch.sum(mask * ugrid * self.quad_weights, dim=(-2, -1)) / torch.sum(mask * self.quad_weights, dim=(-2, -1))
252
+ return out
253
+
254
+ @abstractmethod
255
+ def _compute_loss_term(self, prd: torch.Tensor, tar: torch.Tensor) -> torch.Tensor:
256
+ """Abstract method that must be implemented by child classes to compute loss terms.
257
+
258
+ Args:
259
+ prd (torch.Tensor): Prediction tensor
260
+ tar (torch.Tensor): Target tensor
261
+
262
+ Returns:
263
+ torch.Tensor: Computed loss term before integration
264
+ """
265
+ pass
266
+
267
+ def _post_integration_hook(self, loss: torch.Tensor) -> torch.Tensor:
268
+ """Post-integration hook. Commonly used for the roots in Lp norms"""
269
+ return loss
270
+
271
+ def forward(self, prd: torch.Tensor, tar: torch.Tensor, mask: Optional[torch.Tensor] = None) -> torch.Tensor:
272
+ """Common forward pass that handles masking and reduction.
273
+
274
+ Args:
275
+ prd (torch.Tensor): Prediction tensor
276
+ tar (torch.Tensor): Target tensor
277
+ mask (Optional[torch.Tensor], optional): Mask tensor. Defaults to None.
278
+
279
+ Returns:
280
+ torch.Tensor: Final loss value
281
+ """
282
+ loss_term = self._compute_loss_term(prd, tar)
283
+ # Integrate over the sphere for each item in the batch
284
+ loss = self._integrate_sphere(loss_term, mask)
285
+ # potentially call root
286
+ loss = self._post_integration_hook(loss)
287
+ # Average the loss over the batch dimension
288
+ return torch.mean(loss)
289
+
290
+
291
+ class SquaredL2LossS2(SphericalLossBase):
292
+ def _compute_loss_term(self, prd: torch.Tensor, tar: torch.Tensor) -> torch.Tensor:
293
+ return torch.square(prd - tar)
294
+
295
+
296
+ class L1LossS2(SphericalLossBase):
297
+ def _compute_loss_term(self, prd: torch.Tensor, tar: torch.Tensor) -> torch.Tensor:
298
+ return torch.abs(prd - tar)
299
+
300
+
301
+ class L2LossS2(SquaredL2LossS2):
302
+ def _post_integration_hook(self, loss: torch.Tensor) -> torch.Tensor:
303
+ return torch.sqrt(loss)
304
+
305
+
306
+ class W11LossS2(SphericalLossBase):
307
+ def __init__(self, nlat: int, nlon: int, grid: str = "equiangular"):
308
+ super().__init__(nlat=nlat, nlon=nlon, grid=grid)
309
+ # Set up grid and domain for FFT
310
+ l_phi = 2 * torch.pi # domain size
311
+ l_theta = torch.pi # domain size
312
+
313
+ k_phi = torch.fft.fftfreq(nlon, d=l_phi / (2 * torch.pi * nlon))
314
+ k_theta = torch.fft.fftfreq(nlat, d=l_theta / (2 * torch.pi * nlat))
315
+ k_theta_mesh, k_phi_mesh = torch.meshgrid(k_theta, k_phi, indexing="ij")
316
+ self.register_buffer("k_phi_mesh", k_phi_mesh)
317
+ self.register_buffer("k_theta_mesh", k_theta_mesh)
318
+
319
+ def _compute_loss_term(self, prd: torch.Tensor, tar: torch.Tensor) -> torch.Tensor:
320
+ prdtype = prd.dtype
321
+ with amp.autocast(device_type="cuda", enabled=False):
322
+ prd = prd.to(torch.float32)
323
+ prd_prime_fft2_phi_h = torch.fft.ifft2(1j * self.k_phi_mesh * torch.fft.fft2(prd)).real
324
+ prd_prime_fft2_theta_h = torch.fft.ifft2(1j * self.k_theta_mesh * torch.fft.fft2(prd)).real
325
+
326
+ tar_prime_fft2_phi_h = torch.fft.ifft2(1j * self.k_phi_mesh * torch.fft.fft2(tar)).real
327
+ tar_prime_fft2_theta_h = torch.fft.ifft2(1j * self.k_theta_mesh * torch.fft.fft2(tar)).real
328
+
329
+ # Return the element-wise loss term
330
+ return torch.abs(prd_prime_fft2_phi_h - tar_prime_fft2_phi_h) + torch.abs(prd_prime_fft2_theta_h - tar_prime_fft2_theta_h)
331
+
332
+
333
+ class NormalLossS2(SphericalLossBase):
334
+ """Combined L1 and Surface Normal Consistency Loss for spherical data.
335
+
336
+ This loss function combines an L1 loss term with a surface normal alignment term.
337
+
338
+ The loss consists of:
339
+ 1. L1 Loss: Absolute difference between predicted and target values
340
+ 2. Normal Consistency Loss: 1 - cosine similarity between surface normals
341
+ (equivalent to cosine distance between normal vectors)
342
+
343
+ Surface normals are computed by calculating gradients in latitude and longitude
344
+ directions using FFT, then constructing 3D normal vectors that are normalized.
345
+
346
+ Parameters
347
+ ----------
348
+ nlat : int
349
+ Number of latitude points
350
+ nlon : int
351
+ Number of longitude points
352
+ grid : str, optional
353
+ Grid type, by default "equiangular"
354
+
355
+ Returns
356
+ -------
357
+ torch.Tensor
358
+ Combined loss term
359
+ """
360
+
361
+ def __init__(self, nlat: int, nlon: int, grid: str = "equiangular"):
362
+ super().__init__(nlat=nlat, nlon=nlon, grid=grid)
363
+ # Set up grid and domain for FFT
364
+ l_phi = 2 * torch.pi # domain size
365
+ l_theta = torch.pi # domain size
366
+
367
+ k_phi = torch.fft.fftfreq(nlon, d=l_phi / (2 * torch.pi * nlon))
368
+ k_theta = torch.fft.fftfreq(nlat, d=l_theta / (2 * torch.pi * nlat))
369
+ k_theta_mesh, k_phi_mesh = torch.meshgrid(k_theta, k_phi, indexing="ij")
370
+ self.register_buffer("k_phi_mesh", k_phi_mesh)
371
+ self.register_buffer("k_theta_mesh", k_theta_mesh)
372
+
373
+ def compute_gradients(self, x):
374
+ # Make sure x is reshaped to have a batch dimension if it's missing
375
+ if x.dim() == 2:
376
+ x = x.unsqueeze(0) # Add batch dimension
377
+
378
+ x_prime_fft2_phi_h = torch.fft.ifft2(1j * self.k_phi_mesh * torch.fft.fft2(x)).real
379
+ x_prime_fft2_theta_h = torch.fft.ifft2(1j * self.k_theta_mesh * torch.fft.fft2(x)).real
380
+ return x_prime_fft2_theta_h, x_prime_fft2_phi_h
381
+
382
+ def compute_normals(self, x):
383
+ x = x.to(torch.float32)
384
+ # Ensure x has a batch dimension
385
+ if x.dim() == 2:
386
+ x = x.unsqueeze(0)
387
+
388
+ grad_lat, grad_lon = self.compute_gradients(x)
389
+
390
+ # Create 3D normal vectors
391
+ ones = torch.ones_like(x)
392
+ normals = torch.stack([-grad_lon, -grad_lat, ones], dim=1)
393
+
394
+ # Normalize along component dimension
395
+ normals = F.normalize(normals, p=2, dim=1)
396
+ return normals
397
+
398
+ def _compute_loss_term(self, prd: torch.Tensor, tar: torch.Tensor) -> torch.Tensor:
399
+ # Handle dimensions for both prediction and target
400
+ # Ensure we have at least a batch dimension
401
+ if prd.dim() == 2:
402
+ prd = prd.unsqueeze(0)
403
+ if tar.dim() == 2:
404
+ tar = tar.unsqueeze(0)
405
+
406
+ # For 4D tensors (batch, channel, height, width), remove channel if it's 1
407
+ if prd.dim() == 4 and prd.size(1) == 1:
408
+ prd = prd.squeeze(1)
409
+ if tar.dim() == 4 and tar.size(1) == 1:
410
+ tar = tar.squeeze(1)
411
+
412
+ pred_normals = self.compute_normals(prd)
413
+ tar_normals = self.compute_normals(tar)
414
+
415
+ # Compute cosine similarity
416
+ normal_loss = 1 - torch.sum(pred_normals * tar_normals, dim=1, keepdim=True)
417
+ return normal_loss
.deps/torch_harmonics/examples/metrics.py ADDED
@@ -0,0 +1,291 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2025 The torch-harmonics Authors. All rights reserved.
4
+ # SPDX-License-Identifier: BSD-3-Clause
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are met:
8
+ #
9
+ # 1. Redistributions of source code must retain the above copyright notice, this
10
+ # list of conditions and the following disclaimer.
11
+ #
12
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ # this list of conditions and the following disclaimer in the documentation
14
+ # and/or other materials provided with the distribution.
15
+ #
16
+ # 3. Neither the name of the copyright holder nor the names of its
17
+ # contributors may be used to endorse or promote products derived from
18
+ # this software without specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+ #
31
+
32
+ from typing import Optional, Tuple
33
+
34
+ import torch
35
+ import torch.nn as nn
36
+
37
+ from torch_harmonics.quadrature import _precompute_latitudes
38
+ from .losses import get_quadrature_weights
39
+
40
+
41
+ # routine to compute multiclass labels on the sphere
42
+ # the routine follows the implementation in
43
+ # https://github.com/qubvel-org/segmentation_models.pytorch/blob/4aa36c6ad13f8a12552e4ea4131af2a86e564962/segmentation_models_pytorch/metrics/functional.py
44
+ # but uses quadrature weights
45
+ def _get_stats_multiclass(
46
+ output: torch.LongTensor,
47
+ target: torch.LongTensor,
48
+ num_classes: int,
49
+ quad_weights: torch.Tensor,
50
+ ignore_index: Optional[int],
51
+ ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
52
+ """
53
+ Compute multiclass statistics (TP, FP, FN, TN) on the sphere using quadrature weights.
54
+
55
+ This function computes true positives, false positives, false negatives, and true negatives
56
+ for multiclass classification on spherical data, properly weighted by quadrature weights
57
+ to account for the spherical geometry.
58
+
59
+ Parameters
60
+ -----------
61
+ output : torch.LongTensor
62
+ Predicted class labels
63
+ target : torch.LongTensor
64
+ Ground truth class labels
65
+ num_classes : int
66
+ Number of classes in the classification task
67
+ quad_weights : torch.Tensor
68
+ Quadrature weights for spherical integration
69
+ ignore_index : Optional[int]
70
+ Index to ignore in the computation (e.g., for padding or invalid regions)
71
+
72
+ Returns
73
+ -------
74
+ Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]
75
+ Tuple containing (tp_count, fp_count, fn_count, tn_count) for each class
76
+ """
77
+ batch_size, *dims = output.shape
78
+ num_elements = torch.prod(torch.tensor(dims)).long()
79
+
80
+ if ignore_index is not None:
81
+ ignore = target == ignore_index
82
+ output = torch.where(ignore, -1, output)
83
+ target = torch.where(ignore, -1, target)
84
+ ignore_per_sample = ignore.view(batch_size, -1).sum(1)
85
+
86
+ tp_count = torch.zeros(batch_size, num_classes, dtype=torch.float32, device=output.device)
87
+ fp_count = torch.zeros(batch_size, num_classes, dtype=torch.float32, device=output.device)
88
+ fn_count = torch.zeros(batch_size, num_classes, dtype=torch.float32, device=output.device)
89
+ tn_count = torch.zeros(batch_size, num_classes, dtype=torch.float32, device=output.device)
90
+
91
+ matched = target == output
92
+ not_matched = target != output
93
+ for i in range(batch_size):
94
+ matched_i = matched[i, ...]
95
+ not_matched_i = not_matched[i, ...]
96
+ target_i = target[i, ...]
97
+ output_i = output[i, ...]
98
+ for c in range(num_classes):
99
+ # compute weights
100
+ qwt_c = quad_weights[target_i == c]
101
+ qwo_c = quad_weights[output_i == c]
102
+
103
+ # true positives
104
+ tp_count[i, c] = torch.sum(matched_i[target_i == c] * qwt_c)
105
+ # false positives
106
+ fp_count[i, c] = torch.sum(not_matched_i[output_i == c] * qwo_c)
107
+ # false negatives
108
+ fn_count[i, c] = torch.sum(not_matched_i[target_i == c] * qwt_c)
109
+
110
+ # true negatives is the leftovers
111
+ tn_count = torch.sum(quad_weights) - tp_count - fp_count - fn_count
112
+ return tp_count, fp_count, fn_count, tn_count
113
+
114
+
115
+ def _predict_classes(logits: torch.Tensor) -> torch.Tensor:
116
+ """
117
+ Convert logits to class predictions using softmax and argmax.
118
+
119
+ Parameters
120
+ -----------
121
+ logits : torch.Tensor
122
+ Input logits tensor
123
+
124
+ Returns
125
+ -------
126
+ torch.Tensor
127
+ Predicted class labels
128
+ """
129
+ return torch.argmax(torch.softmax(logits, dim=1), dim=1, keepdim=False)
130
+
131
+
132
+ class BaseMetricS2(nn.Module):
133
+ """
134
+ Base class for spherical metrics that properly handle spherical geometry.
135
+
136
+ This class provides the foundation for computing metrics on spherical data
137
+ by using quadrature weights to account for the non-uniform area distribution
138
+ on the sphere.
139
+
140
+ Parameters
141
+ -----------
142
+ nlat : int
143
+ Number of latitude points
144
+ nlon : int
145
+ Number of longitude points
146
+ grid : str, optional
147
+ Grid type ("equiangular", "legendre-gauss", etc.), by default "equiangular"
148
+ weight : torch.Tensor, optional
149
+ Class weights for weighted averaging, by default None
150
+ ignore_index : int, optional
151
+ Index to ignore in computations, by default -100
152
+ mode : str, optional
153
+ Averaging mode ("micro" or "macro"), by default "micro"
154
+ """
155
+
156
+ def __init__(self, nlat: int, nlon: int, grid: str = "equiangular", weight: torch.Tensor = None, ignore_index: int = -100, mode: str = "micro"):
157
+ super().__init__()
158
+
159
+ self.ignore_index = ignore_index
160
+ self.mode = mode
161
+
162
+ # area weights
163
+ q = get_quadrature_weights(nlat=nlat, nlon=nlon, grid=grid, tile=True)
164
+ self.register_buffer("quad_weights", q)
165
+
166
+ if weight is None:
167
+ self.weight = None
168
+ else:
169
+ self.register_buffer("weight", weight.unsqueeze(0))
170
+
171
+ def _forward(self, pred: torch.Tensor, truth: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
172
+
173
+ # convert logits to class predictions
174
+ pred_class = _predict_classes(pred)
175
+
176
+ # get true positive, false positive, etc
177
+ tp, fp, fn, tn = _get_stats_multiclass(pred_class, truth, pred.shape[1], self.quad_weights, self.ignore_index)
178
+
179
+ # compute averages:
180
+ if self.mode == "micro":
181
+ if self.weight is not None:
182
+ # weighted average
183
+ tp = torch.sum(tp * self.weight)
184
+ fp = torch.sum(fp * self.weight)
185
+ fn = torch.sum(fn * self.weight)
186
+ tn = torch.sum(tn * self.weight)
187
+ else:
188
+ # normal average
189
+ tp = torch.mean(tp)
190
+ fp = torch.mean(fp)
191
+ fn = torch.mean(fn)
192
+ tn = torch.mean(tn)
193
+ else:
194
+ tp = torch.mean(tp, dim=0)
195
+ fp = torch.mean(fp, dim=0)
196
+ fn = torch.mean(fn, dim=0)
197
+ tn = torch.mean(tn, dim=0)
198
+
199
+ return tp, fp, fn, tn
200
+
201
+
202
+ class IntersectionOverUnionS2(BaseMetricS2):
203
+ """
204
+ Intersection over Union (IoU) metric for spherical data.
205
+
206
+ Computes the IoU score for multiclass classification on the sphere,
207
+ properly weighted by quadrature weights to account for spherical geometry.
208
+
209
+ Parameters
210
+ -----------
211
+ nlat : int
212
+ Number of latitude points
213
+ nlon : int
214
+ Number of longitude points
215
+ grid : str, optional
216
+ Grid type ("equiangular", "legendre-gauss", etc.), by default "equiangular"
217
+ weight : torch.Tensor, optional
218
+ Class weights for weighted averaging, by default None
219
+ ignore_index : int, optional
220
+ Index to ignore in computations, by default -100
221
+ mode : str, optional
222
+ Averaging mode ("micro" or "macro"), by default "micro"
223
+ """
224
+
225
+ def __init__(self, nlat: int, nlon: int, grid: str = "equiangular", weight: torch.Tensor = None, ignore_index: int = -100, mode: str = "micro"):
226
+ super().__init__(nlat, nlon, grid, weight, ignore_index, mode)
227
+
228
+ def forward(self, pred: torch.Tensor, truth: torch.Tensor) -> torch.Tensor:
229
+
230
+ tp, fp, fn, tn = self._forward(pred, truth)
231
+
232
+ # compute score
233
+ score = tp / (tp + fp + fn)
234
+
235
+ if self.mode == "macro":
236
+ # we need to do some averaging still:
237
+ # be careful with zeros
238
+ score = torch.where(torch.isnan(score), 0.0, score)
239
+
240
+ if self.weight is not None:
241
+ score = torch.sum(score * self.weight)
242
+ else:
243
+ score = torch.mean(score)
244
+
245
+ return score
246
+
247
+
248
+ class AccuracyS2(BaseMetricS2):
249
+ """
250
+ Accuracy metric for spherical data.
251
+
252
+ Computes the accuracy score for multiclass classification on the sphere,
253
+ properly weighted by quadrature weights to account for spherical geometry.
254
+
255
+ Parameters
256
+ -----------
257
+ nlat : int
258
+ Number of latitude points
259
+ nlon : int
260
+ Number of longitude points
261
+ grid : str, optional
262
+ Grid type ("equiangular", "legendre-gauss", etc.), by default "equiangular"
263
+ weight : torch.Tensor, optional
264
+ Class weights for weighted averaging, by default None
265
+ ignore_index : int, optional
266
+ Index to ignore in computations, by default -100
267
+ mode : str, optional
268
+ Averaging mode ("micro" or "macro"), by default "micro"
269
+ """
270
+
271
+ def __init__(self, nlat: int, nlon: int, grid: str = "equiangular", weight: torch.Tensor = None, ignore_index: int = -100, mode: str = "micro"):
272
+ super().__init__(nlat, nlon, grid, weight, ignore_index, mode)
273
+
274
+ def forward(self, pred: torch.Tensor, truth: torch.Tensor) -> torch.Tensor:
275
+
276
+ tp, fp, fn, tn = self._forward(pred, truth)
277
+
278
+ # compute score
279
+ score = (tp + tn) / (tp + fp + fn + tn)
280
+
281
+ if self.mode == "macro":
282
+ # we need to do some averaging still:
283
+ # be careful with zeros
284
+ score = torch.where(torch.isnan(score), 0.0, score)
285
+
286
+ if self.weight is not None:
287
+ score = torch.sum(score * self.weight)
288
+ else:
289
+ score = torch.mean(score)
290
+
291
+ return score
.deps/torch_harmonics/examples/models/__init__.py ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2022 The torch-harmonics Authors. All rights reserved.
4
+ # SPDX-License-Identifier: BSD-3-Clause
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are met:
8
+ #
9
+ # 1. Redistributions of source code must retain the above copyright notice, this
10
+ # list of conditions and the following disclaimer.
11
+ #
12
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ # this list of conditions and the following disclaimer in the documentation
14
+ # and/or other materials provided with the distribution.
15
+ #
16
+ # 3. Neither the name of the copyright holder nor the names of its
17
+ # contributors may be used to endorse or promote products derived from
18
+ # this software without specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+ #
31
+
32
+ from .sfno import SphericalFourierNeuralOperator
33
+ from .lsno import LocalSphericalNeuralOperator
34
+ from .s2unet import SphericalUNet
35
+ from .s2transformer import SphericalTransformer
36
+ from .s2segformer import SphericalSegformer
.deps/torch_harmonics/examples/models/_layers.py ADDED
@@ -0,0 +1,621 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2022 The torch-harmonics Authors. All rights reserved.
4
+ # SPDX-License-Identifier: BSD-3-Clause
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are met:
8
+ #
9
+ # 1. Redistributions of source code must retain the above copyright notice, this
10
+ # list of conditions and the following disclaimer.
11
+ #
12
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ # this list of conditions and the following disclaimer in the documentation
14
+ # and/or other materials provided with the distribution.
15
+ #
16
+ # 3. Neither the name of the copyright holder nor the names of its
17
+ # contributors may be used to endorse or promote products derived from
18
+ # this software without specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+ #
31
+
32
+ import abc
33
+ import math
34
+
35
+ import torch
36
+ import torch.nn as nn
37
+ import torch.fft
38
+ from torch.utils.checkpoint import checkpoint
39
+
40
+ from torch_harmonics import InverseRealSHT
41
+
42
+
43
+ def _no_grad_trunc_normal_(tensor, mean, std, a, b):
44
+
45
+ # Cut & paste from PyTorch official master until it's in a few official releases - RW
46
+ # Method based on https://people.sc.fsu.edu/~jburkardt/presentations/truncated_normal.pdf
47
+ def norm_cdf(x):
48
+
49
+ # Computes standard normal cumulative distribution function
50
+ return (1.0 + math.erf(x / math.sqrt(2.0))) / 2.0
51
+
52
+ if (mean < a - 2 * std) or (mean > b + 2 * std):
53
+ warnings.warn("mean is more than 2 std from [a, b] in nn.init.trunc_normal_. " "The distribution of values may be incorrect.", stacklevel=2)
54
+
55
+ with torch.no_grad():
56
+ # Values are generated by using a truncated uniform distribution and
57
+ # then using the inverse CDF for the normal distribution.
58
+ # Get upper and lower cdf values
59
+ l = norm_cdf((a - mean) / std)
60
+ u = norm_cdf((b - mean) / std)
61
+
62
+ # Uniformly fill tensor with values from [l, u], then translate to
63
+ # [2l-1, 2u-1].
64
+ tensor.uniform_(2 * l - 1, 2 * u - 1)
65
+
66
+ # Use inverse cdf transform for normal distribution to get truncated
67
+ # standard normal
68
+ tensor.erfinv_()
69
+
70
+ # Transform to proper mean, std
71
+ tensor.mul_(std * math.sqrt(2.0))
72
+ tensor.add_(mean)
73
+
74
+ # Clamp to ensure it's in the proper range
75
+ tensor.clamp_(min=a, max=b)
76
+ return tensor
77
+
78
+
79
+ def trunc_normal_(tensor, mean=0.0, std=1.0, a=-2.0, b=2.0):
80
+ """Fills the input Tensor with values drawn from a truncated
81
+ normal distribution. The values are effectively drawn from the
82
+ normal distribution :math:`\mathcal{N}(\text{mean}, \text{std}^2)`
83
+ with values outside :math:`[a, b]` redrawn until they are within
84
+ the bounds. The method used for generating the random values works
85
+ best when :math:`a \leq \text{mean} \leq b`.
86
+
87
+ Parameters
88
+ -----------
89
+ tensor: torch.Tensor
90
+ an n-dimensional `torch.Tensor`
91
+ mean: float
92
+ the mean of the normal distribution
93
+ std: float
94
+ the standard deviation of the normal distribution
95
+ a: float
96
+ the minimum cutoff value, by default -2.0
97
+ b: float
98
+ the maximum cutoff value
99
+ Examples
100
+ --------
101
+ >>> w = torch.empty(3, 5)
102
+ >>> nn.init.trunc_normal_(w)
103
+ """
104
+ return _no_grad_trunc_normal_(tensor, mean, std, a, b)
105
+
106
+
107
+ @torch.jit.script
108
+ def drop_path(x: torch.Tensor, drop_prob: float = 0.0, training: bool = False) -> torch.Tensor:
109
+ """Drop paths (Stochastic Depth) per sample (when applied in main path of residual blocks).
110
+ This is the same as the DropConnect impl I created for EfficientNet, etc networks, however,
111
+ the original name is misleading as 'Drop Connect' is a different form of dropout in a separate paper...
112
+ See discussion: https://github.com/tensorflow/tpu/issues/494#issuecomment-532968956 ... I've opted for
113
+ changing the layer and argument names to 'drop path' rather than mix DropConnect as a layer name and use
114
+ 'survival rate' as the argument.
115
+
116
+ Parameters
117
+ ----------
118
+ x : torch.Tensor
119
+ Input tensor
120
+ drop_prob : float, optional
121
+ Probability of dropping a path, by default 0.0
122
+ training : bool, optional
123
+ Whether the model is in training mode, by default False
124
+
125
+ Returns
126
+ -------
127
+ torch.Tensor
128
+ Output tensor
129
+ """
130
+ if drop_prob == 0.0 or not training:
131
+ return x
132
+ keep_prob = 1.0 - drop_prob
133
+ shape = (x.shape[0],) + (1,) * (x.ndim - 1) # work with diff dim tensors, not just 2d ConvNets
134
+ random_tensor = keep_prob + torch.rand(shape, dtype=x.dtype, device=x.device)
135
+ random_tensor.floor_() # binarize
136
+ output = x.div(keep_prob) * random_tensor
137
+ return output
138
+
139
+
140
+ class DropPath(nn.Module):
141
+ """
142
+ Drop paths (Stochastic Depth) per sample (when applied in main path of residual blocks).
143
+
144
+ This module implements stochastic depth regularization by randomly dropping
145
+ entire residual paths during training, which helps with regularization and
146
+ training of very deep networks.
147
+
148
+ Parameters
149
+ ----------
150
+ drop_prob : float, optional
151
+ Probability of dropping a path, by default None
152
+ """
153
+
154
+ def __init__(self, drop_prob=None):
155
+ super(DropPath, self).__init__()
156
+ self.drop_prob = drop_prob
157
+
158
+ def forward(self, x):
159
+
160
+ return drop_path(x, self.drop_prob, self.training)
161
+
162
+
163
+ class PatchEmbed(nn.Module):
164
+ """
165
+ Patch embedding layer for vision transformers.
166
+
167
+ This module splits input images into patches and projects them to a
168
+ higher dimensional embedding space using convolutional layers.
169
+
170
+ Parameters
171
+ ----------
172
+ img_size : tuple, optional
173
+ Input image size (height, width), by default (224, 224)
174
+ patch_size : tuple, optional
175
+ Patch size (height, width), by default (16, 16)
176
+ in_chans : int, optional
177
+ Number of input channels, by default 3
178
+ embed_dim : int, optional
179
+ Embedding dimension, by default 768
180
+ """
181
+
182
+ def __init__(self, img_size=(224, 224), patch_size=(16, 16), in_chans=3, embed_dim=768):
183
+ super(PatchEmbed, self).__init__()
184
+ self.red_img_size = ((img_size[0] // patch_size[0]), (img_size[1] // patch_size[1]))
185
+ num_patches = self.red_img_size[0] * self.red_img_size[1]
186
+ self.img_size = img_size
187
+ self.patch_size = patch_size
188
+ self.num_patches = num_patches
189
+ self.proj = nn.Conv2d(in_chans, embed_dim, kernel_size=patch_size, stride=patch_size, bias=True)
190
+ self.proj.weight.is_shared_mp = ["spatial"]
191
+ self.proj.bias.is_shared_mp = ["spatial"]
192
+
193
+ def forward(self, x):
194
+
195
+ # gather input
196
+ B, C, H, W = x.shape
197
+ assert H == self.img_size[0] and W == self.img_size[1], f"Input image size ({H}*{W}) doesn't match model ({self.img_size[0]}*{self.img_size[1]})."
198
+ # new: B, C, H*W
199
+ x = self.proj(x).flatten(2)
200
+ return x
201
+
202
+
203
+ class MLP(nn.Module):
204
+ """
205
+ Multi-layer perceptron with optional checkpointing.
206
+
207
+ This module implements a feed-forward network with two linear layers
208
+ and an activation function, with optional dropout and gradient checkpointing.
209
+
210
+ Parameters
211
+ ----------
212
+ in_features : int
213
+ Number of input features
214
+ hidden_features : int, optional
215
+ Number of hidden features, by default None (same as in_features)
216
+ out_features : int, optional
217
+ Number of output features, by default None (same as in_features)
218
+ act_layer : nn.Module, optional
219
+ Activation layer, by default nn.ReLU
220
+ output_bias : bool, optional
221
+ Whether to use bias in output layer, by default False
222
+ drop_rate : float, optional
223
+ Dropout rate, by default 0.0
224
+ checkpointing : bool, optional
225
+ Whether to use gradient checkpointing, by default False
226
+ gain : float, optional
227
+ Gain factor for weight initialization, by default 1.0
228
+ """
229
+
230
+ def __init__(self, in_features, hidden_features=None, out_features=None, act_layer=nn.ReLU, output_bias=False, drop_rate=0.0, checkpointing=False, gain=1.0):
231
+ super(MLP, self).__init__()
232
+ self.checkpointing = checkpointing
233
+ out_features = out_features or in_features
234
+ hidden_features = hidden_features or in_features
235
+
236
+ # Fist dense layer
237
+ fc1 = nn.Conv2d(in_features, hidden_features, 1, bias=True)
238
+ # initialize the weights correctly
239
+ scale = math.sqrt(2.0 / in_features)
240
+ nn.init.normal_(fc1.weight, mean=0.0, std=scale)
241
+ if fc1.bias is not None:
242
+ nn.init.constant_(fc1.bias, 0.0)
243
+
244
+ # activation
245
+ act = act_layer()
246
+
247
+ # output layer
248
+ fc2 = nn.Conv2d(hidden_features, out_features, 1, bias=output_bias)
249
+ # gain factor for the output determines the scaling of the output init
250
+ scale = math.sqrt(gain / hidden_features)
251
+ nn.init.normal_(fc2.weight, mean=0.0, std=scale)
252
+ if fc2.bias is not None:
253
+ nn.init.constant_(fc2.bias, 0.0)
254
+
255
+ if drop_rate > 0.0:
256
+ drop = nn.Dropout2d(drop_rate)
257
+ self.fwd = nn.Sequential(fc1, act, drop, fc2, drop)
258
+ else:
259
+ self.fwd = nn.Sequential(fc1, act, fc2)
260
+
261
+ @torch.jit.ignore
262
+ def checkpoint_forward(self, x):
263
+
264
+ return checkpoint(self.fwd, x)
265
+
266
+ def forward(self, x):
267
+
268
+ if self.checkpointing:
269
+ return self.checkpoint_forward(x)
270
+ else:
271
+ return self.fwd(x)
272
+
273
+
274
+ class RealFFT2(nn.Module):
275
+ """
276
+ Helper routine to wrap FFT similarly to the SHT.
277
+
278
+ This module provides a wrapper around PyTorch's real FFT2D that mimics
279
+ the interface of spherical harmonic transforms for consistency.
280
+
281
+ Parameters
282
+ -----------
283
+ nlat : int
284
+ Number of latitude points
285
+ nlon : int
286
+ Number of longitude points
287
+ lmax : int, optional
288
+ Maximum spherical harmonic degree, by default None (same as nlat)
289
+ mmax : int, optional
290
+ Maximum spherical harmonic order, by default None (nlon//2 + 1)
291
+ """
292
+
293
+ def __init__(self, nlat, nlon, lmax=None, mmax=None):
294
+ super(RealFFT2, self).__init__()
295
+
296
+ self.nlat = nlat
297
+ self.nlon = nlon
298
+ self.lmax = lmax or self.nlat
299
+ self.mmax = mmax or self.nlon // 2 + 1
300
+
301
+ def forward(self, x):
302
+
303
+ y = torch.fft.rfft2(x, dim=(-2, -1), norm="ortho")
304
+ y = torch.cat((y[..., : math.ceil(self.lmax / 2), : self.mmax], y[..., -math.floor(self.lmax / 2) :, : self.mmax]), dim=-2)
305
+ return y
306
+
307
+
308
+ class InverseRealFFT2(nn.Module):
309
+ """
310
+ Helper routine to wrap inverse FFT similarly to the SHT.
311
+
312
+ This module provides a wrapper around PyTorch's inverse real FFT2D that mimics
313
+ the interface of inverse spherical harmonic transforms for consistency.
314
+
315
+ Parameters
316
+ -----------
317
+ nlat : int
318
+ Number of latitude points
319
+ nlon : int
320
+ Number of longitude points
321
+ lmax : int, optional
322
+ Maximum spherical harmonic degree, by default None (same as nlat)
323
+ mmax : int, optional
324
+ Maximum spherical harmonic order, by default None (nlon//2 + 1)
325
+ """
326
+
327
+ def __init__(self, nlat, nlon, lmax=None, mmax=None):
328
+ super(InverseRealFFT2, self).__init__()
329
+
330
+ self.nlat = nlat
331
+ self.nlon = nlon
332
+ self.lmax = lmax or self.nlat
333
+ self.mmax = mmax or self.nlon // 2 + 1
334
+
335
+ def forward(self, x):
336
+
337
+ return torch.fft.irfft2(x, dim=(-2, -1), s=(self.nlat, self.nlon), norm="ortho")
338
+
339
+
340
+ class LayerNorm(nn.Module):
341
+ """
342
+ Wrapper class that moves the channel dimension to the end.
343
+
344
+ This module provides a layer normalization that works with channel-first
345
+ tensors by temporarily transposing the channel dimension to the end,
346
+ applying normalization, and then transposing back.
347
+
348
+ Parameters
349
+ ----------
350
+ in_channels : int
351
+ Number of input channels
352
+ eps : float, optional
353
+ Epsilon for numerical stability, by default 1e-05
354
+ elementwise_affine : bool, optional
355
+ Whether to use learnable affine parameters, by default True
356
+ bias : bool, optional
357
+ Whether to use bias, by default True
358
+ device : torch.device, optional
359
+ Device to place the module on, by default None
360
+ dtype : torch.dtype, optional
361
+ Data type for the module, by default None
362
+ """
363
+
364
+ def __init__(self, in_channels, eps=1e-05, elementwise_affine=True, bias=True, device=None, dtype=None):
365
+ super().__init__()
366
+
367
+ self.channel_dim = -3
368
+
369
+ self.norm = nn.LayerNorm(normalized_shape=in_channels, eps=1e-6, elementwise_affine=elementwise_affine, bias=bias, device=device, dtype=dtype)
370
+
371
+ def forward(self, x):
372
+
373
+ return self.norm(x.transpose(self.channel_dim, -1)).transpose(-1, self.channel_dim)
374
+
375
+
376
+ class SpectralConvS2(nn.Module):
377
+ """
378
+ Spectral Convolution according to Driscoll & Healy. Designed for convolutions on the two-sphere S2
379
+ using the Spherical Harmonic Transforms in torch-harmonics, but supports convolutions on the periodic
380
+ domain via the RealFFT2 and InverseRealFFT2 wrappers.
381
+
382
+ Parameters
383
+ ----------
384
+ forward_transform : nn.Module
385
+ Forward transform (SHT or FFT)
386
+ inverse_transform : nn.Module
387
+ Inverse transform (ISHT or IFFT)
388
+ in_channels : int
389
+ Number of input channels
390
+ out_channels : int
391
+ Number of output channels
392
+ gain : float, optional
393
+ Gain factor for weight initialization, by default 2.0
394
+ operator_type : str, optional
395
+ Type of spectral operator ("driscoll-healy", "diagonal", "block-diagonal"), by default "driscoll-healy"
396
+ lr_scale_exponent : int, optional
397
+ Learning rate scaling exponent, by default 0
398
+ bias : bool, optional
399
+ Whether to use bias, by default False
400
+ """
401
+
402
+ def __init__(self, forward_transform, inverse_transform, in_channels, out_channels, gain=2.0, operator_type="driscoll-healy", lr_scale_exponent=0, bias=False):
403
+ super().__init__()
404
+
405
+ self.forward_transform = forward_transform
406
+ self.inverse_transform = inverse_transform
407
+
408
+ self.modes_lat = self.inverse_transform.lmax
409
+ self.modes_lon = self.inverse_transform.mmax
410
+
411
+ self.scale_residual = (self.forward_transform.nlat != self.inverse_transform.nlat) or (self.forward_transform.nlon != self.inverse_transform.nlon)
412
+
413
+ # remember factorization details
414
+ self.operator_type = operator_type
415
+
416
+ assert self.inverse_transform.lmax == self.modes_lat
417
+ assert self.inverse_transform.mmax == self.modes_lon
418
+
419
+ weight_shape = [out_channels, in_channels]
420
+
421
+ if self.operator_type == "diagonal":
422
+ weight_shape += [self.modes_lat, self.modes_lon]
423
+ self.contract_func = "...ilm,oilm->...olm"
424
+ elif self.operator_type == "block-diagonal":
425
+ weight_shape += [self.modes_lat, self.modes_lon, self.modes_lon]
426
+ self.contract_func = "...ilm,oilnm->...oln"
427
+ elif self.operator_type == "driscoll-healy":
428
+ weight_shape += [self.modes_lat]
429
+ self.contract_func = "...ilm,oil->...olm"
430
+ else:
431
+ raise NotImplementedError(f"Unkonw operator type f{self.operator_type}")
432
+
433
+ # form weight tensors
434
+ scale = math.sqrt(gain / in_channels)
435
+ self.weight = nn.Parameter(scale * torch.randn(*weight_shape, dtype=torch.complex64))
436
+ if bias:
437
+ self.bias = nn.Parameter(torch.zeros(1, out_channels, 1, 1))
438
+
439
+ def forward(self, x):
440
+
441
+ dtype = x.dtype
442
+ x = x.float()
443
+ residual = x
444
+
445
+ with torch.autocast(device_type="cuda", enabled=False):
446
+ x = self.forward_transform(x)
447
+ if self.scale_residual:
448
+ residual = self.inverse_transform(x)
449
+
450
+ x = torch.einsum(self.contract_func, x, self.weight)
451
+
452
+ with torch.autocast(device_type="cuda", enabled=False):
453
+ x = self.inverse_transform(x)
454
+
455
+ if hasattr(self, "bias"):
456
+ x = x + self.bias
457
+ x = x.type(dtype)
458
+
459
+ return x, residual
460
+
461
+
462
+ class PositionEmbedding(nn.Module, metaclass=abc.ABCMeta):
463
+ """
464
+ Abstract base class for position embeddings.
465
+
466
+ This class defines the interface for position embedding modules
467
+ that add positional information to input tensors.
468
+
469
+ Parameters
470
+ ----------
471
+ img_shape : tuple, optional
472
+ Image shape (height, width), by default (480, 960)
473
+ grid : str, optional
474
+ Grid type, by default "equiangular"
475
+ num_chans : int, optional
476
+ Number of channels, by default 1
477
+ """
478
+
479
+ def __init__(self, img_shape=(480, 960), grid="equiangular", num_chans=1):
480
+ super().__init__()
481
+
482
+ self.img_shape = img_shape
483
+ self.num_chans = num_chans
484
+
485
+ def forward(self, x: torch.Tensor):
486
+
487
+ return x + self.position_embeddings
488
+
489
+
490
+ class SequencePositionEmbedding(PositionEmbedding):
491
+ """
492
+ Standard sequence-based position embedding.
493
+
494
+ This module implements sinusoidal position embeddings similar to those
495
+ used in the original Transformer paper, adapted for 2D spatial data.
496
+
497
+ Parameters
498
+ ----------
499
+ img_shape : tuple, optional
500
+ Image shape (height, width), by default (480, 960)
501
+ grid : str, optional
502
+ Grid type, by default "equiangular"
503
+ num_chans : int, optional
504
+ Number of channels, by default 1
505
+ """
506
+
507
+ def __init__(self, img_shape=(480, 960), grid="equiangular", num_chans=1):
508
+ super().__init__(img_shape=img_shape, grid=grid, num_chans=num_chans)
509
+
510
+ with torch.no_grad():
511
+ # alternating custom position embeddings
512
+ pos = torch.arange(self.img_shape[0] * self.img_shape[1]).reshape(1, 1, *self.img_shape).repeat(1, self.num_chans, 1, 1)
513
+ k = torch.arange(self.num_chans).reshape(1, self.num_chans, 1, 1)
514
+ denom = torch.pow(10000, 2 * k / self.num_chans)
515
+
516
+ pos_embed = torch.where(k % 2 == 0, torch.sin(pos / denom), torch.cos(pos / denom))
517
+
518
+ # register tensor
519
+ self.register_buffer("position_embeddings", pos_embed.float())
520
+
521
+
522
+ class SpectralPositionEmbedding(PositionEmbedding):
523
+ """
524
+ Spectral position embeddings for spherical transformers.
525
+
526
+ This module creates position embeddings in the spectral domain using
527
+ spherical harmonic functions, which are particularly suitable for
528
+ spherical data processing.
529
+
530
+ Parameters
531
+ -----------
532
+ img_shape : tuple, optional
533
+ Image shape (height, width), by default (480, 960)
534
+ grid : str, optional
535
+ Grid type, by default "equiangular"
536
+ num_chans : int, optional
537
+ Number of channels, by default 1
538
+ """
539
+
540
+ def __init__(self, img_shape=(480, 960), grid="equiangular", num_chans=1):
541
+ super().__init__(img_shape=img_shape, grid=grid, num_chans=num_chans)
542
+
543
+ # compute maximum required frequency and prepare isht
544
+ lmax = math.floor(math.sqrt(self.num_chans)) + 1
545
+ isht = InverseRealSHT(nlat=self.img_shape[0], nlon=self.img_shape[1], lmax=lmax, mmax=lmax, grid=grid)
546
+
547
+ # fill position embedding
548
+ with torch.no_grad():
549
+ pos_embed_freq = torch.zeros(1, self.num_chans, isht.lmax, isht.mmax, dtype=torch.complex64)
550
+
551
+ for i in range(self.num_chans):
552
+ l = math.floor(math.sqrt(i))
553
+ m = i - l**2 - l
554
+
555
+ if m < 0:
556
+ pos_embed_freq[0, i, l, -m] = 1.0j
557
+ else:
558
+ pos_embed_freq[0, i, l, m] = 1.0
559
+
560
+ # compute spatial position embeddings
561
+ pos_embed = isht(pos_embed_freq)
562
+
563
+ # normalization
564
+ pos_embed = pos_embed / torch.amax(pos_embed.abs(), dim=(-1, -2), keepdim=True)
565
+
566
+ # register tensor
567
+ self.register_buffer("position_embeddings", pos_embed)
568
+
569
+
570
+ class LearnablePositionEmbedding(PositionEmbedding):
571
+ """
572
+ Learnable position embeddings for spherical transformers.
573
+
574
+ This module provides learnable position embeddings that can be either
575
+ latitude-only or full latitude-longitude embeddings.
576
+
577
+ Parameters
578
+ ----------
579
+ img_shape : tuple, optional
580
+ Image shape (height, width), by default (480, 960)
581
+ grid : str, optional
582
+ Grid type, by default "equiangular"
583
+ num_chans : int, optional
584
+ Number of channels, by default 1
585
+ embed_type : str, optional
586
+ Embedding type ("lat" or "latlon"), by default "lat"
587
+ """
588
+
589
+ def __init__(self, img_shape=(480, 960), grid="equiangular", num_chans=1, embed_type="lat"):
590
+ super().__init__(img_shape=img_shape, grid=grid, num_chans=num_chans)
591
+
592
+ if embed_type == "latlon":
593
+ self.position_embeddings = nn.Parameter(torch.zeros(1, self.num_chans, self.img_shape[0], self.img_shape[1]))
594
+ elif embed_type == "lat":
595
+ self.position_embeddings = nn.Parameter(torch.zeros(1, self.num_chans, self.img_shape[0], 1))
596
+ else:
597
+ raise ValueError(f"Unknown learnable position embedding type {embed_type}")
598
+
599
+ # class SpiralPositionEmbedding(PositionEmbedding):
600
+ # """
601
+ # Returns position embeddings on the torus
602
+ # """
603
+
604
+ # def __init__(self, img_shape=(480, 960), grid="equiangular", num_chans=1):
605
+
606
+ # super().__init__(img_shape=img_shape, grid=grid, num_chans=num_chans)
607
+
608
+ # with torch.no_grad():
609
+
610
+ # # alternating custom position embeddings
611
+ # lats, _ = _precompute_latitudes(img_shape[0], grid=grid)
612
+ # lats = lats.reshape(-1, 1)
613
+ # lons = torch.linspace(0, 2 * math.pi, img_shape[1] + 1)[:-1]
614
+ # lons = lons.reshape(1, -1)
615
+
616
+ # # channel index
617
+ # k = torch.arange(self.num_chans).reshape(1, -1, 1, 1)
618
+ # pos_embed = torch.where(k % 2 == 0, torch.sin(k * (lons + lats)), torch.cos(k * (lons - lats)))
619
+
620
+ # # register tensor
621
+ # self.register_buffer("position_embeddings", pos_embed.float())
.deps/torch_harmonics/examples/models/lsno.py ADDED
@@ -0,0 +1,623 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2024 The torch-harmonics Authors. All rights reserved.
4
+ # SPDX-License-Identifier: BSD-3-Clause
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are met:
8
+ #
9
+ # 1. Redistributions of source code must retain the above copyright notice, this
10
+ # list of conditions and the following disclaimer.
11
+ #
12
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ # this list of conditions and the following disclaimer in the documentation
14
+ # and/or other materials provided with the distribution.
15
+ #
16
+ # 3. Neither the name of the copyright holder nor the names of its
17
+ # contributors may be used to endorse or promote products derived from
18
+ # this software without specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+ #
31
+
32
+ import math
33
+
34
+ import torch
35
+ import torch.nn as nn
36
+ import torch.amp as amp
37
+
38
+ from torch_harmonics import RealSHT, InverseRealSHT
39
+ from torch_harmonics import DiscreteContinuousConvS2, DiscreteContinuousConvTransposeS2
40
+ from torch_harmonics import ResampleS2
41
+
42
+ from torch_harmonics.examples.models._layers import MLP, SpectralConvS2, SequencePositionEmbedding, SpectralPositionEmbedding, LearnablePositionEmbedding
43
+
44
+ from functools import partial
45
+
46
+ # heuristic for finding theta_cutoff
47
+ def _compute_cutoff_radius(nlat, kernel_shape, basis_type):
48
+ theta_cutoff_factor = {"piecewise linear": 0.5, "morlet": 0.5, "zernike": math.sqrt(2.0)}
49
+
50
+ return (kernel_shape[0] + 1) * theta_cutoff_factor[basis_type] * math.pi / float(nlat - 1)
51
+
52
+ class DiscreteContinuousEncoder(nn.Module):
53
+ """
54
+ Discrete-continuous encoder for spherical neural operators.
55
+
56
+ This module performs downsampling using discrete-continuous convolutions on the sphere,
57
+ reducing the spatial resolution while maintaining the spectral properties of the data.
58
+
59
+ Parameters
60
+ ----------
61
+ in_shape : tuple, optional
62
+ Input shape (nlat, nlon), by default (721, 1440)
63
+ out_shape : tuple, optional
64
+ Output shape (nlat, nlon), by default (480, 960)
65
+ grid_in : str, optional
66
+ Input grid type, by default "equiangular"
67
+ grid_out : str, optional
68
+ Output grid type, by default "equiangular"
69
+ inp_chans : int, optional
70
+ Number of input channels, by default 2
71
+ out_chans : int, optional
72
+ Number of output channels, by default 2
73
+ kernel_shape : tuple, optional
74
+ Kernel shape for convolution, by default (3, 3)
75
+ basis_type : str, optional
76
+ Filter basis type, by default "morlet"
77
+ groups : int, optional
78
+ Number of groups for grouped convolution, by default 1
79
+ bias : bool, optional
80
+ Whether to use bias, by default False
81
+ """
82
+ def __init__(
83
+ self,
84
+ in_shape=(721, 1440),
85
+ out_shape=(480, 960),
86
+ grid_in="equiangular",
87
+ grid_out="equiangular",
88
+ inp_chans=2,
89
+ out_chans=2,
90
+ kernel_shape=(3, 3),
91
+ basis_type="morlet",
92
+ groups=1,
93
+ bias=False,
94
+ ):
95
+ super().__init__()
96
+
97
+ # set up local convolution
98
+ self.conv = DiscreteContinuousConvS2(
99
+ inp_chans,
100
+ out_chans,
101
+ in_shape=in_shape,
102
+ out_shape=out_shape,
103
+ kernel_shape=kernel_shape,
104
+ basis_type=basis_type,
105
+ grid_in=grid_in,
106
+ grid_out=grid_out,
107
+ groups=groups,
108
+ bias=bias,
109
+ theta_cutoff=_compute_cutoff_radius(in_shape[0], kernel_shape, basis_type),
110
+ )
111
+
112
+ def forward(self, x):
113
+
114
+ dtype = x.dtype
115
+
116
+ with amp.autocast(device_type="cuda", enabled=False):
117
+ x = x.float()
118
+ x = self.conv(x)
119
+ x = x.to(dtype=dtype)
120
+
121
+ return x
122
+
123
+
124
+ class DiscreteContinuousDecoder(nn.Module):
125
+ """
126
+ Discrete-continuous decoder for spherical neural operators.
127
+
128
+ This module performs upsampling using either spherical harmonic transforms or resampling,
129
+ followed by discrete-continuous convolutions to restore spatial resolution.
130
+
131
+ Parameters
132
+ ----------
133
+ in_shape : tuple, optional
134
+ Input shape (nlat, nlon), by default (480, 960)
135
+ out_shape : tuple, optional
136
+ Output shape (nlat, nlon), by default (721, 1440)
137
+ grid_in : str, optional
138
+ Input grid type, by default "equiangular"
139
+ grid_out : str, optional
140
+ Output grid type, by default "equiangular"
141
+ inp_chans : int, optional
142
+ Number of input channels, by default 2
143
+ out_chans : int, optional
144
+ Number of output channels, by default 2
145
+ kernel_shape : tuple, optional
146
+ Kernel shape for convolution, by default (3, 3)
147
+ basis_type : str, optional
148
+ Filter basis type, by default "morlet"
149
+ groups : int, optional
150
+ Number of groups for grouped convolution, by default 1
151
+ bias : bool, optional
152
+ Whether to use bias, by default False
153
+ upsample_sht : bool, optional
154
+ Whether to use SHT for upsampling, by default False
155
+ """
156
+ def __init__(
157
+ self,
158
+ in_shape=(480, 960),
159
+ out_shape=(721, 1440),
160
+ grid_in="equiangular",
161
+ grid_out="equiangular",
162
+ inp_chans=2,
163
+ out_chans=2,
164
+ kernel_shape=(3, 3),
165
+ basis_type="morlet",
166
+ groups=1,
167
+ bias=False,
168
+ upsample_sht=False,
169
+ ):
170
+ super().__init__()
171
+
172
+ # set up upsampling
173
+ if upsample_sht:
174
+ self.sht = RealSHT(*in_shape, grid=grid_in).float()
175
+ self.isht = InverseRealSHT(*out_shape, lmax=self.sht.lmax, mmax=self.sht.mmax, grid=grid_out).float()
176
+ self.upsample = nn.Sequential(self.sht, self.isht)
177
+ else:
178
+ self.upsample = ResampleS2(*in_shape, *out_shape, grid_in=grid_in, grid_out=grid_out)
179
+
180
+ # set up DISCO convolution
181
+ self.conv = DiscreteContinuousConvS2(
182
+ inp_chans,
183
+ out_chans,
184
+ in_shape=out_shape,
185
+ out_shape=out_shape,
186
+ kernel_shape=kernel_shape,
187
+ basis_type=basis_type,
188
+ grid_in=grid_out,
189
+ grid_out=grid_out,
190
+ groups=groups,
191
+ bias=False,
192
+ theta_cutoff=_compute_cutoff_radius(in_shape[0], kernel_shape, basis_type),
193
+ )
194
+
195
+ def forward(self, x):
196
+
197
+ dtype = x.dtype
198
+
199
+ with amp.autocast(device_type="cuda", enabled=False):
200
+ x = x.float()
201
+ x = self.upsample(x)
202
+ x = self.conv(x)
203
+ x = x.to(dtype=dtype)
204
+
205
+ return x
206
+
207
+
208
+ class SphericalNeuralOperatorBlock(nn.Module):
209
+ """
210
+ Helper module for a single SFNO/FNO block. Can use both FFTs and SHTs to represent either FNO or SFNO blocks.
211
+
212
+ Parameters
213
+ ----------
214
+ forward_transform : torch.nn.Module
215
+ Forward transform to use for the block
216
+ inverse_transform : torch.nn.Module
217
+ Inverse transform to use for the block
218
+ input_dim : int
219
+ Input dimension
220
+ output_dim : int
221
+ Output dimension
222
+ conv_type : str, optional
223
+ Type of convolution to use, by default "local"
224
+ mlp_ratio : float, optional
225
+ MLP expansion ratio, by default 2.0
226
+ drop_rate : float, optional
227
+ Dropout rate, by default 0.0
228
+ drop_path : float, optional
229
+ Drop path rate, by default 0.0
230
+ act_layer : torch.nn.Module, optional
231
+ Activation function to use, by default nn.GELU
232
+ norm_layer : str, optional
233
+ Type of normalization to use, by default "none"
234
+ inner_skip : str, optional
235
+ Type of inner skip connection to use, by default "none"
236
+ outer_skip : str, optional
237
+ Type of outer skip connection to use, by default "identity"
238
+ use_mlp : bool, optional
239
+ Whether to use MLP layers, by default True
240
+ disco_kernel_shape : tuple, optional
241
+ Kernel shape for discrete-continuous convolution, by default (3, 3)
242
+ disco_basis_type : str, optional
243
+ Filter basis type for discrete-continuous convolution, by default "morlet"
244
+ bias : bool, optional
245
+ Whether to use bias, by default False
246
+
247
+ Returns
248
+ -------
249
+ torch.Tensor
250
+ Output tensor
251
+ """
252
+
253
+ def __init__(
254
+ self,
255
+ forward_transform,
256
+ inverse_transform,
257
+ input_dim,
258
+ output_dim,
259
+ conv_type="local",
260
+ mlp_ratio=2.0,
261
+ drop_rate=0.0,
262
+ drop_path=0.0,
263
+ act_layer=nn.GELU,
264
+ norm_layer="none",
265
+ inner_skip="none",
266
+ outer_skip="identity",
267
+ use_mlp=True,
268
+ disco_kernel_shape=(3, 3),
269
+ disco_basis_type="morlet",
270
+ bias=False,
271
+ ):
272
+ super().__init__()
273
+
274
+ if act_layer == nn.Identity:
275
+ gain_factor = 1.0
276
+ else:
277
+ gain_factor = 2.0
278
+
279
+ if inner_skip == "linear" or inner_skip == "identity":
280
+ gain_factor /= 2.0
281
+
282
+ # convolution layer
283
+ if conv_type == "local":
284
+ theta_cutoff = 2.0 * _compute_cutoff_radius(forward_transform.nlat, disco_kernel_shape, disco_basis_type)
285
+ self.local_conv = DiscreteContinuousConvS2(
286
+ input_dim,
287
+ output_dim,
288
+ in_shape=(forward_transform.nlat, forward_transform.nlon),
289
+ out_shape=(inverse_transform.nlat, inverse_transform.nlon),
290
+ kernel_shape=disco_kernel_shape,
291
+ basis_type=disco_basis_type,
292
+ grid_in=forward_transform.grid,
293
+ grid_out=inverse_transform.grid,
294
+ bias=bias,
295
+ theta_cutoff=theta_cutoff,
296
+ )
297
+ elif conv_type == "global":
298
+ self.global_conv = SpectralConvS2(forward_transform, inverse_transform, input_dim, output_dim, gain=gain_factor, bias=bias)
299
+ else:
300
+ raise ValueError(f"Unknown convolution type {conv_type}")
301
+
302
+ if inner_skip == "linear":
303
+ self.inner_skip = nn.Conv2d(input_dim, output_dim, 1, 1)
304
+ nn.init.normal_(self.inner_skip.weight, std=math.sqrt(gain_factor / input_dim))
305
+ elif inner_skip == "identity":
306
+ assert input_dim == output_dim
307
+ self.inner_skip = nn.Identity()
308
+ elif inner_skip == "none":
309
+ pass
310
+ else:
311
+ raise ValueError(f"Unknown skip connection type {inner_skip}")
312
+
313
+ # normalisation layer
314
+ if norm_layer == "layer_norm":
315
+ self.norm = nn.LayerNorm(normalized_shape=(inverse_transform.nlat, inverse_transform.nlon), eps=1e-6)
316
+ elif norm_layer == "instance_norm":
317
+ self.norm = nn.InstanceNorm2d(num_features=output_dim, eps=1e-6, affine=True, track_running_stats=False)
318
+ elif norm_layer == "none":
319
+ self.norm = nn.Identity()
320
+ else:
321
+ raise NotImplementedError(f"Error, normalization {norm_layer} not implemented.")
322
+
323
+ # dropout
324
+ self.drop_path = DropPath(drop_path) if drop_path > 0.0 else nn.Identity()
325
+
326
+ gain_factor = 1.0
327
+ if outer_skip == "linear" or inner_skip == "identity":
328
+ gain_factor /= 2.0
329
+
330
+ if use_mlp == True:
331
+ mlp_hidden_dim = int(output_dim * mlp_ratio)
332
+ self.mlp = MLP(
333
+ in_features=output_dim,
334
+ out_features=input_dim,
335
+ hidden_features=mlp_hidden_dim,
336
+ act_layer=act_layer,
337
+ drop_rate=drop_rate,
338
+ checkpointing=False,
339
+ gain=gain_factor,
340
+ )
341
+
342
+ if outer_skip == "linear":
343
+ self.outer_skip = nn.Conv2d(input_dim, input_dim, 1, 1)
344
+ torch.nn.init.normal_(self.outer_skip.weight, std=math.sqrt(gain_factor / input_dim))
345
+ elif outer_skip == "identity":
346
+ assert input_dim == output_dim
347
+ self.outer_skip = nn.Identity()
348
+ elif outer_skip == "none":
349
+ pass
350
+ else:
351
+ raise ValueError(f"Unknown skip connection type {outer_skip}")
352
+
353
+ def forward(self, x):
354
+
355
+ residual = x
356
+
357
+ if hasattr(self, "global_conv"):
358
+ x, _ = self.global_conv(x)
359
+ elif hasattr(self, "local_conv"):
360
+ x = self.local_conv(x)
361
+
362
+ x = self.norm(x)
363
+
364
+ if hasattr(self, "inner_skip"):
365
+ x = x + self.inner_skip(residual)
366
+
367
+ if hasattr(self, "mlp"):
368
+ x = self.mlp(x)
369
+
370
+ x = self.drop_path(x)
371
+
372
+ if hasattr(self, "outer_skip"):
373
+ x = x + self.outer_skip(residual)
374
+
375
+ return x
376
+
377
+
378
+ class LocalSphericalNeuralOperator(nn.Module):
379
+ """
380
+ LocalSphericalNeuralOperator module. A spherical neural operator which uses both local and global integral
381
+ operators to accureately model both types of solution operators [1]. The architecture is based on the Spherical
382
+ Fourier Neural Operator [2] and improves upon it with local integral operators in both the Neural Operator blocks,
383
+ as well as in the encoder and decoders.
384
+
385
+ Parameters
386
+ ----------
387
+ img_size : tuple, optional
388
+ Input image size (nlat, nlon), by default (128, 256)
389
+ grid : str, optional
390
+ Grid type for input/output, by default "equiangular"
391
+ grid_internal : str, optional
392
+ Grid type for internal processing, by default "legendre-gauss"
393
+ scale_factor : int, optional
394
+ Scale factor for resolution changes, by default 3
395
+ in_chans : int, optional
396
+ Number of input channels, by default 3
397
+ out_chans : int, optional
398
+ Number of output channels, by default 3
399
+ embed_dim : int, optional
400
+ Embedding dimension, by default 256
401
+ num_layers : int, optional
402
+ Number of layers, by default 4
403
+ activation_function : str, optional
404
+ Activation function name, by default "gelu"
405
+ kernel_shape : tuple, optional
406
+ Kernel shape for convolutions, by default (3, 3)
407
+ encoder_kernel_shape : tuple, optional
408
+ Kernel shape for encoder, by default (3, 3)
409
+ filter_basis_type : str, optional
410
+ Filter basis type, by default "morlet"
411
+ use_mlp : bool, optional
412
+ Whether to use MLP layers, by default True
413
+ mlp_ratio : float, optional
414
+ MLP expansion ratio, by default 2.0
415
+ drop_rate : float, optional
416
+ Dropout rate, by default 0.0
417
+ drop_path_rate : float, optional
418
+ Drop path rate, by default 0.0
419
+ normalization_layer : str, optional
420
+ Type of normalization layer to use ("layer_norm", "instance_norm", "none"), by default "instance_norm"
421
+ sfno_block_frequency : int, optional
422
+ Frequency of SFNO blocks, by default 2
423
+ hard_thresholding_fraction : float, optional
424
+ Hard thresholding fraction, by default 1.0
425
+ residual_prediction : bool, optional
426
+ Whether to use residual prediction, by default False
427
+ pos_embed : str, optional
428
+ Position embedding type, by default "none"
429
+ upsample_sht : bool, optional
430
+ Use SHT upsampling if true, else linear interpolation
431
+ bias : bool, optional
432
+ Whether to use a bias, by default False
433
+
434
+ Example
435
+ ----------
436
+ >>> model = LocalSphericalNeuralOperator(
437
+ ... img_shape=(128, 256),
438
+ ... scale_factor=4,
439
+ ... in_chans=2,
440
+ ... out_chans=2,
441
+ ... embed_dim=16,
442
+ ... num_layers=4,
443
+ ... use_mlp=True,)
444
+ >>> model(torch.randn(1, 2, 128, 256)).shape
445
+ torch.Size([1, 2, 128, 256])
446
+
447
+ References
448
+ ----------
449
+ .. [1] Liu-Schiaffini M., Berner J., Bonev B., Kurth T., Azizzadenesheli K., Anandkumar A.;
450
+ "Neural Operators with Localized Integral and Differential Kernels" (2024).
451
+ ICML 2024, https://arxiv.org/pdf/2402.16845.
452
+
453
+ .. [2] Bonev B., Kurth T., Hundt C., Pathak, J., Baust M., Kashinath K., Anandkumar A.;
454
+ "Spherical Fourier Neural Operators: Learning Stable Dynamics on the Sphere" (2023).
455
+ ICML 2023, https://arxiv.org/abs/2306.03838.
456
+
457
+ """
458
+
459
+ def __init__(
460
+ self,
461
+ img_size=(128, 256),
462
+ grid="equiangular",
463
+ grid_internal="legendre-gauss",
464
+ scale_factor=3,
465
+ in_chans=3,
466
+ out_chans=3,
467
+ embed_dim=256,
468
+ num_layers=4,
469
+ activation_function="gelu",
470
+ kernel_shape=(3, 3),
471
+ encoder_kernel_shape=(3, 3),
472
+ filter_basis_type="morlet",
473
+ use_mlp=True,
474
+ mlp_ratio=2.0,
475
+ drop_rate=0.0,
476
+ drop_path_rate=0.0,
477
+ normalization_layer="none",
478
+ sfno_block_frequency=2,
479
+ hard_thresholding_fraction=1.0,
480
+ residual_prediction=False,
481
+ pos_embed="none",
482
+ upsample_sht=False,
483
+ bias=False,
484
+ ):
485
+ super().__init__()
486
+
487
+ self.img_size = img_size
488
+ self.grid = grid
489
+ self.grid_internal = grid_internal
490
+ self.scale_factor = scale_factor
491
+ self.in_chans = in_chans
492
+ self.out_chans = out_chans
493
+ self.embed_dim = embed_dim
494
+ self.num_layers = num_layers
495
+ self.encoder_kernel_shape = encoder_kernel_shape
496
+ self.hard_thresholding_fraction = hard_thresholding_fraction
497
+ self.normalization_layer = normalization_layer
498
+ self.use_mlp = use_mlp
499
+ self.residual_prediction = residual_prediction
500
+
501
+ # activation function
502
+ if activation_function == "relu":
503
+ self.activation_function = nn.ReLU
504
+ elif activation_function == "gelu":
505
+ self.activation_function = nn.GELU
506
+ # for debugging purposes
507
+ elif activation_function == "identity":
508
+ self.activation_function = nn.Identity
509
+ else:
510
+ raise ValueError(f"Unknown activation function {activation_function}")
511
+
512
+ # compute downsampled image size. We assume that the latitude-grid includes both poles
513
+ self.h = (self.img_size[0] - 1) // scale_factor + 1
514
+ self.w = self.img_size[1] // scale_factor
515
+
516
+ # dropout
517
+ self.pos_drop = nn.Dropout(p=drop_rate) if drop_rate > 0.0 else nn.Identity()
518
+ dpr = [x.item() for x in torch.linspace(0, drop_path_rate, self.num_layers)]
519
+
520
+ if pos_embed == "sequence":
521
+ self.pos_embed = SequencePositionEmbedding((self.h, self.w), num_chans=self.embed_dim, grid=grid_internal)
522
+ elif pos_embed == "spectral":
523
+ self.pos_embed = SpectralPositionEmbedding((self.h, self.w), num_chans=self.embed_dim, grid=grid_internal)
524
+ elif pos_embed == "learnable lat":
525
+ self.pos_embed = LearnablePositionEmbedding((self.h, self.w), num_chans=self.embed_dim, grid=grid_internal, embed_type="lat")
526
+ elif pos_embed == "learnable latlon":
527
+ self.pos_embed = LearnablePositionEmbedding((self.h, self.w), num_chans=self.embed_dim, grid=grid_internal, embed_type="latlon")
528
+ elif pos_embed == "none":
529
+ self.pos_embed = nn.Identity()
530
+ else:
531
+ raise ValueError(f"Unknown position embedding type {pos_embed}")
532
+
533
+ # encoder
534
+ self.encoder = DiscreteContinuousEncoder(
535
+ in_shape=self.img_size,
536
+ out_shape=(self.h, self.w),
537
+ grid_in=grid,
538
+ grid_out=grid_internal,
539
+ inp_chans=self.in_chans,
540
+ out_chans=self.embed_dim,
541
+ kernel_shape=self.encoder_kernel_shape,
542
+ basis_type=filter_basis_type,
543
+ groups=1,
544
+ bias=False,
545
+ )
546
+
547
+ # compute the modes for the sht
548
+ modes_lat = self.h
549
+ # due to some spectral artifacts with cufft, we substract one mode here
550
+ modes_lon = (self.w // 2 + 1) - 1
551
+
552
+ modes_lat = modes_lon = int(min(modes_lat, modes_lon) * self.hard_thresholding_fraction)
553
+
554
+ self.trans = RealSHT(self.h, self.w, lmax=modes_lat, mmax=modes_lon, grid=grid_internal).float()
555
+ self.itrans = InverseRealSHT(self.h, self.w, lmax=modes_lat, mmax=modes_lon, grid=grid_internal).float()
556
+
557
+ self.blocks = nn.ModuleList([])
558
+ for i in range(self.num_layers):
559
+
560
+ block = SphericalNeuralOperatorBlock(
561
+ self.trans,
562
+ self.itrans,
563
+ self.embed_dim,
564
+ self.embed_dim,
565
+ conv_type="global" if i % sfno_block_frequency == (sfno_block_frequency-1) else "local",
566
+ mlp_ratio=mlp_ratio,
567
+ drop_rate=drop_rate,
568
+ drop_path=dpr[i],
569
+ act_layer=self.activation_function,
570
+ norm_layer=self.normalization_layer,
571
+ use_mlp=use_mlp,
572
+ disco_kernel_shape=kernel_shape,
573
+ disco_basis_type=filter_basis_type,
574
+ bias=bias,
575
+ )
576
+
577
+ self.blocks.append(block)
578
+
579
+ # decoder
580
+ self.decoder = DiscreteContinuousDecoder(
581
+ in_shape=(self.h, self.w),
582
+ out_shape=self.img_size,
583
+ grid_in=grid_internal,
584
+ grid_out=grid,
585
+ inp_chans=self.embed_dim,
586
+ out_chans=self.out_chans,
587
+ kernel_shape=self.encoder_kernel_shape,
588
+ basis_type=filter_basis_type,
589
+ groups=1,
590
+ bias=False,
591
+ upsample_sht=upsample_sht,
592
+ )
593
+
594
+ @torch.jit.ignore
595
+ def no_weight_decay(self):
596
+ return {"pos_embed", "cls_token"}
597
+
598
+ def forward_features(self, x):
599
+ x = self.pos_drop(x)
600
+
601
+ for blk in self.blocks:
602
+ x = blk(x)
603
+
604
+ return x
605
+
606
+ def forward(self, x):
607
+
608
+ if self.residual_prediction:
609
+ residual = x
610
+
611
+ x = self.encoder(x)
612
+
613
+ if self.pos_embed is not None:
614
+ x = self.pos_embed(x)
615
+
616
+ x = self.forward_features(x)
617
+
618
+ x = self.decoder(x)
619
+
620
+ if self.residual_prediction:
621
+ x = x + residual
622
+
623
+ return x
.deps/torch_harmonics/examples/models/s2segformer.py ADDED
@@ -0,0 +1,810 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2025 The torch-harmonics Authors. All rights reserved.
4
+ # SPDX-License-Identifier: BSD-3-Clause
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are met:
8
+ #
9
+ # 1. Redistributions of source code must retain the above copyright notice, this
10
+ # list of conditions and the following disclaimer.
11
+ #
12
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ # this list of conditions and the following disclaimer in the documentation
14
+ # and/or other materials provided with the distribution.
15
+ #
16
+ # 3. Neither the name of the copyright holder nor the names of its
17
+ # contributors may be used to endorse or promote products derived from
18
+ # this software without specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+ #
31
+
32
+ import math
33
+
34
+ import torch
35
+ import torch.nn as nn
36
+ import torch.amp as amp
37
+
38
+ from torch_harmonics import DiscreteContinuousConvS2, DiscreteContinuousConvTransposeS2
39
+ from torch_harmonics import AttentionS2, NeighborhoodAttentionS2
40
+ from torch_harmonics import ResampleS2
41
+ from torch_harmonics import RealSHT, InverseRealSHT
42
+ from torch_harmonics.quadrature import _precompute_latitudes
43
+
44
+ from torch_harmonics.examples.models._layers import MLP, LayerNorm, DropPath
45
+
46
+ from functools import partial
47
+
48
+
49
+ # heuristic for finding theta_cutoff
50
+ def _compute_cutoff_radius(nlat, kernel_shape, basis_type):
51
+ theta_cutoff_factor = {"piecewise linear": 0.5, "morlet": 0.5, "zernike": math.sqrt(2.0)}
52
+
53
+ return (kernel_shape[0] + 1) * theta_cutoff_factor[basis_type] * math.pi / float(nlat - 1)
54
+
55
+
56
+ class OverlapPatchMerging(nn.Module):
57
+ """
58
+ Overlap patch merging module for spherical segformer.
59
+
60
+ This module performs patch merging with overlapping patches using discrete-continuous
61
+ convolutions on the sphere, followed by layer normalization.
62
+
63
+ Parameters
64
+ -----------
65
+ in_shape : tuple, optional
66
+ Input shape (nlat, nlon), by default (721, 1440)
67
+ out_shape : tuple, optional
68
+ Output shape (nlat, nlon), by default (481, 960)
69
+ grid_in : str, optional
70
+ Input grid type, by default "equiangular"
71
+ grid_out : str, optional
72
+ Output grid type, by default "equiangular"
73
+ in_channels : int, optional
74
+ Number of input channels, by default 3
75
+ out_channels : int, optional
76
+ Number of output channels, by default 64
77
+ kernel_shape : tuple, optional
78
+ Kernel shape for convolution, by default (3, 3)
79
+ basis_type : str, optional
80
+ Filter basis type, by default "morlet"
81
+ bias : bool, optional
82
+ Whether to use bias, by default False
83
+ """
84
+
85
+ def __init__(
86
+ self,
87
+ in_shape=(721, 1440),
88
+ out_shape=(481, 960),
89
+ grid_in="equiangular",
90
+ grid_out="equiangular",
91
+ in_channels=3,
92
+ out_channels=64,
93
+ kernel_shape=(3, 3),
94
+ basis_type="morlet",
95
+ bias=False,
96
+ ):
97
+ super().__init__()
98
+
99
+ # convolution for patches, curtoff radius inferred from kernel shape
100
+ theta_cutoff = _compute_cutoff_radius(out_shape[0], kernel_shape, basis_type)
101
+ self.conv = DiscreteContinuousConvS2(
102
+ in_channels,
103
+ out_channels,
104
+ in_shape=in_shape,
105
+ out_shape=out_shape,
106
+ kernel_shape=kernel_shape,
107
+ basis_type=basis_type,
108
+ grid_in=grid_in,
109
+ grid_out=grid_out,
110
+ bias=bias,
111
+ theta_cutoff=theta_cutoff,
112
+ )
113
+
114
+ # layer norm
115
+ self.norm = nn.LayerNorm((out_channels), eps=1e-05, elementwise_affine=True, bias=True)
116
+
117
+ self.apply(self._init_weights)
118
+
119
+ def _init_weights(self, m):
120
+
121
+ if isinstance(m, nn.LayerNorm):
122
+ nn.init.constant_(m.bias, 0)
123
+ nn.init.constant_(m.weight, 1.0)
124
+
125
+ def forward(self, x):
126
+
127
+ dtype = x.dtype
128
+
129
+ with amp.autocast(device_type="cuda", enabled=False):
130
+ x = x.float()
131
+ x = self.conv(x).to(dtype=dtype)
132
+
133
+ # permute
134
+ x = x.permute(0, 2, 3, 1)
135
+ x = self.norm(x)
136
+ out = x.permute(0, 3, 1, 2)
137
+
138
+ return out
139
+
140
+
141
+ class MixFFN(nn.Module):
142
+ """
143
+ Mix FFN module for spherical segformer.
144
+
145
+ This module implements a feed-forward network that combines MLP operations
146
+ with discrete-continuous convolutions on the sphere.
147
+
148
+ Parameters
149
+ -----------
150
+ shape : tuple
151
+ Shape (nlat, nlon) of the input
152
+ inout_channels : int
153
+ Number of input/output channels
154
+ hidden_channels : int
155
+ Number of hidden channels in MLP
156
+ mlp_bias : bool, optional
157
+ Whether to use bias in MLP, by default True
158
+ grid : str, optional
159
+ Grid type, by default "equiangular"
160
+ kernel_shape : tuple, optional
161
+ Kernel shape for convolution, by default (3, 3)
162
+ basis_type : str, optional
163
+ Filter basis type, by default "morlet"
164
+ conv_bias : bool, optional
165
+ Whether to use bias in convolution, by default False
166
+ activation : nn.Module, optional
167
+ Activation function, by default nn.GELU
168
+ use_mlp : bool, optional
169
+ Whether to use MLP instead of linear layers, by default False
170
+ drop_path : float, optional
171
+ Drop path rate, by default 0.0
172
+ """
173
+
174
+ def __init__(
175
+ self,
176
+ shape,
177
+ inout_channels,
178
+ hidden_channels,
179
+ mlp_bias=True,
180
+ grid="equiangular",
181
+ kernel_shape=(3, 3),
182
+ basis_type="morlet",
183
+ conv_bias=False,
184
+ activation=nn.GELU,
185
+ use_mlp=False,
186
+ drop_path=0.0,
187
+ ):
188
+ super().__init__()
189
+
190
+ self.drop_path = DropPath(drop_path) if drop_path > 0.0 else nn.Identity()
191
+
192
+ self.norm = nn.LayerNorm((inout_channels), eps=1e-05, elementwise_affine=True, bias=True)
193
+
194
+ if use_mlp:
195
+ # although the paper says MLP, it uses a single linear layer
196
+ self.mlp_in = MLP(inout_channels, hidden_features=hidden_channels, out_features=inout_channels, act_layer=activation, output_bias=False, drop_rate=0.0)
197
+ else:
198
+ self.mlp_in = nn.Conv2d(in_channels=inout_channels, out_channels=inout_channels, kernel_size=1, bias=True)
199
+
200
+ # convolution for patches, curtoff radius inferred from kernel shape
201
+ theta_cutoff = _compute_cutoff_radius(shape[0], kernel_shape, basis_type)
202
+ self.conv = DiscreteContinuousConvS2(
203
+ inout_channels,
204
+ inout_channels,
205
+ in_shape=shape,
206
+ out_shape=shape,
207
+ kernel_shape=kernel_shape,
208
+ basis_type=basis_type,
209
+ grid_in=grid,
210
+ grid_out=grid,
211
+ groups=inout_channels,
212
+ bias=conv_bias,
213
+ theta_cutoff=theta_cutoff,
214
+ )
215
+
216
+ if use_mlp:
217
+ self.mlp_out = MLP(inout_channels, hidden_features=hidden_channels, out_features=inout_channels, act_layer=activation, output_bias=False, drop_rate=0.0)
218
+ else:
219
+ self.mlp_out = nn.Conv2d(in_channels=inout_channels, out_channels=inout_channels, kernel_size=1, bias=True)
220
+
221
+ self.act = activation()
222
+
223
+ self.apply(self._init_weights)
224
+
225
+ def _init_weights(self, m):
226
+
227
+ if isinstance(m, nn.Conv2d):
228
+ nn.init.trunc_normal_(m.weight, std=0.02)
229
+ if m.bias is not None:
230
+ nn.init.constant_(m.bias, 0)
231
+ elif isinstance(m, nn.LayerNorm):
232
+ nn.init.constant_(m.bias, 0)
233
+ nn.init.constant_(m.weight, 1.0)
234
+
235
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
236
+ residual = x
237
+
238
+ # norm
239
+ x = x.permute(0, 2, 3, 1)
240
+ x = self.norm(x)
241
+ x = x.permute(0, 3, 1, 2)
242
+
243
+ # NOTE: we add another activation here
244
+ # because in the paper they only use depthwise conv,
245
+ # but without this activation it would just be a fused MM
246
+ # with the disco conv
247
+ x = self.mlp_in(x)
248
+
249
+ # conv parth
250
+ x = self.act(self.conv(x))
251
+
252
+ # second linear
253
+ x = self.mlp_out(x)
254
+
255
+ return residual + self.drop_path(x)
256
+
257
+
258
+ class AttentionWrapper(nn.Module):
259
+ """
260
+ Attention wrapper for spherical segformer.
261
+
262
+ This module wraps attention mechanisms (neighborhood or global) with optional
263
+ normalization and drop path regularization.
264
+
265
+ Parameters
266
+ -----------
267
+ channels : int
268
+ Number of channels
269
+ shape : tuple
270
+ Shape (nlat, nlon) of the input
271
+ grid : str
272
+ Grid type
273
+ heads : int
274
+ Number of attention heads
275
+ pre_norm : bool, optional
276
+ Whether to apply normalization before attention, by default False
277
+ attention_drop_rate : float, optional
278
+ Dropout rate for attention, by default 0.0
279
+ drop_path : float, optional
280
+ Drop path rate, by default 0.0
281
+ attention_mode : str, optional
282
+ Attention mode ("neighborhood" or "global"), by default "neighborhood"
283
+ theta_cutoff : float, optional
284
+ Cutoff radius for neighborhood attention, by default None
285
+ bias : bool, optional
286
+ Whether to use bias, by default True
287
+ """
288
+ def __init__(
289
+ self,
290
+ channels,
291
+ shape,
292
+ grid,
293
+ heads,
294
+ pre_norm=False,
295
+ attention_drop_rate=0.0,
296
+ drop_path=0.0,
297
+ attention_mode="neighborhood",
298
+ theta_cutoff=None,
299
+ bias=True
300
+ ):
301
+ super().__init__()
302
+
303
+ self.drop_path = DropPath(drop_path) if drop_path > 0.0 else nn.Identity()
304
+ self.attention_mode = attention_mode
305
+
306
+ if attention_mode == "neighborhood":
307
+ if theta_cutoff is None:
308
+ theta_cutoff = (7.0 / math.sqrt(math.pi)) * math.pi / (shape[0] - 1)
309
+ self.att = NeighborhoodAttentionS2(
310
+ in_channels=channels,
311
+ in_shape=shape,
312
+ out_shape=shape,
313
+ grid_in=grid,
314
+ grid_out=grid,
315
+ theta_cutoff=theta_cutoff,
316
+ out_channels=channels,
317
+ num_heads=heads,
318
+ bias=bias
319
+ # drop_rate=attention_drop_rate,
320
+ )
321
+ else:
322
+ self.att = AttentionS2(
323
+ in_channels=channels,
324
+ num_heads=heads,
325
+ in_shape=shape,
326
+ out_shape=shape,
327
+ grid_in=grid,
328
+ grid_out=grid,
329
+ out_channels=channels,
330
+ drop_rate=attention_drop_rate,
331
+ bias=bias
332
+ )
333
+
334
+ self.norm = None
335
+ if pre_norm:
336
+ self.norm = nn.LayerNorm((channels), eps=1e-05, elementwise_affine=True, bias=True)
337
+
338
+ self.apply(self._init_weights)
339
+
340
+ def _init_weights(self, m):
341
+ if isinstance(m, nn.LayerNorm):
342
+ nn.init.constant_(m.bias, 0)
343
+ nn.init.constant_(m.weight, 1.0)
344
+
345
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
346
+
347
+ residual = x
348
+ if self.norm is not None:
349
+ x = x.permute(0, 2, 3, 1)
350
+ x = self.norm(x)
351
+ x = x.permute(0, 3, 1, 2)
352
+
353
+ if self.attention_mode == "neighborhood":
354
+ dtype = x.dtype
355
+ with amp.autocast(device_type="cuda", enabled=False):
356
+ x = x.float()
357
+ x = self.att(x).to(dtype=dtype)
358
+ else:
359
+ x = self.att(x)
360
+
361
+ return residual + self.drop_path(x)
362
+
363
+
364
+ class TransformerBlock(nn.Module):
365
+ """
366
+ Transformer block for spherical segformer.
367
+
368
+ This block combines patch merging, attention, and Mix FFN operations
369
+ in a hierarchical structure for processing spherical data.
370
+
371
+ Parameters
372
+ -----------
373
+ in_shape : tuple
374
+ Input shape (nlat, nlon)
375
+ out_shape : tuple
376
+ Output shape (nlat, nlon)
377
+ in_channels : int
378
+ Number of input channels
379
+ out_channels : int
380
+ Number of output channels
381
+ mlp_hidden_channels : int
382
+ Number of hidden channels in MLP
383
+ grid_in : str, optional
384
+ Input grid type, by default "equiangular"
385
+ grid_out : str, optional
386
+ Output grid type, by default "equiangular"
387
+ nrep : int, optional
388
+ Number of repetitions, by default 1
389
+ heads : int, optional
390
+ Number of attention heads, by default 1
391
+ kernel_shape : tuple, optional
392
+ Kernel shape for convolution, by default (3, 3)
393
+ basis_type : str, optional
394
+ Filter basis type, by default "morlet"
395
+ activation : nn.Module, optional
396
+ Activation function, by default nn.GELU
397
+ att_drop_rate : float, optional
398
+ Dropout rate for attention, by default 0.0
399
+ drop_path_rates : float, optional
400
+ Drop path rates, by default 0.0
401
+ attention_mode : str, optional
402
+ Attention mode ("neighborhood" or "global"), by default "neighborhood"
403
+ theta_cutoff : float, optional
404
+ Cutoff radius for neighborhood attention, by default None
405
+ bias : bool, optional
406
+ Whether to use bias, by default True
407
+ """
408
+ def __init__(
409
+ self,
410
+ in_shape,
411
+ out_shape,
412
+ in_channels,
413
+ out_channels,
414
+ mlp_hidden_channels,
415
+ grid_in="equiangular",
416
+ grid_out="equiangular",
417
+ nrep=1,
418
+ heads=1,
419
+ kernel_shape=(3, 3),
420
+ basis_type="morlet",
421
+ activation=nn.GELU,
422
+ att_drop_rate=0.0,
423
+ drop_path_rates=0.0,
424
+ attention_mode="neighborhood",
425
+ theta_cutoff=None,
426
+ bias=True
427
+ ):
428
+ super().__init__()
429
+
430
+ self.in_shape = in_shape
431
+ self.out_shape = out_shape
432
+ self.in_channels = in_channels
433
+ self.out_channels = out_channels
434
+
435
+ if isinstance(drop_path_rates, float):
436
+ drop_path_rates = [x.item() for x in torch.linspace(0, drop_path_rates, nrep)]
437
+
438
+ assert len(drop_path_rates) == nrep
439
+
440
+ self.fwd = [
441
+ OverlapPatchMerging(
442
+ in_shape=in_shape,
443
+ out_shape=out_shape,
444
+ grid_in=grid_in,
445
+ grid_out=grid_out,
446
+ in_channels=in_channels,
447
+ out_channels=out_channels,
448
+ kernel_shape=kernel_shape,
449
+ basis_type=basis_type,
450
+ bias=False,
451
+ )
452
+ ]
453
+
454
+ for i in range(nrep):
455
+ self.fwd.append(
456
+ AttentionWrapper(
457
+ channels=out_channels,
458
+ shape=out_shape,
459
+ grid=grid_out,
460
+ heads=heads,
461
+ pre_norm=True,
462
+ attention_drop_rate=att_drop_rate,
463
+ drop_path=drop_path_rates[i],
464
+ attention_mode=attention_mode,
465
+ theta_cutoff=theta_cutoff,
466
+ bias=bias
467
+ )
468
+ )
469
+
470
+ self.fwd.append(
471
+ MixFFN(
472
+ out_shape,
473
+ inout_channels=out_channels,
474
+ hidden_channels=mlp_hidden_channels,
475
+ mlp_bias=True,
476
+ grid=grid_out,
477
+ kernel_shape=kernel_shape,
478
+ basis_type=basis_type,
479
+ conv_bias=False,
480
+ activation=activation,
481
+ use_mlp=False,
482
+ drop_path=drop_path_rates[i],
483
+ )
484
+ )
485
+
486
+ # make sequential
487
+ self.fwd = nn.Sequential(*self.fwd)
488
+
489
+ # final norm
490
+ self.norm = nn.LayerNorm((out_channels), eps=1e-05, elementwise_affine=True, bias=True)
491
+
492
+ self.apply(self._init_weights)
493
+
494
+ def _init_weights(self, m):
495
+ if isinstance(m, nn.LayerNorm):
496
+ nn.init.constant_(m.bias, 0)
497
+ nn.init.constant_(m.weight, 1.0)
498
+
499
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
500
+ x = self.fwd(x)
501
+
502
+ # apply norm
503
+ x = x.permute(0, 2, 3, 1)
504
+ x = self.norm(x)
505
+ x = x.permute(0, 3, 1, 2)
506
+
507
+ return x
508
+
509
+
510
+ class Upsampling(nn.Module):
511
+ """
512
+ Upsampling module for spherical segformer.
513
+
514
+ This module performs upsampling using either discrete-continuous transposed convolutions
515
+ or bilinear resampling on spherical data.
516
+
517
+ Parameters
518
+ -----------
519
+ in_shape : tuple
520
+ Input shape (nlat, nlon)
521
+ out_shape : tuple
522
+ Output shape (nlat, nlon)
523
+ in_channels : int
524
+ Number of input channels
525
+ out_channels : int
526
+ Number of output channels
527
+ hidden_channels : int
528
+ Number of hidden channels in MLP
529
+ mlp_bias : bool, optional
530
+ Whether to use bias in MLP, by default True
531
+ grid_in : str, optional
532
+ Input grid type, by default "equiangular"
533
+ grid_out : str, optional
534
+ Output grid type, by default "equiangular"
535
+ kernel_shape : tuple, optional
536
+ Kernel shape for convolution, by default (3, 3)
537
+ basis_type : str, optional
538
+ Filter basis type, by default "morlet"
539
+ conv_bias : bool, optional
540
+ Whether to use bias in convolution, by default False
541
+ activation : nn.Module, optional
542
+ Activation function, by default nn.GELU
543
+ use_mlp : bool, optional
544
+ Whether to use MLP instead of linear layers, by default False
545
+ upsampling_method : str, optional
546
+ Upsampling method ("conv" or "bilinear"), by default "conv"
547
+ """
548
+ def __init__(
549
+ self,
550
+ in_shape,
551
+ out_shape,
552
+ in_channels,
553
+ out_channels,
554
+ hidden_channels,
555
+ mlp_bias=True,
556
+ grid_in="equiangular",
557
+ grid_out="equiangular",
558
+ kernel_shape=(3, 3),
559
+ basis_type="morlet",
560
+ conv_bias=False,
561
+ activation=nn.GELU,
562
+ use_mlp=False,
563
+ upsampling_method="conv"
564
+ ):
565
+ super().__init__()
566
+
567
+ if use_mlp:
568
+ self.mlp = MLP(in_channels, hidden_features=hidden_channels, out_features=out_channels, act_layer=activation, output_bias=False, drop_rate=0.0)
569
+ else:
570
+ self.mlp = nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=1, bias=True)
571
+
572
+ if upsampling_method == "conv":
573
+ theta_cutoff = _compute_cutoff_radius(in_shape[0], kernel_shape, basis_type)
574
+ self.upsample = DiscreteContinuousConvTransposeS2(
575
+ out_channels,
576
+ out_channels,
577
+ in_shape=in_shape,
578
+ out_shape=out_shape,
579
+ kernel_shape=kernel_shape,
580
+ basis_type=basis_type,
581
+ grid_in=grid_in,
582
+ grid_out=grid_out,
583
+ bias=conv_bias,
584
+ theta_cutoff=theta_cutoff,
585
+ )
586
+ elif upsampling_method == "bilinear":
587
+ self.upsample = ResampleS2(*in_shape, *out_shape, grid_in=grid_in, grid_out=grid_out)
588
+ else:
589
+ raise ValueError(f"Unknown upsampling method {upsampling_method}")
590
+
591
+ self.apply(self._init_weights)
592
+
593
+ def _init_weights(self, m):
594
+ if isinstance(m, nn.Conv2d):
595
+ nn.init.trunc_normal_(m.weight, std=0.02)
596
+ if m.bias is not None:
597
+ nn.init.constant_(m.bias, 0)
598
+ elif isinstance(m, nn.LayerNorm):
599
+ nn.init.constant_(m.bias, 0)
600
+ nn.init.constant_(m.weight, 1.0)
601
+
602
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
603
+
604
+ x = self.upsample(self.mlp(x))
605
+
606
+ return x
607
+
608
+
609
+ class SphericalSegformer(nn.Module):
610
+ """
611
+ Spherical segformer model designed to approximate mappings from spherical signals to spherical segmentation masks
612
+
613
+ Parameters
614
+ -----------
615
+ img_shape : tuple, optional
616
+ Shape of the input channels, by default (128, 256)
617
+ kernel_shape: tuple, int
618
+ scale_factor: int, optional
619
+ Scale factor to use, by default 2
620
+ in_chans : int, optional
621
+ Number of input channels, by default 3
622
+ out_chans : int, optional
623
+ Number of classes, by default 3
624
+ embed_dims : List[int], optional
625
+ Dimension of the embeddings for each block, has to be the same length as heads
626
+ heads : List[int], optional
627
+ Number of heads for each block in the network, has to be the same length as embed_dims
628
+ depths: List[in], optional
629
+ Number of repetitions of attentions blocks and ffn mixers per layer. Has to be the same length as embed_dims and heads
630
+ activation_function : str, optional
631
+ Activation function to use, by default "gelu"
632
+ embedder_kernel_shape : int, optional
633
+ size of the encoder kernel
634
+ filter_basis_type: Optional[str]: str, optional
635
+ filter basis type
636
+ use_mlp : int, optional
637
+ Whether to use MLPs in the SFNO blocks, by default True
638
+ mlp_ratio : int, optional
639
+ Ratio of MLP to use, by default 2.0
640
+ drop_rate : float, optional
641
+ Dropout rate, by default 0.0
642
+ drop_path_rate : float, optional
643
+ Dropout path rate, by default 0.0
644
+ normalization_layer : str, optional
645
+ Type of normalization layer to use ("layer_norm", "instance_norm", "none"), by default "instance_norm"
646
+ hard_thresholding_fraction : float, optional
647
+ Fraction of hard thresholding (frequency cutoff) to apply, by default 1.0
648
+ upsampling_method : str
649
+ Conv, bilinear
650
+
651
+ Example
652
+ -----------
653
+ >>> model = SphericalTransformer(
654
+ ... img_shape=(128, 256),
655
+ ... scale_factor=4,
656
+ ... in_chans=2,
657
+ ... out_chans=2,
658
+ ... embed_dim=16,
659
+ ... num_layers=4,
660
+ ... use_mlp=True,)
661
+ >>> model(torch.randn(1, 2, 128, 256)).shape
662
+ torch.Size([1, 2, 128, 256])
663
+ """
664
+
665
+ def __init__(
666
+ self,
667
+ img_size=(128, 256),
668
+ grid="equiangular",
669
+ grid_internal="legendre-gauss",
670
+ in_chans=3,
671
+ out_chans=3,
672
+ embed_dims=[64, 128, 256, 512],
673
+ heads=[1, 2, 4, 8],
674
+ depths=[3, 4, 6, 3],
675
+ scale_factor=2,
676
+ activation_function="gelu",
677
+ kernel_shape=(3, 3),
678
+ filter_basis_type="morlet",
679
+ mlp_ratio=2.0,
680
+ att_drop_rate=0.0,
681
+ drop_path_rate=0.1,
682
+ attention_mode="neighborhood",
683
+ theta_cutoff=None,
684
+ upsampling_method="bilinear",
685
+ bias=True
686
+ ):
687
+ super().__init__()
688
+
689
+ self.img_size = img_size
690
+ self.grid = grid
691
+ self.grid_internal = grid_internal
692
+ self.in_chans = in_chans
693
+ self.out_chans = out_chans
694
+ self.embed_dims = embed_dims
695
+ self.heads = heads
696
+ self.num_blocks = len(self.embed_dims)
697
+ self.depths = depths
698
+ self.kernel_shape = kernel_shape
699
+
700
+ assert len(self.heads) == self.num_blocks
701
+ assert len(self.depths) == self.num_blocks
702
+
703
+ # activation function
704
+ if activation_function == "relu":
705
+ self.activation_function = nn.ReLU
706
+ elif activation_function == "gelu":
707
+ self.activation_function = nn.GELU
708
+ # for debugging purposes
709
+ elif activation_function == "identity":
710
+ self.activation_function = nn.Identity
711
+ else:
712
+ raise ValueError(f"Unknown activation function {activation_function}")
713
+
714
+ # set up drop path rates
715
+ dpr = [x.item() for x in torch.linspace(0, drop_path_rate, sum(self.depths))]
716
+
717
+ self.blocks = nn.ModuleList([])
718
+ out_shape = img_size
719
+ grid_in = grid
720
+ grid_out = grid_internal
721
+ in_channels = in_chans
722
+ cur = 0
723
+ for i in range(self.num_blocks):
724
+ out_shape_new = (out_shape[0] // scale_factor, out_shape[1] // scale_factor)
725
+ out_channels = self.embed_dims[i]
726
+ self.blocks.append(
727
+ TransformerBlock(
728
+ in_shape=out_shape,
729
+ out_shape=out_shape_new,
730
+ in_channels=in_channels,
731
+ out_channels=out_channels,
732
+ mlp_hidden_channels=int(mlp_ratio * out_channels),
733
+ grid_in=grid_in,
734
+ grid_out=grid_out,
735
+ nrep=self.depths[i],
736
+ heads=self.heads[i],
737
+ kernel_shape=kernel_shape,
738
+ basis_type=filter_basis_type,
739
+ activation=self.activation_function,
740
+ att_drop_rate=att_drop_rate,
741
+ drop_path_rates=dpr[cur : cur + self.depths[i]],
742
+ attention_mode=attention_mode,
743
+ theta_cutoff=theta_cutoff,
744
+ bias=bias
745
+ )
746
+ )
747
+ cur += self.depths[i]
748
+ out_shape = out_shape_new
749
+ grid_in = grid_internal
750
+ in_channels = out_channels
751
+
752
+ self.upsamplers = nn.ModuleList([])
753
+ out_shape = img_size
754
+ grid_out = grid
755
+ for i in range(self.num_blocks):
756
+ in_shape = self.blocks[i].out_shape
757
+ self.upsamplers.append(
758
+ Upsampling(
759
+ in_shape=in_shape,
760
+ out_shape=out_shape,
761
+ in_channels=self.embed_dims[i],
762
+ out_channels=self.embed_dims[i],
763
+ hidden_channels=int(mlp_ratio * self.embed_dims[i]),
764
+ mlp_bias=True,
765
+ grid_in=grid_internal,
766
+ grid_out=grid,
767
+ kernel_shape=kernel_shape,
768
+ basis_type=filter_basis_type,
769
+ conv_bias=False,
770
+ activation=nn.GELU,
771
+ upsampling_method=upsampling_method
772
+ )
773
+ )
774
+
775
+ segmentation_head_dim = sum(self.embed_dims)
776
+ self.segmentation_head = nn.Conv2d(in_channels=segmentation_head_dim, out_channels=out_chans, kernel_size=1, bias=True)
777
+
778
+ self.apply(self._init_weights)
779
+
780
+ def _init_weights(self, m):
781
+
782
+ if isinstance(m, nn.Conv2d):
783
+ nn.init.trunc_normal_(m.weight, std=0.02)
784
+ if m.bias is not None:
785
+ nn.init.constant_(m.bias, 0)
786
+ elif isinstance(m, nn.LayerNorm):
787
+ nn.init.constant_(m.bias, 0)
788
+ nn.init.constant_(m.weight, 1.0)
789
+
790
+ def forward(self, x):
791
+
792
+ # encoder:
793
+ features = []
794
+ feat = x
795
+ for block in self.blocks:
796
+ feat = block(feat)
797
+ features.append(feat)
798
+
799
+ # perform upsample
800
+ upfeats = []
801
+ for feat, upsampler in zip(features, self.upsamplers):
802
+ upfeats.append(upsampler(feat))
803
+
804
+ # perform concatenation
805
+ upfeats = torch.cat(upfeats, dim=1)
806
+
807
+ # final upsampling and prediction
808
+ out = self.segmentation_head(upfeats)
809
+
810
+ return out
.deps/torch_harmonics/examples/models/s2transformer.py ADDED
@@ -0,0 +1,589 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2025 The torch-harmonics Authors. All rights reserved.
4
+ # SPDX-License-Identifier: BSD-3-Clause
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are met:
8
+ #
9
+ # 1. Redistributions of source code must retain the above copyright notice, this
10
+ # list of conditions and the following disclaimer.
11
+ #
12
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ # this list of conditions and the following disclaimer in the documentation
14
+ # and/or other materials provided with the distribution.
15
+ #
16
+ # 3. Neither the name of the copyright holder nor the names of its
17
+ # contributors may be used to endorse or promote products derived from
18
+ # this software without specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+ #
31
+
32
+ import math
33
+
34
+ import torch
35
+ import torch.nn as nn
36
+ import torch.amp as amp
37
+
38
+ from torch_harmonics import DiscreteContinuousConvS2, DiscreteContinuousConvTransposeS2
39
+ from torch_harmonics import NeighborhoodAttentionS2, AttentionS2
40
+ from torch_harmonics import ResampleS2
41
+ from torch_harmonics import RealSHT, InverseRealSHT
42
+ from torch_harmonics.quadrature import _precompute_latitudes
43
+
44
+ from torch_harmonics.examples.models._layers import MLP, DropPath, LayerNorm, SequencePositionEmbedding, SpectralPositionEmbedding, LearnablePositionEmbedding
45
+
46
+ from functools import partial
47
+
48
+ # heuristic for finding theta_cutoff
49
+ def _compute_cutoff_radius(nlat, kernel_shape, basis_type):
50
+ theta_cutoff_factor = {"piecewise linear": 0.5, "morlet": 0.5, "zernike": math.sqrt(2.0)}
51
+
52
+ return (kernel_shape[0] + 1) * theta_cutoff_factor[basis_type] * math.pi / float(nlat - 1)
53
+
54
+ class DiscreteContinuousEncoder(nn.Module):
55
+ """
56
+ Discrete-continuous encoder for spherical transformers.
57
+
58
+ This module performs downsampling using discrete-continuous convolutions on the sphere,
59
+ reducing the spatial resolution while maintaining the spectral properties of the data.
60
+
61
+ Parameters
62
+ -----------
63
+ in_shape : tuple, optional
64
+ Input shape (nlat, nlon), by default (721, 1440)
65
+ out_shape : tuple, optional
66
+ Output shape (nlat, nlon), by default (480, 960)
67
+ grid_in : str, optional
68
+ Input grid type, by default "equiangular"
69
+ grid_out : str, optional
70
+ Output grid type, by default "equiangular"
71
+ in_chans : int, optional
72
+ Number of input channels, by default 2
73
+ out_chans : int, optional
74
+ Number of output channels, by default 2
75
+ kernel_shape : tuple, optional
76
+ Kernel shape for convolution, by default (3, 3)
77
+ basis_type : str, optional
78
+ Filter basis type, by default "morlet"
79
+ groups : int, optional
80
+ Number of groups for grouped convolution, by default 1
81
+ bias : bool, optional
82
+ Whether to use bias, by default False
83
+ """
84
+
85
+ def __init__(
86
+ self,
87
+ in_shape=(721, 1440),
88
+ out_shape=(480, 960),
89
+ grid_in="equiangular",
90
+ grid_out="equiangular",
91
+ in_chans=2,
92
+ out_chans=2,
93
+ kernel_shape=(3, 3),
94
+ basis_type="morlet",
95
+ groups=1,
96
+ bias=False,
97
+ ):
98
+ super().__init__()
99
+
100
+ # set up local convolution
101
+ self.conv = DiscreteContinuousConvS2(
102
+ in_chans,
103
+ out_chans,
104
+ in_shape=in_shape,
105
+ out_shape=out_shape,
106
+ kernel_shape=kernel_shape,
107
+ basis_type=basis_type,
108
+ grid_in=grid_in,
109
+ grid_out=grid_out,
110
+ groups=groups,
111
+ bias=bias,
112
+ theta_cutoff=_compute_cutoff_radius(in_shape[0], kernel_shape, basis_type),
113
+ )
114
+
115
+ def forward(self, x):
116
+
117
+ dtype = x.dtype
118
+
119
+ with amp.autocast(device_type="cuda", enabled=False):
120
+ x = x.float()
121
+ x = self.conv(x)
122
+ x = x.to(dtype=dtype)
123
+
124
+ return x
125
+
126
+
127
+ class DiscreteContinuousDecoder(nn.Module):
128
+ """
129
+ Discrete-continuous decoder for spherical transformers.
130
+
131
+ This module performs upsampling using either spherical harmonic transforms or resampling,
132
+ followed by discrete-continuous convolutions to restore spatial resolution.
133
+
134
+ Parameters
135
+ -----------
136
+ in_shape : tuple, optional
137
+ Input shape (nlat, nlon), by default (480, 960)
138
+ out_shape : tuple, optional
139
+ Output shape (nlat, nlon), by default (721, 1440)
140
+ grid_in : str, optional
141
+ Input grid type, by default "equiangular"
142
+ grid_out : str, optional
143
+ Output grid type, by default "equiangular"
144
+ in_chans : int, optional
145
+ Number of input channels, by default 2
146
+ out_chans : int, optional
147
+ Number of output channels, by default 2
148
+ kernel_shape : tuple, optional
149
+ Kernel shape for convolution, by default (3, 3)
150
+ basis_type : str, optional
151
+ Filter basis type, by default "morlet"
152
+ groups : int, optional
153
+ Number of groups for grouped convolution, by default 1
154
+ bias : bool, optional
155
+ Whether to use bias, by default False
156
+ upsample_sht : bool, optional
157
+ Whether to use SHT for upsampling, by default False
158
+ """
159
+
160
+ def __init__(
161
+ self,
162
+ in_shape=(480, 960),
163
+ out_shape=(721, 1440),
164
+ grid_in="equiangular",
165
+ grid_out="equiangular",
166
+ in_chans=2,
167
+ out_chans=2,
168
+ kernel_shape=(3, 3),
169
+ basis_type="morlet",
170
+ groups=1,
171
+ bias=False,
172
+ upsample_sht=False,
173
+ ):
174
+ super().__init__()
175
+
176
+ # set up upsampling
177
+ if upsample_sht:
178
+ self.sht = RealSHT(*in_shape, grid=grid_in).float()
179
+ self.isht = InverseRealSHT(*out_shape, lmax=self.sht.lmax, mmax=self.sht.mmax, grid=grid_out).float()
180
+ self.upsample = nn.Sequential(self.sht, self.isht)
181
+ else:
182
+ self.upsample = ResampleS2(*in_shape, *out_shape, grid_in=grid_in, grid_out=grid_out)
183
+
184
+ # set up DISCO convolution
185
+ self.conv = DiscreteContinuousConvS2(
186
+ in_chans,
187
+ out_chans,
188
+ in_shape=out_shape,
189
+ out_shape=out_shape,
190
+ kernel_shape=kernel_shape,
191
+ basis_type=basis_type,
192
+ grid_in=grid_out,
193
+ grid_out=grid_out,
194
+ groups=groups,
195
+ bias=False,
196
+ theta_cutoff=_compute_cutoff_radius(in_shape[0], kernel_shape, basis_type),
197
+ )
198
+
199
+ def forward(self, x):
200
+
201
+ dtype = x.dtype
202
+
203
+ with amp.autocast(device_type="cuda", enabled=False):
204
+ x = x.float()
205
+ x = self.upsample(x)
206
+ x = self.conv(x)
207
+ x = x.to(dtype=dtype)
208
+
209
+ return x
210
+
211
+
212
+ class SphericalAttentionBlock(nn.Module):
213
+ """
214
+ Spherical attention block for transformers on the sphere.
215
+
216
+ This module implements a single attention block that can use either global attention
217
+ or neighborhood attention on spherical data, followed by an optional MLP.
218
+
219
+ Parameters
220
+ -----------
221
+ in_shape : tuple, optional
222
+ Input shape (nlat, nlon), by default (480, 960)
223
+ out_shape : tuple, optional
224
+ Output shape (nlat, nlon), by default (480, 960)
225
+ grid_in : str, optional
226
+ Input grid type, by default "equiangular"
227
+ grid_out : str, optional
228
+ Output grid type, by default "equiangular"
229
+ in_chans : int, optional
230
+ Number of input channels, by default 2
231
+ out_chans : int, optional
232
+ Number of output channels, by default 2
233
+ num_heads : int, optional
234
+ Number of attention heads, by default 1
235
+ mlp_ratio : float, optional
236
+ Ratio of MLP hidden dimension to output dimension, by default 2.0
237
+ drop_rate : float, optional
238
+ Dropout rate, by default 0.0
239
+ drop_path : float, optional
240
+ Drop path rate, by default 0.0
241
+ act_layer : nn.Module, optional
242
+ Activation layer, by default nn.GELU
243
+ norm_layer : str, optional
244
+ Normalization layer type, by default "none"
245
+ use_mlp : bool, optional
246
+ Whether to use MLP after attention, by default True
247
+ bias : bool, optional
248
+ Whether to use bias, by default False
249
+ attention_mode : str, optional
250
+ Attention mode ("neighborhood" or "global"), by default "neighborhood"
251
+ theta_cutoff : float, optional
252
+ Cutoff radius for neighborhood attention, by default None
253
+ """
254
+
255
+ def __init__(
256
+ self,
257
+ in_shape=(480, 960),
258
+ out_shape=(480, 960),
259
+ grid_in="equiangular",
260
+ grid_out="equiangular",
261
+ in_chans=2,
262
+ out_chans=2,
263
+ num_heads=1,
264
+ mlp_ratio=2.0,
265
+ drop_rate=0.0,
266
+ drop_path=0.0,
267
+ act_layer=nn.GELU,
268
+ norm_layer="none",
269
+ use_mlp=True,
270
+ bias=False,
271
+ attention_mode="neighborhood",
272
+ theta_cutoff=None,
273
+ ):
274
+ super().__init__()
275
+
276
+ # normalisation layer
277
+ if norm_layer == "layer_norm":
278
+ self.norm0 = LayerNorm(in_channels=in_chans, eps=1e-6)
279
+ self.norm1 = LayerNorm(in_channels=out_chans, eps=1e-6)
280
+ elif norm_layer == "instance_norm":
281
+ self.norm0 = nn.InstanceNorm2d(num_features=in_chans, eps=1e-6, affine=True, track_running_stats=False)
282
+ self.norm1 = nn.InstanceNorm2d(num_features=out_chans, eps=1e-6, affine=True, track_running_stats=False)
283
+ elif norm_layer == "none":
284
+ self.norm0 = nn.Identity()
285
+ self.norm1 = nn.Identity()
286
+ else:
287
+ raise NotImplementedError(f"Error, normalization {norm_layer} not implemented.")
288
+
289
+ # determine radius for neighborhood attention
290
+ self.attention_mode = attention_mode
291
+ if attention_mode == "neighborhood":
292
+ if theta_cutoff is None:
293
+ theta_cutoff = (7.0 / math.sqrt(math.pi)) * math.pi / (in_shape[0] - 1)
294
+ self.self_attn = NeighborhoodAttentionS2(
295
+ in_channels=in_chans,
296
+ in_shape=in_shape,
297
+ out_shape=out_shape,
298
+ grid_in=grid_in,
299
+ grid_out=grid_out,
300
+ num_heads=num_heads,
301
+ theta_cutoff=theta_cutoff,
302
+ k_channels=None,
303
+ out_channels=out_chans,
304
+ bias=bias,
305
+ )
306
+ else:
307
+ self.self_attn = AttentionS2(
308
+ in_channels=in_chans,
309
+ num_heads=num_heads,
310
+ in_shape=in_shape,
311
+ out_shape=out_shape,
312
+ grid_in=grid_in,
313
+ grid_out=grid_out,
314
+ out_channels=out_chans,
315
+ drop_rate=drop_rate,
316
+ bias=bias,
317
+ )
318
+
319
+ self.skip0 = nn.Identity()
320
+
321
+ # dropout
322
+ self.drop_path = DropPath(drop_path) if drop_path > 0.0 else nn.Identity()
323
+
324
+ if use_mlp == True:
325
+ mlp_hidden_dim = int(out_chans * mlp_ratio)
326
+ self.mlp = MLP(
327
+ in_features=out_chans,
328
+ out_features=out_chans,
329
+ hidden_features=mlp_hidden_dim,
330
+ act_layer=act_layer,
331
+ drop_rate=drop_rate,
332
+ checkpointing=False,
333
+ gain=0.5,
334
+ )
335
+
336
+ self.skip1 = nn.Identity()
337
+
338
+ def forward(self, x):
339
+
340
+ residual = x
341
+
342
+ x = self.norm0(x)
343
+
344
+ if self.attention_mode == "neighborhood":
345
+ dtype = x.dtype
346
+ with amp.autocast(device_type="cuda", enabled=False):
347
+ x = x.float()
348
+ x = self.self_attn(x).to(dtype=dtype)
349
+ else:
350
+ x = self.self_attn(x)
351
+
352
+ if hasattr(self, "skip0"):
353
+ x = x + self.skip0(residual)
354
+
355
+ residual = x
356
+
357
+ x = self.norm1(x)
358
+
359
+ if hasattr(self, "mlp"):
360
+ x = self.mlp(x)
361
+
362
+ x = self.drop_path(x)
363
+
364
+ if hasattr(self, "skip1"):
365
+ x = x + self.skip1(residual)
366
+
367
+ return x
368
+
369
+
370
+ class SphericalTransformer(nn.Module):
371
+ """
372
+ Spherical transformer model designed to approximate mappings from spherical signals to spherical signals
373
+
374
+ Parameters
375
+ -----------
376
+ img_shape : tuple, optional
377
+ Shape of the input channels, by default (128, 256)
378
+ kernel_shape: tuple, int
379
+ scale_factor : int, optional
380
+ Scale factor to use, by default 3
381
+ in_chans : int, optional
382
+ Number of input channels, by default 3
383
+ out_chans : int, optional
384
+ Number of output channels, by default 3
385
+ embed_dim : int, optional
386
+ Dimension of the embeddings, by default 256
387
+ num_layers : int, optional
388
+ Number of layers in the network, by default 4
389
+ activation_function : str, optional
390
+ Activation function to use, by default "gelu"
391
+ encoder_kernel_shape : int, optional
392
+ size of the encoder kernel
393
+ filter_basis_type: str, optional
394
+ filter basis type
395
+ num_heads: int, optional
396
+ number of attention heads
397
+ use_mlp : int, optional
398
+ Whether to use MLPs in the SFNO blocks, by default True
399
+ mlp_ratio : int, optional
400
+ Ratio of MLP to use, by default 2.0
401
+ drop_rate : float, optional
402
+ Dropout rate, by default 0.0
403
+ drop_path_rate : float, optional
404
+ Dropout path rate, by default 0.0
405
+ normalization_layer : str, optional
406
+ Type of normalization layer to use ("layer_norm", "instance_norm", "none"), by default "instance_norm"
407
+ hard_thresholding_fraction : float, optional
408
+ Fraction of hard thresholding (frequency cutoff) to apply, by default 1.0
409
+ residual_prediction : bool, optional
410
+ Whether to add a single large skip connection, by default True
411
+ pos_embed : bool, optional
412
+ Whether to use positional embedding, by default True
413
+ upsample_sht : bool, optional
414
+ Use SHT upsampling if true, else linear interpolation
415
+ bias : bool, optional
416
+ Whether to use a bias, by default False
417
+
418
+ Example
419
+ -----------
420
+ >>> model = SphericalTransformer(
421
+ ... img_shape=(128, 256),
422
+ ... scale_factor=4,
423
+ ... in_chans=2,
424
+ ... out_chans=2,
425
+ ... embed_dim=16,
426
+ ... num_layers=4,
427
+ ... use_mlp=True,)
428
+ >>> model(torch.randn(1, 2, 128, 256)).shape
429
+ torch.Size([1, 2, 128, 256])
430
+ """
431
+
432
+ def __init__(
433
+ self,
434
+ img_size=(128, 256),
435
+ grid="equiangular",
436
+ grid_internal="legendre-gauss",
437
+ scale_factor=3,
438
+ in_chans=3,
439
+ out_chans=3,
440
+ embed_dim=256,
441
+ num_layers=4,
442
+ activation_function="gelu",
443
+ encoder_kernel_shape=(3, 3),
444
+ filter_basis_type="morlet",
445
+ num_heads=1,
446
+ use_mlp=True,
447
+ mlp_ratio=2.0,
448
+ drop_rate=0.0,
449
+ drop_path_rate=0.0,
450
+ normalization_layer="none",
451
+ hard_thresholding_fraction=1.0,
452
+ residual_prediction=False,
453
+ pos_embed="spectral",
454
+ upsample_sht=False,
455
+ attention_mode="neighborhood",
456
+ bias=False,
457
+ theta_cutoff=None,
458
+ ):
459
+ super().__init__()
460
+ self.img_size = img_size
461
+ self.grid = grid
462
+ self.grid_internal = grid_internal
463
+ self.scale_factor = scale_factor
464
+ self.in_chans = in_chans
465
+ self.out_chans = out_chans
466
+ self.embed_dim = embed_dim
467
+ self.num_layers = num_layers
468
+ self.encoder_kernel_shape = encoder_kernel_shape
469
+ self.hard_thresholding_fraction = hard_thresholding_fraction
470
+ self.normalization_layer = normalization_layer
471
+ self.use_mlp = use_mlp
472
+ self.residual_prediction = residual_prediction
473
+
474
+ # activation function
475
+ if activation_function == "relu":
476
+ self.activation_function = nn.ReLU
477
+ elif activation_function == "gelu":
478
+ self.activation_function = nn.GELU
479
+ # for debugging purposes
480
+ elif activation_function == "identity":
481
+ self.activation_function = nn.Identity
482
+ else:
483
+ raise ValueError(f"Unknown activation function {activation_function}")
484
+
485
+ # compute downsampled image size. We assume that the latitude-grid includes both poles
486
+ self.h = (self.img_size[0] - 1) // scale_factor + 1
487
+ self.w = self.img_size[1] // scale_factor
488
+
489
+ # dropout
490
+ self.pos_drop = nn.Dropout(p=drop_rate) if drop_rate > 0.0 else nn.Identity()
491
+ dpr = [x.item() for x in torch.linspace(0, drop_path_rate, self.num_layers)]
492
+
493
+ if pos_embed == "sequence":
494
+ self.pos_embed = SequencePositionEmbedding((self.h, self.w), num_chans=self.embed_dim, grid=grid_internal)
495
+ elif pos_embed == "spectral":
496
+ self.pos_embed = SpectralPositionEmbedding((self.h, self.w), num_chans=self.embed_dim, grid=grid_internal)
497
+ elif pos_embed == "learnable lat":
498
+ self.pos_embed = LearnablePositionEmbedding((self.h, self.w), num_chans=self.embed_dim, grid=grid_internal, embed_type="lat")
499
+ elif pos_embed == "learnable latlon":
500
+ self.pos_embed = LearnablePositionEmbedding((self.h, self.w), num_chans=self.embed_dim, grid=grid_internal, embed_type="latlon")
501
+ elif pos_embed == "none":
502
+ self.pos_embed = nn.Identity()
503
+ else:
504
+ raise ValueError(f"Unknown position embedding type {pos_embed}")
505
+
506
+ # maybe keep for now becuase tr
507
+ # encoder
508
+ self.encoder = DiscreteContinuousEncoder(
509
+ in_shape=self.img_size,
510
+ out_shape=(self.h, self.w),
511
+ grid_in=grid,
512
+ grid_out=grid_internal,
513
+ in_chans=self.in_chans,
514
+ out_chans=self.embed_dim,
515
+ kernel_shape=self.encoder_kernel_shape,
516
+ basis_type=filter_basis_type,
517
+ groups=1,
518
+ bias=False,
519
+ )
520
+
521
+ self.blocks = nn.ModuleList([])
522
+ for i in range(self.num_layers):
523
+ block = SphericalAttentionBlock(
524
+ in_shape=(self.h, self.w),
525
+ out_shape=(self.h, self.w),
526
+ grid_in=grid_internal,
527
+ grid_out=grid_internal,
528
+ in_chans=self.embed_dim,
529
+ out_chans=self.embed_dim,
530
+ num_heads=num_heads,
531
+ mlp_ratio=mlp_ratio,
532
+ drop_rate=drop_rate,
533
+ drop_path=dpr[i],
534
+ act_layer=self.activation_function,
535
+ norm_layer=self.normalization_layer,
536
+ use_mlp=use_mlp,
537
+ bias=bias,
538
+ attention_mode=attention_mode,
539
+ theta_cutoff=theta_cutoff,
540
+ )
541
+
542
+ self.blocks.append(block)
543
+
544
+ # decoder
545
+ self.decoder = DiscreteContinuousDecoder(
546
+ in_shape=(self.h, self.w),
547
+ out_shape=self.img_size,
548
+ grid_in=grid_internal,
549
+ grid_out=grid,
550
+ in_chans=self.embed_dim,
551
+ out_chans=self.out_chans,
552
+ kernel_shape=self.encoder_kernel_shape,
553
+ basis_type=filter_basis_type,
554
+ groups=1,
555
+ bias=False,
556
+ upsample_sht=upsample_sht,
557
+ )
558
+
559
+ @torch.jit.ignore
560
+ def no_weight_decay(self):
561
+ return {"pos_embed", "cls_token"}
562
+
563
+ def forward_features(self, x):
564
+ x = self.pos_drop(x)
565
+
566
+ for blk in self.blocks:
567
+ x = blk(x)
568
+
569
+ return x
570
+
571
+ def forward(self, x):
572
+
573
+ if self.residual_prediction:
574
+ residual = x
575
+
576
+ x = self.encoder(x)
577
+
578
+ if self.pos_embed is not None:
579
+ # x = x + self.pos_embed
580
+ x = self.pos_embed(x)
581
+
582
+ x = self.forward_features(x)
583
+
584
+ x = self.decoder(x)
585
+
586
+ if self.residual_prediction:
587
+ x = x + residual
588
+
589
+ return x
.deps/torch_harmonics/examples/models/s2unet.py ADDED
@@ -0,0 +1,610 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2025 The torch-harmonics Authors. All rights reserved.
4
+ # SPDX-License-Identifier: BSD-3-Clause
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are met:
8
+ #
9
+ # 1. Redistributions of source code must retain the above copyright notice, this
10
+ # list of conditions and the following disclaimer.
11
+ #
12
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ # this list of conditions and the following disclaimer in the documentation
14
+ # and/or other materials provided with the distribution.
15
+ #
16
+ # 3. Neither the name of the copyright holder nor the names of its
17
+ # contributors may be used to endorse or promote products derived from
18
+ # this software without specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+ #
31
+
32
+ import math
33
+
34
+ import torch
35
+ import torch.nn as nn
36
+ import torch.amp as amp
37
+
38
+ from torch_harmonics import DiscreteContinuousConvS2, DiscreteContinuousConvTransposeS2
39
+ from torch_harmonics import NeighborhoodAttentionS2
40
+ from torch_harmonics import ResampleS2
41
+ from torch_harmonics import RealSHT, InverseRealSHT
42
+ from torch_harmonics.quadrature import _precompute_latitudes
43
+
44
+ from torch_harmonics.examples.models._layers import MLP, DropPath
45
+
46
+ from functools import partial
47
+
48
+
49
+ # heuristic for finding theta_cutoff
50
+ def _compute_cutoff_radius(nlat, kernel_shape, basis_type):
51
+ theta_cutoff_factor = {"piecewise linear": 0.5, "morlet": 0.5, "zernike": math.sqrt(2.0)}
52
+
53
+ return (kernel_shape[0] + 1) * theta_cutoff_factor[basis_type] * math.pi / float(nlat - 1)
54
+
55
+
56
+ class DownsamplingBlock(nn.Module):
57
+ """
58
+ Downsampling block for spherical U-Net architecture.
59
+
60
+ This block performs convolution operations followed by downsampling on spherical data,
61
+ using discrete-continuous convolutions to maintain spectral properties.
62
+
63
+ Parameters
64
+ -----------
65
+ in_shape : tuple
66
+ Input shape (nlat, nlon)
67
+ out_shape : tuple
68
+ Output shape (nlat, nlon)
69
+ in_channels : int
70
+ Number of input channels
71
+ out_channels : int
72
+ Number of output channels
73
+ grid_in : str, optional
74
+ Input grid type, by default "equiangular"
75
+ grid_out : str, optional
76
+ Output grid type, by default "equiangular"
77
+ nrep : int, optional
78
+ Number of convolution repetitions, by default 1
79
+ kernel_shape : tuple, optional
80
+ Kernel shape for convolution, by default (3, 3)
81
+ basis_type : str, optional
82
+ Filter basis type, by default "morlet"
83
+ activation : nn.Module, optional
84
+ Activation function, by default nn.ReLU
85
+ transform_skip : bool, optional
86
+ Whether to transform skip connection, by default False
87
+ drop_conv_rate : float, optional
88
+ Dropout rate for convolutions, by default 0.0
89
+ drop_path_rate : float, optional
90
+ Drop path rate, by default 0.0
91
+ drop_dense_rate : float, optional
92
+ Dropout rate for dense layers, by default 0.0
93
+ downsampling_mode : str, optional
94
+ Downsampling mode ("bilinear", "conv"), by default "bilinear"
95
+ """
96
+
97
+ def __init__(
98
+ self,
99
+ in_shape,
100
+ out_shape,
101
+ in_channels,
102
+ out_channels,
103
+ grid_in="equiangular",
104
+ grid_out="equiangular",
105
+ nrep=1,
106
+ kernel_shape=(3, 3),
107
+ basis_type="morlet",
108
+ activation=nn.ReLU,
109
+ transform_skip=False,
110
+ drop_conv_rate=0.0,
111
+ drop_path_rate=0.0,
112
+ drop_dense_rate=0.0,
113
+ downsampling_mode="bilinear",
114
+ ):
115
+ super().__init__()
116
+
117
+ self.in_shape = in_shape
118
+ self.out_shape = out_shape
119
+ self.in_channels = in_channels
120
+ self.out_channels = out_channels
121
+ self.grid_in = grid_in
122
+ self.grid_out = grid_out
123
+
124
+ self.drop_path = DropPath(drop_path_rate) if drop_path_rate > 0.0 else nn.Identity()
125
+
126
+ self.fwd = []
127
+ for i in range(nrep):
128
+ # conv
129
+ theta_cutoff = _compute_cutoff_radius(in_shape[0], kernel_shape, basis_type)
130
+ self.fwd.append(
131
+ DiscreteContinuousConvS2(
132
+ in_channels=(in_channels if i == 0 else out_channels),
133
+ out_channels=out_channels,
134
+ in_shape=in_shape,
135
+ out_shape=in_shape,
136
+ kernel_shape=kernel_shape,
137
+ basis_type=basis_type,
138
+ grid_in=grid_out,
139
+ grid_out=grid_out,
140
+ bias=False,
141
+ theta_cutoff=theta_cutoff,
142
+ )
143
+ )
144
+
145
+ if drop_conv_rate > 0.0:
146
+ self.fwd.append(nn.Dropout2d(p=drop_conv_rate))
147
+
148
+ # batchnorm
149
+ self.fwd.append(nn.BatchNorm2d(out_channels, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True))
150
+
151
+ # activation
152
+ self.fwd.append(
153
+ activation(),
154
+ )
155
+
156
+ if downsampling_mode == "conv":
157
+ theta_cutoff = _compute_cutoff_radius(out_shape[0], kernel_shape, basis_type)
158
+ self.downsample = DiscreteContinuousConvS2(
159
+ out_channels,
160
+ out_channels,
161
+ in_shape=in_shape,
162
+ out_shape=out_shape,
163
+ kernel_shape=kernel_shape,
164
+ basis_type=basis_type,
165
+ grid_in=grid_in,
166
+ grid_out=grid_out,
167
+ bias=False,
168
+ theta_cutoff=theta_cutoff,
169
+ )
170
+ else:
171
+ self.downsample = ResampleS2(
172
+ nlat_in=in_shape[0],
173
+ nlon_in=in_shape[1],
174
+ nlat_out=out_shape[0],
175
+ nlon_out=out_shape[1],
176
+ grid_in=grid_in,
177
+ grid_out=grid_out,
178
+ mode=downsampling_mode,
179
+ )
180
+
181
+ # make sequential
182
+ self.fwd = nn.Sequential(*self.fwd)
183
+
184
+ # final norm
185
+ if transform_skip or (in_channels != out_channels):
186
+ self.transform_skip = nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=True)
187
+
188
+ if drop_dense_rate > 0.0:
189
+ self.transform_skip = nn.Sequential(
190
+ self.transform_skip,
191
+ nn.Dropout2d(p=drop_dense_rate),
192
+ )
193
+
194
+ self.apply(self._init_weights)
195
+
196
+ def _init_weights(self, m):
197
+
198
+ if isinstance(m, nn.Conv2d):
199
+ nn.init.trunc_normal_(m.weight, std=0.02)
200
+ if m.bias is not None:
201
+ nn.init.constant_(m.bias, 0)
202
+
203
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
204
+
205
+ # skip connection
206
+ residual = x
207
+ if hasattr(self, "transform_skip"):
208
+ residual = self.transform_skip(residual)
209
+
210
+ # main path
211
+ x = self.fwd(x)
212
+
213
+ # add residual connection
214
+ x = residual + self.drop_path(x)
215
+
216
+ # downsample
217
+ x = self.downsample(x)
218
+
219
+ return x
220
+
221
+
222
+ class UpsamplingBlock(nn.Module):
223
+ """
224
+ Upsampling block for spherical U-Net architecture.
225
+
226
+ This block performs upsampling followed by convolution operations on spherical data,
227
+ using discrete-continuous convolutions to maintain spectral properties.
228
+
229
+ Parameters
230
+ -----------
231
+ in_shape : tuple
232
+ Input shape (nlat, nlon)
233
+ out_shape : tuple
234
+ Output shape (nlat, nlon)
235
+ in_channels : int
236
+ Number of input channels
237
+ out_channels : int
238
+ Number of output channels
239
+ grid_in : str, optional
240
+ Input grid type, by default "equiangular"
241
+ grid_out : str, optional
242
+ Output grid type, by default "equiangular"
243
+ nrep : int, optional
244
+ Number of convolution repetitions, by default 1
245
+ kernel_shape : tuple, optional
246
+ Kernel shape for convolution, by default (3, 3)
247
+ basis_type : str, optional
248
+ Filter basis type, by default "morlet"
249
+ activation : nn.Module, optional
250
+ Activation function, by default nn.ReLU
251
+ transform_skip : bool, optional
252
+ Whether to transform skip connection, by default False
253
+ drop_conv_rate : float, optional
254
+ Dropout rate for convolutions, by default 0.0
255
+ drop_path_rate : float, optional
256
+ Drop path rate, by default 0.0
257
+ drop_dense_rate : float, optional
258
+ Dropout rate for dense layers, by default 0.0
259
+ upsampling_mode : str, optional
260
+ Upsampling mode ("bilinear", "conv"), by default "bilinear"
261
+ """
262
+
263
+ def __init__(
264
+ self,
265
+ in_shape,
266
+ out_shape,
267
+ in_channels,
268
+ out_channels,
269
+ grid_in="equiangular",
270
+ grid_out="equiangular",
271
+ nrep=1,
272
+ kernel_shape=(3, 3),
273
+ basis_type="morlet",
274
+ activation=nn.ReLU,
275
+ transform_skip=False,
276
+ drop_conv_rate=0.0,
277
+ drop_path_rate=0.0,
278
+ drop_dense_rate=0.0,
279
+ upsampling_mode="bilinear",
280
+ ):
281
+ super().__init__()
282
+
283
+ self.in_shape = in_shape
284
+ self.out_shape = out_shape
285
+ self.in_channels = in_channels
286
+ self.out_channels = out_channels
287
+
288
+ self.drop_path = DropPath(drop_path_rate) if drop_path_rate > 0.0 else nn.Identity()
289
+
290
+ if in_shape != out_shape:
291
+ if upsampling_mode == "conv":
292
+ theta_cutoff = _compute_cutoff_radius(in_shape[0], kernel_shape, basis_type)
293
+ self.upsample = nn.Sequential(
294
+ DiscreteContinuousConvTransposeS2(
295
+ in_channels=out_channels,
296
+ out_channels=out_channels,
297
+ in_shape=in_shape,
298
+ out_shape=out_shape,
299
+ kernel_shape=kernel_shape,
300
+ basis_type=basis_type,
301
+ grid_in=grid_in,
302
+ grid_out=grid_out,
303
+ bias=False,
304
+ theta_cutoff=theta_cutoff,
305
+ ),
306
+ nn.BatchNorm2d(out_channels, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True),
307
+ activation(),
308
+ DiscreteContinuousConvS2(
309
+ in_channels=out_channels,
310
+ out_channels=out_channels,
311
+ in_shape=out_shape,
312
+ out_shape=out_shape,
313
+ kernel_shape=kernel_shape,
314
+ basis_type=basis_type,
315
+ grid_in=grid_in,
316
+ grid_out=grid_out,
317
+ bias=False,
318
+ theta_cutoff=theta_cutoff,
319
+ ),
320
+ )
321
+
322
+ else:
323
+ self.upsample = ResampleS2(
324
+ nlat_in=in_shape[0],
325
+ nlon_in=in_shape[1],
326
+ nlat_out=out_shape[0],
327
+ nlon_out=out_shape[1],
328
+ grid_in=grid_in,
329
+ grid_out=grid_out,
330
+ mode=upsampling_mode,
331
+ )
332
+ else:
333
+ theta_cutoff = _compute_cutoff_radius(in_shape[0], kernel_shape, basis_type)
334
+ self.upsample = DiscreteContinuousConvS2(
335
+ in_channels=out_channels,
336
+ out_channels=out_channels,
337
+ in_shape=in_shape,
338
+ out_shape=in_shape,
339
+ kernel_shape=kernel_shape,
340
+ basis_type=basis_type,
341
+ grid_in=grid_in,
342
+ grid_out=grid_out,
343
+ bias=False,
344
+ theta_cutoff=theta_cutoff,
345
+ )
346
+
347
+ self.fwd = []
348
+ for i in range(nrep):
349
+ # conv
350
+ theta_cutoff = _compute_cutoff_radius(in_shape[0], kernel_shape, basis_type)
351
+ self.fwd.append(
352
+ DiscreteContinuousConvS2(
353
+ in_channels=in_channels,
354
+ out_channels=(out_channels if i == nrep - 1 else in_channels),
355
+ in_shape=in_shape,
356
+ out_shape=in_shape,
357
+ kernel_shape=kernel_shape,
358
+ basis_type=basis_type,
359
+ grid_in=grid_in,
360
+ grid_out=grid_in,
361
+ bias=False,
362
+ theta_cutoff=theta_cutoff,
363
+ )
364
+ )
365
+
366
+ if drop_conv_rate > 0.0:
367
+ self.fwd.append(nn.Dropout2d(p=drop_conv_rate))
368
+
369
+ # batchnorm
370
+ self.fwd.append(nn.BatchNorm2d((out_channels if i == nrep - 1 else in_channels), eps=1e-05, momentum=0.1, affine=True, track_running_stats=True))
371
+
372
+ # activation
373
+ self.fwd.append(
374
+ activation(),
375
+ )
376
+
377
+ # make sequential
378
+ self.fwd = nn.Sequential(*self.fwd)
379
+
380
+ # final norm
381
+ if transform_skip or (in_channels != out_channels):
382
+ self.transform_skip = nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=True)
383
+ if drop_dense_rate > 0.0:
384
+ self.transform_skip = nn.Sequential(
385
+ self.transform_skip,
386
+ nn.Dropout2d(p=drop_dense_rate),
387
+ )
388
+
389
+ self.apply(self._init_weights)
390
+
391
+ def _init_weights(self, m):
392
+ if isinstance(m, nn.Conv2d):
393
+ nn.init.trunc_normal_(m.weight, std=0.02)
394
+ if m.bias is not None:
395
+ nn.init.constant_(m.bias, 0)
396
+
397
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
398
+ # skip connection
399
+ residual = x
400
+ if hasattr(self, "transform_skip"):
401
+ residual = self.transform_skip(residual)
402
+
403
+ # main path
404
+ x = residual + self.drop_path(self.fwd(x))
405
+
406
+ # upsampling
407
+ x = self.upsample(x)
408
+
409
+ return x
410
+
411
+
412
+ class SphericalUNet(nn.Module):
413
+ """
414
+ Spherical segformer model designed to approximate mappings from spherical signals to spherical segmentation masks
415
+
416
+ Parameters
417
+ -----------
418
+ img_shape : tuple, optional
419
+ Shape of the input channels, by default (128, 256)
420
+ kernel_shape: tuple, int
421
+ scale_factor: int, optional
422
+ Scale factor to use, by default 2
423
+ in_chans : int, optional
424
+ Number of input channels, by default 3
425
+ out_chans : int, optional
426
+ Number of classes, by default 3
427
+ embed_dims : List[int], optional
428
+ Dimension of the embeddings for each block, has to be the same length as depths
429
+ depths: List[in], optional
430
+ Number of repetitions of conv blocks and ffn mixers per layer. Has to be the same length as embed_dims
431
+ activation_function : str, optional
432
+ Activation function to use, by default "relu"
433
+ embedder_kernel_shape : int, optional
434
+ size of the encoder kernel
435
+ filter_basis_type: Optional[str]: str, optional
436
+ filter basis type
437
+ use_mlp : int, optional
438
+ Whether to use MLPs in the SFNO blocks, by default True
439
+ mlp_ratio : int, optional
440
+ Ratio of MLP to use, by default 2.0
441
+ drop_rate : float, optional
442
+ Dropout rate, by default 0.0
443
+ drop_path_rate : float, optional
444
+ Dropout path rate, by default 0.0
445
+ normalization_layer : str, optional
446
+ Type of normalization layer to use ("layer_norm", "instance_norm", "none"), by default "instance_norm"
447
+ hard_thresholding_fraction : float, optional
448
+ Fraction of hard thresholding (frequency cutoff) to apply, by default 1.0
449
+ upsample_sht : bool, optional
450
+ Use SHT upsampling if true, else linear interpolation
451
+
452
+ Example
453
+ -----------
454
+ >>> model = SphericalTransformer(
455
+ ... img_shape=(128, 256),
456
+ ... scale_factor=4,
457
+ ... in_chans=2,
458
+ ... out_chans=2,
459
+ ... embed_dim=16,
460
+ ... num_layers=4,
461
+ ... use_mlp=True,)
462
+ >>> model(torch.randn(1, 2, 128, 256)).shape
463
+ torch.Size([1, 2, 128, 256])
464
+ """
465
+
466
+ def __init__(
467
+ self,
468
+ img_size=(128, 256),
469
+ grid="equiangular",
470
+ grid_internal="legendre-gauss",
471
+ in_chans=3,
472
+ out_chans=3,
473
+ embed_dims=[64, 128, 256, 512],
474
+ depths=[2, 2, 2, 2],
475
+ scale_factor=2,
476
+ activation_function="relu",
477
+ kernel_shape=(3, 3),
478
+ filter_basis_type="morlet",
479
+ transform_skip=False,
480
+ drop_conv_rate=0.1,
481
+ drop_path_rate=0.1,
482
+ drop_dense_rate=0.5,
483
+ downsampling_mode="bilinear",
484
+ upsampling_mode="bilinear",
485
+ ):
486
+ super().__init__()
487
+
488
+ self.img_size = img_size
489
+ self.grid = grid
490
+ self.grid_internal = grid_internal
491
+ self.in_chans = in_chans
492
+ self.out_chans = out_chans
493
+ self.embed_dims = embed_dims
494
+ self.num_blocks = len(self.embed_dims)
495
+ self.depths = depths
496
+ self.kernel_shape = kernel_shape
497
+
498
+ assert len(self.depths) == self.num_blocks
499
+
500
+ # activation function
501
+ if activation_function == "relu":
502
+ self.activation_function = nn.ReLU
503
+ elif activation_function == "gelu":
504
+ self.activation_function = nn.GELU
505
+ # for debugging purposes
506
+ elif activation_function == "identity":
507
+ self.activation_function = nn.Identity
508
+ else:
509
+ raise ValueError(f"Unknown activation function {activation_function}")
510
+
511
+ # set up drop path rates
512
+ dpr = [x.item() for x in torch.linspace(0, drop_path_rate, self.num_blocks)]
513
+
514
+ self.dblocks = nn.ModuleList([])
515
+ out_shape = img_size
516
+ grid_in = grid
517
+ grid_out = grid_internal
518
+ in_channels = in_chans
519
+ for i in range(self.num_blocks):
520
+ out_shape_new = (out_shape[0] // scale_factor, out_shape[1] // scale_factor)
521
+ out_channels = self.embed_dims[i]
522
+ self.dblocks.append(
523
+ DownsamplingBlock(
524
+ in_shape=out_shape,
525
+ out_shape=out_shape_new,
526
+ in_channels=in_channels,
527
+ out_channels=out_channels,
528
+ grid_in=grid_in,
529
+ grid_out=grid_out,
530
+ nrep=self.depths[i],
531
+ kernel_shape=kernel_shape,
532
+ basis_type=filter_basis_type,
533
+ activation=self.activation_function,
534
+ drop_conv_rate=drop_conv_rate,
535
+ drop_path_rate=dpr[i],
536
+ drop_dense_rate=drop_dense_rate,
537
+ transform_skip=transform_skip,
538
+ downsampling_mode=downsampling_mode,
539
+ )
540
+ )
541
+ out_shape = out_shape_new
542
+ grid_in = grid_internal
543
+ in_channels = out_channels
544
+
545
+ self.ublocks = nn.ModuleList([])
546
+ for i in range(self.num_blocks - 1, -1, -1):
547
+ in_shape = self.dblocks[i].out_shape
548
+ out_shape = self.dblocks[i].in_shape
549
+ in_channels = self.dblocks[i].out_channels
550
+ if i != self.num_blocks - 1:
551
+ in_channels = 2 * in_channels
552
+ out_channels = self.dblocks[i].in_channels
553
+ if i == 0:
554
+ out_channels = self.embed_dims[0]
555
+ grid_in = self.dblocks[i].grid_out
556
+ grid_out = self.dblocks[i].grid_in
557
+ self.ublocks.append(
558
+ UpsamplingBlock(
559
+ in_shape=in_shape,
560
+ out_shape=out_shape,
561
+ in_channels=in_channels,
562
+ out_channels=out_channels,
563
+ grid_in=grid_in,
564
+ grid_out=grid_out,
565
+ kernel_shape=kernel_shape,
566
+ basis_type=filter_basis_type,
567
+ activation=self.activation_function,
568
+ drop_conv_rate=drop_conv_rate,
569
+ drop_path_rate=0.0,
570
+ drop_dense_rate=drop_dense_rate,
571
+ transform_skip=transform_skip,
572
+ upsampling_mode=upsampling_mode,
573
+ )
574
+ )
575
+
576
+ self.head = nn.Conv2d(self.embed_dims[0], self.out_chans, kernel_size=1, bias=True)
577
+
578
+ self.apply(self._init_weights)
579
+
580
+ def _init_weights(self, m):
581
+
582
+ if isinstance(m, nn.Conv2d):
583
+ nn.init.trunc_normal_(m.weight, std=0.02)
584
+ if m.bias is not None:
585
+ nn.init.constant_(m.bias, 0)
586
+ elif isinstance(m, nn.LayerNorm):
587
+ nn.init.constant_(m.bias, 0)
588
+ nn.init.constant_(m.weight, 1.0)
589
+
590
+ def forward(self, x):
591
+
592
+ # encoder:
593
+ features = []
594
+ feat = x
595
+ for dblock in self.dblocks:
596
+ feat = dblock(feat)
597
+ features.append(feat)
598
+
599
+ # reverse list
600
+ features = features[::-1]
601
+
602
+ # perform upsample
603
+ ufeat = self.ublocks[0](features[0])
604
+ for feat, ublock in zip(features[1:], self.ublocks[1:]):
605
+ ufeat = ublock(torch.cat([feat, ufeat], dim=1))
606
+
607
+ # last layer
608
+ out = self.head(ufeat)
609
+
610
+ return out
.deps/torch_harmonics/examples/models/sfno.py ADDED
@@ -0,0 +1,423 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2022 The torch-harmonics Authors. All rights reserved.
4
+ # SPDX-License-Identifier: BSD-3-Clause
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are met:
8
+ #
9
+ # 1. Redistributions of source code must retain the above copyright notice, this
10
+ # list of conditions and the following disclaimer.
11
+ #
12
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ # this list of conditions and the following disclaimer in the documentation
14
+ # and/or other materials provided with the distribution.
15
+ #
16
+ # 3. Neither the name of the copyright holder nor the names of its
17
+ # contributors may be used to endorse or promote products derived from
18
+ # this software without specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+ #
31
+ import math
32
+
33
+ import torch
34
+ import torch.nn as nn
35
+
36
+ from torch_harmonics import RealSHT, InverseRealSHT
37
+
38
+ from torch_harmonics.examples.models._layers import MLP, SpectralConvS2, SequencePositionEmbedding, SpectralPositionEmbedding, LearnablePositionEmbedding
39
+
40
+ from functools import partial
41
+
42
+
43
+ class SphericalFourierNeuralOperatorBlock(nn.Module):
44
+ """
45
+ Helper module for a single SFNO/FNO block. Can use both FFTs and SHTs to represent either FNO or SFNO blocks.
46
+
47
+ Parameters
48
+ ----------
49
+ forward_transform : torch.nn.Module
50
+ Forward transform to use for the block
51
+ inverse_transform : torch.nn.Module
52
+ Inverse transform to use for the block
53
+ input_dim : int
54
+ Input dimension
55
+ output_dim : int
56
+ Output dimension
57
+ mlp_ratio : float, optional
58
+ MLP expansion ratio, by default 2.0
59
+ drop_rate : float, optional
60
+ Dropout rate, by default 0.0
61
+ drop_path : float, optional
62
+ Drop path rate, by default 0.0
63
+ act_layer : torch.nn.Module, optional
64
+ Activation function to use, by default nn.GELU
65
+ norm_layer : str, optional
66
+ Type of normalization to use, by default "none"
67
+ inner_skip : str, optional
68
+ Type of inner skip connection to use, by default "none"
69
+ outer_skip : str, optional
70
+ Type of outer skip connection to use, by default "identity"
71
+ use_mlp : bool, optional
72
+ Whether to use MLP layers, by default True
73
+ bias : bool, optional
74
+ Whether to use bias, by default False
75
+
76
+ Returns
77
+ -------
78
+ torch.Tensor
79
+ Output tensor
80
+ """
81
+
82
+ def __init__(
83
+ self,
84
+ forward_transform,
85
+ inverse_transform,
86
+ input_dim,
87
+ output_dim,
88
+ mlp_ratio=2.0,
89
+ drop_rate=0.0,
90
+ drop_path=0.0,
91
+ act_layer=nn.GELU,
92
+ norm_layer="none",
93
+ inner_skip="none",
94
+ outer_skip="identity",
95
+ use_mlp=True,
96
+ bias=False,
97
+ ):
98
+ super().__init__()
99
+
100
+ if act_layer == nn.Identity:
101
+ gain_factor = 1.0
102
+ else:
103
+ gain_factor = 2.0
104
+
105
+ if inner_skip == "linear" or inner_skip == "identity":
106
+ gain_factor /= 2.0
107
+
108
+ self.global_conv = SpectralConvS2(forward_transform, inverse_transform, input_dim, output_dim, gain=gain_factor, bias=bias)
109
+
110
+ if inner_skip == "linear":
111
+ self.inner_skip = nn.Conv2d(input_dim, output_dim, 1, 1)
112
+ nn.init.normal_(self.inner_skip.weight, std=math.sqrt(gain_factor / input_dim))
113
+ elif inner_skip == "identity":
114
+ assert input_dim == output_dim
115
+ self.inner_skip = nn.Identity()
116
+ elif inner_skip == "none":
117
+ pass
118
+ else:
119
+ raise ValueError(f"Unknown skip connection type {inner_skip}")
120
+
121
+ # normalisation layer
122
+ if norm_layer == "layer_norm":
123
+ self.norm = nn.LayerNorm(normalized_shape=(inverse_transform.nlat, inverse_transform.nlon), eps=1e-6)
124
+ elif norm_layer == "instance_norm":
125
+ self.norm = nn.InstanceNorm2d(num_features=output_dim, eps=1e-6, affine=True, track_running_stats=False)
126
+ elif norm_layer == "none":
127
+ self.norm = nn.Identity()
128
+ else:
129
+ raise NotImplementedError(f"Error, normalization {self.norm_layer} not implemented.")
130
+
131
+ # dropout
132
+ self.drop_path = DropPath(drop_path) if drop_path > 0.0 else nn.Identity()
133
+
134
+ gain_factor = 1.0
135
+ if outer_skip == "linear" or inner_skip == "identity":
136
+ gain_factor /= 2.0
137
+
138
+ if use_mlp == True:
139
+ mlp_hidden_dim = int(output_dim * mlp_ratio)
140
+ self.mlp = MLP(
141
+ in_features=output_dim, out_features=input_dim, hidden_features=mlp_hidden_dim, act_layer=act_layer, drop_rate=drop_rate, checkpointing=False, gain=gain_factor
142
+ )
143
+
144
+ if outer_skip == "linear":
145
+ self.outer_skip = nn.Conv2d(input_dim, input_dim, 1, 1)
146
+ torch.nn.init.normal_(self.outer_skip.weight, std=math.sqrt(gain_factor / input_dim))
147
+ elif outer_skip == "identity":
148
+ assert input_dim == output_dim
149
+ self.outer_skip = nn.Identity()
150
+ elif outer_skip == "none":
151
+ pass
152
+ else:
153
+ raise ValueError(f"Unknown skip connection type {outer_skip}")
154
+
155
+ def forward(self, x):
156
+
157
+ x, residual = self.global_conv(x)
158
+
159
+ x = self.norm(x)
160
+
161
+ if hasattr(self, "inner_skip"):
162
+ x = x + self.inner_skip(residual)
163
+
164
+ if hasattr(self, "mlp"):
165
+ x = self.mlp(x)
166
+
167
+ x = self.drop_path(x)
168
+
169
+ if hasattr(self, "outer_skip"):
170
+ x = x + self.outer_skip(residual)
171
+
172
+ return x
173
+
174
+
175
+ class SphericalFourierNeuralOperator(nn.Module):
176
+ """
177
+ SphericalFourierNeuralOperator module. Implements the 'linear' variant of the Spherical Fourier Neural Operator
178
+ as presented in [1]. Spherical convolutions are applied via spectral transforms to apply a geometrically consistent
179
+ and approximately equivariant architecture.
180
+
181
+ Parameters
182
+ ----------
183
+ img_size : tuple, optional
184
+ Shape of the input channels, by default (128, 256)
185
+ grid : str, optional
186
+ Input grid type, by default "equiangular"
187
+ grid_internal : str, optional
188
+ Internal grid type for computations, by default "legendre-gauss"
189
+ scale_factor : int, optional
190
+ Scale factor to use, by default 3
191
+ in_chans : int, optional
192
+ Number of input channels, by default 3
193
+ out_chans : int, optional
194
+ Number of output channels, by default 3
195
+ embed_dim : int, optional
196
+ Dimension of the embeddings, by default 256
197
+ num_layers : int, optional
198
+ Number of layers in the network, by default 4
199
+ activation_function : str, optional
200
+ Activation function to use, by default "gelu"
201
+ encoder_layers : int, optional
202
+ Number of layers in the encoder, by default 1
203
+ use_mlp : int, optional
204
+ Whether to use MLPs in the SFNO blocks, by default True
205
+ mlp_ratio : int, optional
206
+ Ratio of MLP to use, by default 2.0
207
+ drop_rate : float, optional
208
+ Dropout rate, by default 0.0
209
+ drop_path_rate : float, optional
210
+ Dropout path rate, by default 0.0
211
+ normalization_layer : str, optional
212
+ Type of normalization layer to use ("layer_norm", "instance_norm", "none"), by default "none"
213
+ hard_thresholding_fraction : float, optional
214
+ Fraction of hard thresholding (frequency cutoff) to apply, by default 1.0
215
+ residual_prediction : bool, optional
216
+ Whether to add a single large skip connection, by default False
217
+ pos_embed : str, optional
218
+ Type of positional embedding to use, by default "none"
219
+ bias : bool, optional
220
+ Whether to use a bias, by default False
221
+
222
+ Example:
223
+ ----------
224
+ >>> model = SphericalFourierNeuralOperator(
225
+ ... img_size=(128, 256),
226
+ ... scale_factor=4,
227
+ ... in_chans=2,
228
+ ... out_chans=2,
229
+ ... embed_dim=16,
230
+ ... num_layers=4,
231
+ ... use_mlp=True,)
232
+ >>> model(torch.randn(1, 2, 128, 256)).shape
233
+ torch.Size([1, 2, 128, 256])
234
+
235
+ References
236
+ ----------
237
+ .. [1] Bonev B., Kurth T., Hundt C., Pathak, J., Baust M., Kashinath K., Anandkumar A.;
238
+ "Spherical Fourier Neural Operators: Learning Stable Dynamics on the Sphere" (2023).
239
+ ICML 2023, https://arxiv.org/abs/2306.03838.
240
+ """
241
+
242
+ def __init__(
243
+ self,
244
+ img_size=(128, 256),
245
+ grid="equiangular",
246
+ grid_internal="legendre-gauss",
247
+ scale_factor=3,
248
+ in_chans=3,
249
+ out_chans=3,
250
+ embed_dim=256,
251
+ num_layers=4,
252
+ activation_function="gelu",
253
+ encoder_layers=1,
254
+ use_mlp=True,
255
+ mlp_ratio=2.0,
256
+ drop_rate=0.0,
257
+ drop_path_rate=0.0,
258
+ normalization_layer="none",
259
+ hard_thresholding_fraction=1.0,
260
+ residual_prediction=False,
261
+ pos_embed="none",
262
+ bias=False,
263
+ ):
264
+
265
+ super().__init__()
266
+
267
+ self.img_size = img_size
268
+ self.grid = grid
269
+ self.grid_internal = grid_internal
270
+ self.scale_factor = scale_factor
271
+ self.in_chans = in_chans
272
+ self.out_chans = out_chans
273
+ self.embed_dim = embed_dim
274
+ self.num_layers = num_layers
275
+ self.hard_thresholding_fraction = hard_thresholding_fraction
276
+ self.normalization_layer = normalization_layer
277
+ self.use_mlp = use_mlp
278
+ self.encoder_layers = encoder_layers
279
+ self.residual_prediction = residual_prediction
280
+
281
+ # activation function
282
+ if activation_function == "relu":
283
+ self.activation_function = nn.ReLU
284
+ elif activation_function == "gelu":
285
+ self.activation_function = nn.GELU
286
+ # for debugging purposes
287
+ elif activation_function == "identity":
288
+ self.activation_function = nn.Identity
289
+ else:
290
+ raise ValueError(f"Unknown activation function {activation_function}")
291
+
292
+ # compute downsampled image size. We assume that the latitude-grid includes both poles
293
+ self.h = (self.img_size[0] - 1) // scale_factor + 1
294
+ self.w = self.img_size[1] // scale_factor
295
+
296
+ # dropout
297
+ self.pos_drop = nn.Dropout(p=drop_rate) if drop_rate > 0.0 else nn.Identity()
298
+ dpr = [x.item() for x in torch.linspace(0, drop_path_rate, self.num_layers)]
299
+
300
+ if pos_embed == "sequence":
301
+ self.pos_embed = SequencePositionEmbedding((self.h, self.w), num_chans=self.embed_dim, grid=grid_internal)
302
+ elif pos_embed == "spectral":
303
+ self.pos_embed = SpectralPositionEmbedding((self.h, self.w), num_chans=self.embed_dim, grid=grid_internal)
304
+ elif pos_embed == "learnable lat":
305
+ self.pos_embed = LearnablePositionEmbedding((self.h, self.w), num_chans=self.embed_dim, grid=grid_internal, embed_type="lat")
306
+ elif pos_embed == "learnable latlon":
307
+ self.pos_embed = LearnablePositionEmbedding((self.h, self.w), num_chans=self.embed_dim, grid=grid_internal, embed_type="latlon")
308
+ elif pos_embed == "none":
309
+ self.pos_embed = nn.Identity()
310
+ else:
311
+ raise ValueError(f"Unknown position embedding type {pos_embed}")
312
+
313
+ # construct an encoder with num_encoder_layers
314
+ num_encoder_layers = 1
315
+ encoder_hidden_dim = int(self.embed_dim * mlp_ratio)
316
+ current_dim = self.in_chans
317
+ encoder_layers = []
318
+ for l in range(num_encoder_layers - 1):
319
+ fc = nn.Conv2d(current_dim, encoder_hidden_dim, 1, bias=True)
320
+ # initialize the weights correctly
321
+ scale = math.sqrt(2.0 / current_dim)
322
+ nn.init.normal_(fc.weight, mean=0.0, std=scale)
323
+ if fc.bias is not None:
324
+ nn.init.constant_(fc.bias, 0.0)
325
+ encoder_layers.append(fc)
326
+ encoder_layers.append(self.activation_function())
327
+ current_dim = encoder_hidden_dim
328
+ fc = nn.Conv2d(current_dim, self.embed_dim, 1, bias=bias)
329
+ scale = math.sqrt(1.0 / current_dim)
330
+ nn.init.normal_(fc.weight, mean=0.0, std=scale)
331
+ if fc.bias is not None:
332
+ nn.init.constant_(fc.bias, 0.0)
333
+ encoder_layers.append(fc)
334
+ self.encoder = nn.Sequential(*encoder_layers)
335
+
336
+ # compute the modes for the sht
337
+ modes_lat = self.h
338
+ # due to some spectral artifacts with cufft, we substract one mode here
339
+ modes_lon = (self.w // 2 + 1) - 1
340
+
341
+ modes_lat = modes_lon = int(min(modes_lat, modes_lon) * self.hard_thresholding_fraction)
342
+
343
+ self.trans_down = RealSHT(*self.img_size, lmax=modes_lat, mmax=modes_lon, grid=self.grid).float()
344
+ self.itrans_up = InverseRealSHT(*self.img_size, lmax=modes_lat, mmax=modes_lon, grid=self.grid).float()
345
+ self.trans = RealSHT(self.h, self.w, lmax=modes_lat, mmax=modes_lon, grid=grid_internal).float()
346
+ self.itrans = InverseRealSHT(self.h, self.w, lmax=modes_lat, mmax=modes_lon, grid=grid_internal).float()
347
+
348
+ self.blocks = nn.ModuleList([])
349
+ for i in range(self.num_layers):
350
+
351
+ first_layer = i == 0
352
+ last_layer = i == self.num_layers - 1
353
+
354
+ block = SphericalFourierNeuralOperatorBlock(
355
+ self.trans_down if first_layer else self.trans,
356
+ self.itrans_up if last_layer else self.itrans,
357
+ self.embed_dim,
358
+ self.embed_dim,
359
+ mlp_ratio=mlp_ratio,
360
+ drop_rate=drop_rate,
361
+ drop_path=dpr[i],
362
+ act_layer=self.activation_function,
363
+ norm_layer=self.normalization_layer,
364
+ use_mlp=use_mlp,
365
+ bias=bias,
366
+ )
367
+
368
+ self.blocks.append(block)
369
+
370
+ # construct an decoder with num_decoder_layers
371
+ num_decoder_layers = 1
372
+ decoder_hidden_dim = int(self.embed_dim * mlp_ratio)
373
+ current_dim = self.embed_dim
374
+ decoder_layers = []
375
+ for l in range(num_decoder_layers - 1):
376
+ fc = nn.Conv2d(current_dim, decoder_hidden_dim, 1, bias=True)
377
+ # initialize the weights correctly
378
+ scale = math.sqrt(2.0 / current_dim)
379
+ nn.init.normal_(fc.weight, mean=0.0, std=scale)
380
+ if fc.bias is not None:
381
+ nn.init.constant_(fc.bias, 0.0)
382
+ decoder_layers.append(fc)
383
+ decoder_layers.append(self.activation_function())
384
+ current_dim = decoder_hidden_dim
385
+ fc = nn.Conv2d(current_dim, self.out_chans, 1, bias=bias)
386
+ scale = math.sqrt(1.0 / current_dim)
387
+ nn.init.normal_(fc.weight, mean=0.0, std=scale)
388
+ if fc.bias is not None:
389
+ nn.init.constant_(fc.bias, 0.0)
390
+ decoder_layers.append(fc)
391
+ self.decoder = nn.Sequential(*decoder_layers)
392
+
393
+ @torch.jit.ignore
394
+ def no_weight_decay(self):
395
+ return {"pos_embed", "cls_token"}
396
+
397
+ def forward_features(self, x):
398
+
399
+ x = self.pos_drop(x)
400
+
401
+ for blk in self.blocks:
402
+ x = blk(x)
403
+
404
+ return x
405
+
406
+ def forward(self, x):
407
+
408
+ if self.residual_prediction:
409
+ residual = x
410
+
411
+ x = self.encoder(x)
412
+
413
+ if self.pos_embed is not None:
414
+ x = self.pos_embed(x)
415
+
416
+ x = self.forward_features(x)
417
+
418
+ x = self.decoder(x)
419
+
420
+ if self.residual_prediction:
421
+ x = x + residual
422
+
423
+ return x
.deps/torch_harmonics/examples/pde_dataset.py ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2022 The torch-harmonics Authors. All rights reserved.
4
+ # SPDX-License-Identifier: BSD-3-Clause
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are met:
8
+ #
9
+ # 1. Redistributions of source code must retain the above copyright notice, this
10
+ # list of conditions and the following disclaimer.
11
+ #
12
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ # this list of conditions and the following disclaimer in the documentation
14
+ # and/or other materials provided with the distribution.
15
+ #
16
+ # 3. Neither the name of the copyright holder nor the names of its
17
+ # contributors may be used to endorse or promote products derived from
18
+ # this software without specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+ #
31
+
32
+ import torch
33
+
34
+ from math import ceil
35
+
36
+ from .shallow_water_equations import ShallowWaterSolver
37
+
38
+
39
+ class PdeDataset(torch.utils.data.Dataset):
40
+ """Custom Dataset class for PDE training data
41
+
42
+ Parameters
43
+ ----------
44
+ dt : float
45
+ Time step
46
+ nsteps : int
47
+ Number of solver steps
48
+ dims : tuple, optional
49
+ Number of latitude and longitude points, by default (384, 768)
50
+ grid : str, optional
51
+ Grid type, by default "equiangular"
52
+ pde : str, optional
53
+ PDE type, by default "shallow water equations"
54
+ initial_condition : str, optional
55
+ Initial condition type, by default "random"
56
+ num_examples : int, optional
57
+ Number of examples, by default 32
58
+ device : torch.device, optional
59
+ Device to use, by default torch.device("cpu")
60
+ normalize : bool, optional
61
+ Whether to normalize the input and target, by default True
62
+ stream : torch.cuda.Stream, optional
63
+ CUDA stream to use, by default None
64
+
65
+ Returns
66
+ -------
67
+ inp : torch.Tensor
68
+ Input tensor
69
+ tar : torch.Tensor
70
+ Target tensor
71
+ """
72
+
73
+ def __init__(
74
+ self,
75
+ dt,
76
+ nsteps,
77
+ dims=(384, 768),
78
+ grid="equiangular",
79
+ pde="shallow water equations",
80
+ initial_condition="random",
81
+ num_examples=32,
82
+ device=torch.device("cpu"),
83
+ normalize=True,
84
+ stream=None,
85
+ ):
86
+ self.num_examples = num_examples
87
+ self.device = device
88
+ self.stream = stream
89
+
90
+ self.nlat = dims[0]
91
+ self.nlon = dims[1]
92
+
93
+ # number of solver steps used to compute the target
94
+ self.nsteps = nsteps
95
+ self.normalize = normalize
96
+
97
+ if pde == "shallow water equations":
98
+ lmax = ceil(self.nlat / 3)
99
+ mmax = lmax
100
+ dt_solver = dt / float(self.nsteps)
101
+ self.solver = ShallowWaterSolver(self.nlat, self.nlon, dt_solver, lmax=lmax, mmax=mmax, grid=grid).to(self.device).float()
102
+ else:
103
+ raise NotImplementedError
104
+
105
+ self.set_initial_condition(ictype=initial_condition)
106
+
107
+ if self.normalize:
108
+ inp0, _ = self._get_sample()
109
+ self.inp_mean = torch.mean(inp0, dim=(-1, -2)).reshape(-1, 1, 1)
110
+ self.inp_var = torch.var(inp0, dim=(-1, -2)).reshape(-1, 1, 1)
111
+
112
+ def __len__(self):
113
+ length = self.num_examples if self.ictype == "random" else 1
114
+ return length
115
+
116
+ def set_initial_condition(self, ictype="random"):
117
+ self.ictype = ictype
118
+
119
+ def set_num_examples(self, num_examples=32):
120
+ self.num_examples = num_examples
121
+
122
+ def _get_sample(self):
123
+ if self.ictype == "random":
124
+ inp = self.solver.random_initial_condition(mach=0.2)
125
+ elif self.ictype == "galewsky":
126
+ inp = self.solver.galewsky_initial_condition()
127
+
128
+ # solve pde for n steps to return the target
129
+ tar = self.solver.timestep(inp, self.nsteps)
130
+ inp = self.solver.spec2grid(inp)
131
+ tar = self.solver.spec2grid(tar)
132
+
133
+ return inp, tar
134
+
135
+ def __getitem__(self, index):
136
+
137
+ # if self.stream is None:
138
+ # self.stream = torch.cuda.Stream()
139
+
140
+ # with torch.cuda.stream(self.stream):
141
+ # with torch.inference_mode():
142
+ # with torch.no_grad():
143
+ # inp, tar = self._get_sample()
144
+
145
+ # if self.normalize:
146
+ # inp = (inp - self.inp_mean) / torch.sqrt(self.inp_var)
147
+ # tar = (tar - self.inp_mean) / torch.sqrt(self.inp_var)
148
+
149
+ # self.stream.synchronize()
150
+
151
+ with torch.inference_mode():
152
+ with torch.no_grad():
153
+ inp, tar = self._get_sample()
154
+
155
+ if self.normalize:
156
+ inp = (inp - self.inp_mean) / torch.sqrt(self.inp_var)
157
+ tar = (tar - self.inp_mean) / torch.sqrt(self.inp_var)
158
+
159
+ return inp.clone(), tar.clone()
.deps/torch_harmonics/examples/pde_sphere.py ADDED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2022 The torch-harmonics Authors. All rights reserved.
4
+ # SPDX-License-Identifier: BSD-3-Clause
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are met:
8
+ #
9
+ # 1. Redistributions of source code must retain the above copyright notice, this
10
+ # list of conditions and the following disclaimer.
11
+ #
12
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ # this list of conditions and the following disclaimer in the documentation
14
+ # and/or other materials provided with the distribution.
15
+ #
16
+ # 3. Neither the name of the copyright holder nor the names of its
17
+ # contributors may be used to endorse or promote products derived from
18
+ # this software without specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+ #
31
+
32
+
33
+ import torch
34
+ import torch.nn as nn
35
+ import torch_harmonics as th
36
+ from torch_harmonics.quadrature import _precompute_longitudes
37
+
38
+ import math
39
+ import numpy as np
40
+
41
+
42
+ class SphereSolver(nn.Module):
43
+ """
44
+ Solver class on the sphere. Can solve the following PDEs:
45
+ - Allen-Cahn equation
46
+ - Ginzburg-Landau equation
47
+
48
+ Parameters
49
+ -----------
50
+ nlat : int
51
+ Number of latitude points
52
+ nlon : int
53
+ Number of longitude points
54
+ dt : float
55
+ Time step size
56
+ lmax : int, optional
57
+ Maximum l mode for spherical harmonics, by default None
58
+ mmax : int, optional
59
+ Maximum m mode for spherical harmonics, by default None
60
+ grid : str, optional
61
+ Grid type ("equiangular", "legendre-gauss", "lobatto"), by default "equiangular"
62
+ radius : float, optional
63
+ Radius of the sphere, by default 1.0
64
+ coeff : float, optional
65
+ Coefficient for the PDE, by default 0.001
66
+ """
67
+
68
+ def __init__(self, nlat, nlon, dt, lmax=None, mmax=None, grid="equiangular", radius=1.0, coeff=0.001):
69
+ super().__init__()
70
+
71
+ # time stepping param
72
+ self.dt = dt
73
+
74
+ # grid parameters
75
+ self.nlat = nlat
76
+ self.nlon = nlon
77
+ self.grid = grid
78
+
79
+ # physical sonstants
80
+ self.register_buffer('radius', torch.as_tensor(radius, dtype=torch.float64))
81
+ self.register_buffer('coeff', torch.as_tensor(coeff, dtype=torch.float64))
82
+
83
+ # SHT
84
+ self.sht = th.RealSHT(nlat, nlon, lmax=lmax, mmax=mmax, grid=grid, csphase=False)
85
+ self.isht = th.InverseRealSHT(nlat, nlon, lmax=lmax, mmax=mmax, grid=grid, csphase=False)
86
+
87
+ self.lmax = lmax or self.sht.lmax
88
+ self.mmax = lmax or self.sht.mmax
89
+
90
+ # compute gridpoints
91
+ if self.grid == "legendre-gauss":
92
+ cost, _ = th.quadrature.legendre_gauss_weights(self.nlat, -1, 1)
93
+ elif self.grid == "lobatto":
94
+ cost, _ = th.quadrature.lobatto_weights(self.nlat, -1, 1)
95
+ elif self.grid == "equiangular":
96
+ cost, _ = th.quadrature.clenshaw_curtiss_weights(self.nlat, -1, 1)
97
+
98
+ # apply cosine transform and flip them
99
+ lats = -torch.arcsin(cost)
100
+ lons = _precompute_longitudes(self.nlon)
101
+
102
+ self.lmax = self.sht.lmax
103
+ self.mmax = self.sht.mmax
104
+
105
+ l = torch.arange(0, self.lmax).reshape(self.lmax, 1).cdouble()
106
+ l = l.expand(self.lmax, self.mmax)
107
+ # the laplace operator acting on the coefficients is given by l (l + 1)
108
+ lap = - l * (l + 1) / self.radius**2
109
+ invlap = - self.radius**2 / l / (l + 1)
110
+ invlap[0] = 0.
111
+
112
+ # register all
113
+ self.register_buffer('lats', lats)
114
+ self.register_buffer('lons', lons)
115
+ self.register_buffer('l', l)
116
+ self.register_buffer('lap', lap)
117
+ self.register_buffer('invlap', invlap)
118
+
119
+ def grid2spec(self, u):
120
+ return self.sht(u)
121
+
122
+ def spec2grid(self, uspec):
123
+ """Convert spectral coefficients to spatial data."""
124
+ return self.isht(uspec)
125
+
126
+ def dudtspec(self, uspec, pde='allen-cahn'):
127
+ """Compute the time derivative of spectral coefficients for different PDEs."""
128
+
129
+ if pde == 'allen-cahn':
130
+ ugrid = self.spec2grid(uspec)
131
+ u3spec = self.grid2spec(ugrid**3)
132
+ dudtspec = self.coeff*self.lap*uspec + uspec - u3spec
133
+ elif pde == 'ginzburg-landau':
134
+ ugrid = self.spec2grid(uspec)
135
+ u3spec = self.grid2spec(ugrid**3)
136
+ dudtspec = uspec + (1. + 2.j)*self.coeff*self.lap*uspec - (1. + 2.j)*u3spec
137
+ else:
138
+ raise NotImplementedError(f"PDE type {pde} not implemented")
139
+
140
+ return dudtspec
141
+
142
+ def randspec(self):
143
+ """Generate random spectral data on the sphere."""
144
+ rspec = torch.randn_like(self.lap) / 4 / torch.pi
145
+ return rspec
146
+
147
+
148
+ def plot_griddata(self, data, fig, cmap='twilight_shifted', vmax=None, vmin=None, projection='3d', title=None, antialiased=False):
149
+ """
150
+ Plot data on the sphere grid. Requires cartopy for 3d plots.
151
+
152
+ Parameters
153
+ -----------
154
+ data : torch.Tensor
155
+ Data to plot
156
+ fig : matplotlib.figure.Figure
157
+ Figure to plot on
158
+ cmap : str, optional
159
+ Colormap name, by default 'twilight_shifted'
160
+ vmax : float, optional
161
+ Maximum value for color scaling, by default None
162
+ vmin : float, optional
163
+ Minimum value for color scaling, by default None
164
+ projection : str, optional
165
+ Projection type ("mollweide", "3d"), by default "3d"
166
+ title : str, optional
167
+ Plot title, by default None
168
+ antialiased : bool, optional
169
+ Whether to use antialiasing, by default False
170
+
171
+ Returns
172
+ -------
173
+ matplotlib.collections.QuadMesh
174
+ The plotted image object
175
+
176
+ Raises
177
+ ------
178
+ NotImplementedError
179
+ If projection type is not supported
180
+ """
181
+ import matplotlib.pyplot as plt
182
+
183
+ lons = self.lons.squeeze() - torch.pi
184
+ lats = self.lats.squeeze()
185
+
186
+ if data.is_cuda:
187
+ data = data.cpu()
188
+ lons = lons.cpu()
189
+ lats = lats.cpu()
190
+
191
+ Lons, Lats = np.meshgrid(lons, lats)
192
+
193
+ if projection == 'mollweide':
194
+
195
+ #ax = plt.gca(projection=projection)
196
+ ax = fig.add_subplot(projection=projection)
197
+ im = ax.pcolormesh(Lons, Lats, data, cmap=cmap, vmax=vmax, vmin=vmin)
198
+ # ax.set_title("Elevation map of mars")
199
+ ax.grid(True)
200
+ ax.set_xticklabels([])
201
+ ax.set_yticklabels([])
202
+ plt.colorbar(im, orientation='horizontal')
203
+ plt.title(title)
204
+
205
+ elif projection == '3d':
206
+
207
+ import cartopy.crs as ccrs
208
+
209
+ proj = ccrs.Orthographic(central_longitude=0.0, central_latitude=25.0)
210
+
211
+ #ax = plt.gca(projection=proj, frameon=True)
212
+ ax = fig.add_subplot(projection=proj)
213
+ Lons = Lons*180/math.pi
214
+ Lats = Lats*180/math.pi
215
+
216
+ # contour data over the map.
217
+ im = ax.pcolormesh(Lons, Lats, data, cmap=cmap, transform=ccrs.PlateCarree(), antialiased=antialiased, vmax=vmax, vmin=vmin)
218
+ plt.title(title, y=1.05)
219
+
220
+ else:
221
+ raise NotImplementedError(f"Projection {projection} not implemented")
222
+
223
+ return im
224
+
225
+ def plot_specdata(self, data, fig, **kwargs):
226
+ """Plot spectral data by converting to spatial data first."""
227
+ return self.plot_griddata(self.isht(data), fig, **kwargs)
.deps/torch_harmonics/examples/shallow_water_equations.py ADDED
@@ -0,0 +1,394 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2022 The torch-harmonics Authors. All rights reserved.
4
+ # SPDX-License-Identifier: BSD-3-Clause
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are met:
8
+ #
9
+ # 1. Redistributions of source code must retain the above copyright notice, this
10
+ # list of conditions and the following disclaimer.
11
+ #
12
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ # this list of conditions and the following disclaimer in the documentation
14
+ # and/or other materials provided with the distribution.
15
+ #
16
+ # 3. Neither the name of the copyright holder nor the names of its
17
+ # contributors may be used to endorse or promote products derived from
18
+ # this software without specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+ #
31
+
32
+
33
+ import torch
34
+ import torch.nn as nn
35
+ import torch_harmonics as th
36
+ from torch_harmonics.quadrature import _precompute_longitudes
37
+
38
+ import math
39
+ import numpy as np
40
+
41
+
42
+ class ShallowWaterSolver(nn.Module):
43
+ """
44
+ Shallow Water Equations (SWE) solver class for spherical geometry.
45
+
46
+ Interface inspired by pyspharm and SHTns. Solves the shallow water equations
47
+ on a rotating sphere using spectral methods.
48
+
49
+ Parameters
50
+ -----------
51
+ nlat : int
52
+ Number of latitude points
53
+ nlon : int
54
+ Number of longitude points
55
+ dt : float
56
+ Time step size
57
+ lmax : int, optional
58
+ Maximum l mode for spherical harmonics, by default None
59
+ mmax : int, optional
60
+ Maximum m mode for spherical harmonics, by default None
61
+ grid : str, optional
62
+ Grid type ("equiangular", "legendre-gauss", "lobatto"), by default "equiangular"
63
+ radius : float, optional
64
+ Radius of the sphere in meters, by default 6.37122E6 (Earth radius)
65
+ omega : float, optional
66
+ Angular velocity of rotation in rad/s, by default 7.292E-5 (Earth)
67
+ gravity : float, optional
68
+ Gravitational acceleration in m/s², by default 9.80616
69
+ havg : float, optional
70
+ Average height in meters, by default 10.e3
71
+ hamp : float, optional
72
+ Height amplitude in meters, by default 120.
73
+ """
74
+
75
+ def __init__(self, nlat, nlon, dt, lmax=None, mmax=None, grid="equiangular", radius=6.37122E6, \
76
+ omega=7.292E-5, gravity=9.80616, havg=10.e3, hamp=120.):
77
+ super().__init__()
78
+
79
+ # time stepping param
80
+ self.dt = dt
81
+
82
+ # grid parameters
83
+ self.nlat = nlat
84
+ self.nlon = nlon
85
+ self.grid = grid
86
+
87
+ # physical sonstants
88
+ self.register_buffer('radius', torch.as_tensor(radius, dtype=torch.float64))
89
+ self.register_buffer('omega', torch.as_tensor(omega, dtype=torch.float64))
90
+ self.register_buffer('gravity', torch.as_tensor(gravity, dtype=torch.float64))
91
+ self.register_buffer('havg', torch.as_tensor(havg, dtype=torch.float64))
92
+ self.register_buffer('hamp', torch.as_tensor(hamp, dtype=torch.float64))
93
+
94
+ # SHT
95
+ self.sht = th.RealSHT(nlat, nlon, lmax=lmax, mmax=mmax, grid=grid, csphase=False)
96
+ self.isht = th.InverseRealSHT(nlat, nlon, lmax=lmax, mmax=mmax, grid=grid, csphase=False)
97
+ self.vsht = th.RealVectorSHT(nlat, nlon, lmax=lmax, mmax=mmax, grid=grid, csphase=False)
98
+ self.ivsht = th.InverseRealVectorSHT(nlat, nlon, lmax=lmax, mmax=mmax, grid=grid, csphase=False)
99
+
100
+ self.lmax = lmax or self.sht.lmax
101
+ self.mmax = lmax or self.sht.mmax
102
+
103
+ # compute gridpoints
104
+ if self.grid == "legendre-gauss":
105
+ cost, quad_weights = th.quadrature.legendre_gauss_weights(self.nlat, -1, 1)
106
+ elif self.grid == "lobatto":
107
+ cost, quad_weights = th.quadrature.lobatto_weights(self.nlat, -1, 1)
108
+ elif self.grid == "equiangular":
109
+ cost, quad_weights = th.quadrature.clenshaw_curtiss_weights(self.nlat, -1, 1)
110
+
111
+ quad_weights = quad_weights.reshape(-1, 1)
112
+
113
+ # apply cosine transform and flip them
114
+ lats = -torch.arcsin(cost)
115
+ lons = _precompute_longitudes(self.nlon)
116
+
117
+ self.lmax = self.sht.lmax
118
+ self.mmax = self.sht.mmax
119
+
120
+ # compute the laplace and inverse laplace operators
121
+ l = torch.arange(0, self.lmax).reshape(self.lmax, 1).double()
122
+ l = l.expand(self.lmax, self.mmax)
123
+ # the laplace operator acting on the coefficients is given by - l (l + 1)
124
+ lap = - l * (l + 1) / self.radius**2
125
+ invlap = - self.radius**2 / l / (l + 1)
126
+ invlap[0] = 0.
127
+
128
+ # compute coriolis force
129
+ coriolis = 2 * self.omega * torch.sin(lats).reshape(self.nlat, 1)
130
+
131
+ # hyperdiffusion
132
+ hyperdiff = torch.exp(torch.asarray((-self.dt / 2 / 3600.)*(lap / lap[-1, 0])**4))
133
+
134
+ # register all
135
+ self.register_buffer('lats', lats)
136
+ self.register_buffer('lons', lons)
137
+ self.register_buffer('l', l)
138
+ self.register_buffer('lap', lap)
139
+ self.register_buffer('invlap', invlap)
140
+ self.register_buffer('coriolis', coriolis)
141
+ self.register_buffer('hyperdiff', hyperdiff)
142
+ self.register_buffer('quad_weights', quad_weights)
143
+
144
+ def grid2spec(self, ugrid):
145
+ """Convert spatial data to spectral coefficients."""
146
+ return self.sht(ugrid)
147
+
148
+ def spec2grid(self, uspec):
149
+ """Convert spectral coefficients to spatial data."""
150
+ return self.isht(uspec)
151
+
152
+ def vrtdivspec(self, ugrid):
153
+ """Compute vorticity and divergence from velocity field."""
154
+ vrtdivspec = self.lap * self.radius * self.vsht(ugrid)
155
+ return vrtdivspec
156
+
157
+ def getuv(self, vrtdivspec):
158
+ """Compute wind vector from spectral coefficients of vorticity and divergence."""
159
+ return self.ivsht( self.invlap * vrtdivspec / self.radius)
160
+
161
+ def gethuv(self, uspec):
162
+ """Compute height and wind vector from spectral coefficients."""
163
+ hgrid = self.spec2grid(uspec[:1])
164
+ uvgrid = self.getuv(uspec[1:])
165
+ return torch.cat((hgrid, uvgrid), dim=-3)
166
+
167
+ def potential_vorticity(self, uspec):
168
+ """Compute potential vorticity from spectral coefficients."""
169
+ ugrid = self.spec2grid(uspec)
170
+ pvrt = (0.5 * self.havg * self.gravity / self.omega) * (ugrid[1] + self.coriolis) / ugrid[0]
171
+ return pvrt
172
+
173
+ def dimensionless(self, uspec):
174
+ """Remove dimensions from variables for dimensionless analysis."""
175
+ uspec[0] = (uspec[0] - self.havg * self.gravity) / self.hamp / self.gravity
176
+ # vorticity is measured in 1/s so we normalize using sqrt(g h) / r
177
+ uspec[1:] = uspec[1:] * self.radius / torch.sqrt(self.gravity * self.havg)
178
+ return uspec
179
+
180
+ def dudtspec(self, uspec):
181
+ """Compute time derivatives from solution represented in spectral coefficients."""
182
+ dudtspec = torch.zeros_like(uspec)
183
+
184
+ # compute the derivatives - this should be incorporated into the solver:
185
+ ugrid = self.spec2grid(uspec)
186
+ uvgrid = self.getuv(uspec[1:])
187
+
188
+ # phi = ugrid[0]
189
+ # vrtdiv = ugrid[1:]
190
+
191
+ tmp = uvgrid * (ugrid[1] + self.coriolis)
192
+ tmpspec = self.vrtdivspec(tmp)
193
+ dudtspec[2] = tmpspec[0]
194
+ dudtspec[1] = -1 * tmpspec[1]
195
+
196
+ tmp = uvgrid * ugrid[0]
197
+ tmp = self.vrtdivspec(tmp)
198
+ dudtspec[0] = -1 * tmp[1]
199
+
200
+ tmpspec = self.grid2spec(ugrid[0] + 0.5 * (uvgrid[0]**2 + uvgrid[1]**2))
201
+ dudtspec[2] = dudtspec[2] - self.lap * tmpspec
202
+
203
+ return dudtspec
204
+
205
+ def galewsky_initial_condition(self):
206
+ """Initialize non-linear barotropically unstable shallow water test case."""
207
+ device = self.lap.device
208
+
209
+ umax = 80.
210
+ phi0 = torch.asarray(torch.pi / 7., device=device)
211
+ phi1 = torch.asarray(0.5 * torch.pi - phi0, device=device)
212
+ phi2 = 0.25 * torch.pi
213
+ en = torch.exp(torch.asarray(-4.0 / (phi1 - phi0)**2, device=device))
214
+ alpha = 1. / 3.
215
+ beta = 1. / 15.
216
+
217
+ lats, lons = torch.meshgrid(self.lats, self.lons)
218
+
219
+ u1 = (umax/en)*torch.exp(1./((lats-phi0)*(lats-phi1)))
220
+ ugrid = torch.where(torch.logical_and(lats < phi1, lats > phi0), u1, torch.zeros(self.nlat, self.nlon, device=device))
221
+ vgrid = torch.zeros((self.nlat, self.nlon), device=device)
222
+ hbump = self.hamp * torch.cos(lats) * torch.exp(-((lons-torch.pi)/alpha)**2) * torch.exp(-(phi2-lats)**2/beta)
223
+
224
+ # intial velocity field
225
+ ugrid = torch.stack((ugrid, vgrid))
226
+ # intial vorticity/divergence field
227
+ vrtdivspec = self.vrtdivspec(ugrid)
228
+ vrtdivgrid = self.spec2grid(vrtdivspec)
229
+
230
+ # solve balance eqn to get initial zonal geopotential with a localized bump (not balanced).
231
+ tmp = ugrid * (vrtdivgrid + self.coriolis)
232
+ tmpspec = self.vrtdivspec(tmp)
233
+ tmpspec[1] = self.grid2spec(0.5 * torch.sum(ugrid**2, dim=0))
234
+ phispec = self.invlap*tmpspec[0] - tmpspec[1] + self.grid2spec(self.gravity*(self.havg + hbump))
235
+
236
+ # assemble solution
237
+ uspec = torch.zeros(3, self.lmax, self.mmax, dtype=vrtdivspec.dtype, device=device)
238
+ uspec[0] = phispec
239
+ uspec[1:] = vrtdivspec
240
+
241
+ return torch.tril(uspec)
242
+
243
+ def random_initial_condition(self, mach=0.1) -> torch.Tensor:
244
+ """Generate random initial condition on the sphere."""
245
+ device = self.lap.device
246
+ ctype = torch.complex128 if self.lap.dtype == torch.float64 else torch.complex64
247
+
248
+ # mach number relative to wave speed
249
+ llimit = mlimit = 120
250
+
251
+ # hgrid = self.havg + hamp * torch.randn(self.nlat, self.nlon, device=device, dtype=dtype)
252
+ # ugrid = uamp * torch.randn(self.nlat, self.nlon, device=device, dtype=dtype)
253
+ # vgrid = vamp * torch.randn(self.nlat, self.nlon, device=device, dtype=dtype)
254
+ # ugrid = torch.stack((ugrid, vgrid))
255
+
256
+ # initial geopotential
257
+ uspec = torch.zeros(3, self.lmax, self.mmax, dtype=ctype, device=self.lap.device)
258
+ uspec[:, :llimit, :mlimit] = torch.sqrt(torch.tensor(4 * torch.pi / llimit / (llimit+1), device=device, dtype=ctype)) * torch.randn_like(uspec[:, :llimit, :mlimit])
259
+
260
+ uspec[0] = self.gravity * self.hamp * uspec[0]
261
+ uspec[0, 0, 0] += torch.sqrt(torch.tensor(4 * torch.pi, device=device, dtype=ctype)) * self.havg * self.gravity
262
+ uspec[1:] = mach * uspec[1:] * torch.sqrt(self.gravity * self.havg) / self.radius
263
+ # uspec[1:] = self.vrtdivspec(self.spec2grid(uspec[1:]) * torch.cos(self.lats.reshape(-1, 1)))
264
+
265
+ # # intial velocity field
266
+ # ugrid = uamp * self.spec2grid(uspec[1])
267
+ # vgrid = vamp * self.spec2grid(uspec[2])
268
+ # ugrid = torch.stack((ugrid, vgrid))
269
+
270
+
271
+
272
+ # # intial vorticity/divergence field
273
+ # vrtdivspec = self.vrtdivspec(ugrid)
274
+ # vrtdivgrid = self.spec2grid(vrtdivspec)
275
+
276
+ # # solve balance eqn to get initial zonal geopotential with a localized bump (not balanced).
277
+ # tmp = ugrid * (vrtdivgrid + self.coriolis)
278
+ # tmpspec = self.vrtdivspec(tmp)
279
+ # tmpspec[1] = self.grid2spec(0.5 * torch.sum(ugrid**2, dim=0))
280
+ # phispec = self.invlap*tmpspec[0] - tmpspec[1] + self.grid2spec(self.gravity * hgrid)
281
+
282
+ # # assemble solution
283
+ # uspec = torch.zeros(3, self.lmax, self.mmax, dtype=phispec.dtype, device=device)
284
+ # uspec[0] = phispec
285
+ # uspec[1:] = vrtdivspec
286
+
287
+ return torch.tril(uspec)
288
+
289
+ def timestep(self, uspec: torch.Tensor, nsteps: int) -> torch.Tensor:
290
+ """Integrate the solution using Adams-Bashforth / forward Euler for nsteps steps."""
291
+ dudtspec = torch.zeros(3, 3, self.lmax, self.mmax, dtype=uspec.dtype, device=uspec.device)
292
+
293
+ # pointers to indicate the most current result
294
+ inew = 0
295
+ inow = 1
296
+ iold = 2
297
+
298
+ for iter in range(nsteps):
299
+ dudtspec[inew] = self.dudtspec(uspec)
300
+
301
+ # update vort,div,phiv with third-order adams-bashforth.
302
+ # forward euler, then 2nd-order adams-bashforth time steps to start.
303
+ if iter == 0:
304
+ dudtspec[inow] = dudtspec[inew]
305
+ dudtspec[iold] = dudtspec[inew]
306
+ elif iter == 1:
307
+ dudtspec[iold] = dudtspec[inew]
308
+
309
+ uspec = uspec + self.dt*( (23./12.) * dudtspec[inew] - (16./12.) * dudtspec[inow] + (5./12.) * dudtspec[iold] )
310
+
311
+ # implicit hyperdiffusion for vort and div.
312
+ uspec[1:] = self.hyperdiff * uspec[1:]
313
+
314
+ # cycle through the indices
315
+ inew = (inew - 1) % 3
316
+ inow = (inow - 1) % 3
317
+ iold = (iold - 1) % 3
318
+
319
+ return uspec
320
+
321
+ def integrate_grid(self, ugrid, dimensionless=False, polar_opt=0):
322
+ """Integrate the solution on the grid."""
323
+ dlon = 2 * torch.pi / self.nlon
324
+ radius = 1 if dimensionless else self.radius
325
+ if polar_opt > 0:
326
+ out = torch.sum(ugrid[..., polar_opt:-polar_opt, :] * self.quad_weights[polar_opt:-polar_opt] * dlon * radius**2, dim=(-2, -1))
327
+ else:
328
+ out = torch.sum(ugrid * self.quad_weights * dlon * radius**2, dim=(-2, -1))
329
+ return out
330
+
331
+
332
+ def plot_griddata(self, data, fig, cmap='twilight_shifted', vmax=None, vmin=None, projection='3d', title=None, antialiased=False):
333
+ """Plotting routine for data on the grid. Requires cartopy for 3d plots."""
334
+ import matplotlib.pyplot as plt
335
+
336
+ lons = self.lons.squeeze() - torch.pi
337
+ lats = self.lats.squeeze()
338
+
339
+ if data.is_cuda:
340
+ data = data.cpu()
341
+ lons = lons.cpu()
342
+ lats = lats.cpu()
343
+
344
+ Lons, Lats = np.meshgrid(lons, lats)
345
+
346
+ if projection == 'mollweide':
347
+
348
+ #ax = plt.gca(projection=projection)
349
+ ax = fig.add_subplot(projection=projection)
350
+ im = ax.pcolormesh(Lons, Lats, data, cmap=cmap, vmax=vmax, vmin=vmin)
351
+ # ax.set_title("Elevation map of mars")
352
+ ax.grid(True)
353
+ ax.set_xticklabels([])
354
+ ax.set_yticklabels([])
355
+ plt.colorbar(im, orientation='horizontal')
356
+ plt.title(title)
357
+
358
+ elif projection == '3d':
359
+
360
+ import cartopy.crs as ccrs
361
+
362
+ proj = ccrs.Orthographic(central_longitude=0.0, central_latitude=25.0)
363
+
364
+ #ax = plt.gca(projection=proj, frameon=True)
365
+ ax = fig.add_subplot(projection=proj)
366
+ Lons = Lons*180/math.pi
367
+ Lats = Lats*180/math.pi
368
+
369
+ # contour data over the map.
370
+ im = ax.pcolormesh(Lons, Lats, data, cmap=cmap, transform=ccrs.PlateCarree(), antialiased=antialiased, vmax=vmax, vmin=vmin)
371
+ plt.title(title, y=1.05)
372
+
373
+ elif projection == 'robinson':
374
+
375
+ import cartopy.crs as ccrs
376
+
377
+ proj = ccrs.Robinson(central_longitude=0.0)
378
+
379
+ #ax = plt.gca(projection=proj, frameon=True)
380
+ ax = fig.add_subplot(projection=proj)
381
+ Lons = Lons*180/math.pi
382
+ Lats = Lats*180/math.pi
383
+
384
+ # contour data over the map.
385
+ im = ax.pcolormesh(Lons, Lats, data, cmap=cmap, transform=ccrs.PlateCarree(), antialiased=antialiased, vmax=vmax, vmin=vmin)
386
+ plt.title(title, y=1.05)
387
+
388
+ else:
389
+ raise NotImplementedError
390
+
391
+ return im
392
+
393
+ def plot_specdata(self, data, fig, **kwargs):
394
+ return self.plot_griddata(self.isht(data), fig, **kwargs)
.deps/torch_harmonics/examples/stanford_2d3ds_dataset.py ADDED
@@ -0,0 +1,777 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2025 The torch-harmonics Authors. All rights reserved.
4
+ # SPDX-License-Identifier: BSD-3-Clause
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are met:
8
+ #
9
+ # 1. Redistributions of source code must retain the above copyright notice, this
10
+ # list of conditions and the following disclaimer.
11
+ #
12
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ # this list of conditions and the following disclaimer in the documentation
14
+ # and/or other materials provided with the distribution.
15
+ #
16
+ # 3. Neither the name of the copyright holder nor the names of its
17
+ # contributors may be used to endorse or promote products derived from
18
+ # this software without specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+ #
31
+
32
+ import os
33
+ import math
34
+
35
+ import torch
36
+ from torch.utils.data import Dataset, DataLoader, Subset
37
+
38
+ import numpy as np
39
+
40
+ from torch_harmonics.quadrature import _precompute_latitudes
41
+ from torch_harmonics.examples.losses import get_quadrature_weights
42
+
43
+ # some specifiers where to find the dataset
44
+ DEFAULT_BASE_URL = "https://cvg-data.inf.ethz.ch/2d3ds/no_xyz/"
45
+ DEFAULT_TAR_FILE_PAIRS = [
46
+ ("area_1_no_xyz.tar", "area_1"),
47
+ ("area_2_no_xyz.tar", "area_2"),
48
+ ("area_3_no_xyz.tar", "area_3"),
49
+ ("area_4_no_xyz.tar", "area_4"),
50
+ ("area_5a_no_xyz.tar", "area_5a"),
51
+ ("area_5b_no_xyz.tar", "area_5b"),
52
+ ("area_6_no_xyz.tar", "area_6"),
53
+ ]
54
+ DEFAULT_LABELS_URL = "https://raw.githubusercontent.com/alexsax/2D-3D-Semantics/refs/heads/master/assets/semantic_labels.json"
55
+
56
+
57
+ class Stanford2D3DSDownloader:
58
+ """
59
+ Convenience class for downloading the 2d3ds dataset [1].
60
+
61
+ Parameters
62
+ ----------
63
+ base_url : str, optional
64
+ Base URL for downloading the dataset, by default DEFAULT_BASE_URL
65
+ local_dir : str, optional
66
+ Local directory to store downloaded files, by default "data"
67
+
68
+ Returns
69
+ -------
70
+ data_folders : list
71
+ List of extracted directory names
72
+ class_labels : list
73
+ List of semantic class labels
74
+
75
+ References
76
+ ----------
77
+ .. [1] Armeni, I., Sax, S., Zamir, A. R., Savarese, S.;
78
+ "Joint 2D-3D-Semantic Data for Indoor Scene Understanding" (2017).
79
+ https://arxiv.org/abs/1702.01105.
80
+ """
81
+
82
+ def __init__(self, base_url: str = DEFAULT_BASE_URL, local_dir: str = "data"):
83
+
84
+ self.base_url = base_url
85
+ self.local_dir = local_dir
86
+ os.makedirs(self.local_dir, exist_ok=True)
87
+
88
+ def _download_file(self, filename):
89
+
90
+ import requests
91
+ from tqdm import tqdm
92
+
93
+ url = f"{self.base_url}/{filename}"
94
+ local_path = os.path.join(self.local_dir, filename)
95
+ if os.path.exists(local_path):
96
+ print(f"Note: Skipping download for {filename}, because it already exists")
97
+ return local_path
98
+
99
+ print(f"Downloading {filename}...")
100
+ temp_path = local_path.split(".")[0] + ".part"
101
+
102
+ # Resume logic
103
+ headers = {}
104
+ if os.path.exists(temp_path):
105
+ headers = {"Range": f"bytes={os.stat(temp_path).st_size}-"}
106
+
107
+ response = requests.get(url, headers=headers, stream=True, timeout=30)
108
+ if os.path.exists(temp_path):
109
+ total_size = int(response.headers.get("content-length", 0)) + os.stat(temp_path).st_size
110
+ else:
111
+ total_size = int(response.headers.get("content-length", 0))
112
+
113
+ with open(temp_path, "ab") as f, tqdm(desc=filename, total=total_size, unit="B", unit_scale=True, unit_divisor=1024, initial=os.stat(temp_path).st_size) as pbar:
114
+ for chunk in response.iter_content(chunk_size=1024):
115
+ if chunk:
116
+ f.write(chunk)
117
+ pbar.update(len(chunk))
118
+
119
+ os.rename(temp_path, local_path)
120
+ return local_path
121
+
122
+ def _extract_tar(self, tar_path):
123
+
124
+ import tarfile
125
+
126
+ with tarfile.open(tar_path) as tar:
127
+ tar.extractall(path=self.local_dir)
128
+ tar_filenames = tar.getnames()
129
+ extracted_dir = tar_filenames[0]
130
+ os.remove(tar_path)
131
+ return extracted_dir
132
+
133
+ def download_dataset(self, file_extracted_directory_pairs=DEFAULT_TAR_FILE_PAIRS):
134
+ """
135
+ Download and extract the complete dataset.
136
+
137
+ Parameters
138
+ -----------
139
+ file_extracted_directory_pairs : list, optional
140
+ List of (filename, extracted_folder_name) pairs, by default DEFAULT_TAR_FILE_PAIRS
141
+
142
+ Returns
143
+ -------
144
+ tuple
145
+ (data_folders, class_labels) where data_folders is a list of extracted directory names
146
+ and class_labels is the semantic label mapping
147
+ """
148
+ import requests
149
+
150
+ data_folders = []
151
+ for file, extracted_folder_name in file_extracted_directory_pairs:
152
+ if not os.path.exists(os.path.join(self.local_dir, extracted_folder_name)):
153
+ downloaded_file = self._download_file(file)
154
+ data_folders.append(self._extract_tar(downloaded_file))
155
+ else:
156
+ print(f"Warning: Skipping D/L for '{file}' because folder '{extracted_folder_name}' already exists")
157
+ data_folders.append(extracted_folder_name)
158
+
159
+ labels_json_url = DEFAULT_LABELS_URL
160
+ class_labels = requests.get(labels_json_url).json()
161
+ return data_folders, class_labels
162
+
163
+ def _rgb_to_id(self, img, class_labels_map, class_labels_indices):
164
+
165
+ # Convert to int32 first to avoid overflow
166
+ r = img[..., 0].astype(np.int32)
167
+ g = img[..., 1].astype(np.int32)
168
+ b = img[..., 2].astype(np.int32)
169
+ lookup_indices = r * 256 * 256 + g * 256 + b
170
+
171
+ def _convert(lookup: int) -> int:
172
+ # the dataset has a bad label for clutter, so we need to fix it
173
+ # clutter is 855309, but the labels file has it as 3341
174
+ # The original conversion used uint8, which overflowed the clutter label to 3341
175
+ # this is a fix to handle that accidental usage of undefined overflow behavior
176
+ if lookup == 855309:
177
+ label = class_labels_map[3341] # clutter
178
+ else:
179
+ label = class_labels_map[lookup]
180
+ class_index = class_labels_indices.index(label)
181
+ return class_index
182
+
183
+ lookup_fn = np.vectorize(_convert)
184
+
185
+ return lookup_fn(lookup_indices)
186
+
187
+ def convert_dataset(
188
+ self,
189
+ data_folders,
190
+ class_labels,
191
+ rgb_path: str = "pano/rgb",
192
+ semantic_path: str = "pano/semantic",
193
+ depth_path: str = "pano/depth",
194
+ output_filename="semantic",
195
+ dataset_file: str = "stanford_2d3ds_dataset.h5",
196
+ downsampling_factor: int = 16,
197
+ remove_alpha_channel: bool = True,
198
+ ):
199
+ """
200
+ Convert the downloaded dataset to HDF5 format for efficient loading.
201
+
202
+ Parameters
203
+ -----------
204
+ data_folders : list
205
+ List of extracted data folder names
206
+ class_labels : list
207
+ List of semantic class labels
208
+ rgb_path : str, optional
209
+ Relative path to RGB images within each data folder, by default "pano/rgb"
210
+ semantic_path : str, optional
211
+ Relative path to semantic labels within each data folder, by default "pano/semantic"
212
+ depth_path : str, optional
213
+ Relative path to depth images within each data folder, by default "pano/depth"
214
+ output_filename : str, optional
215
+ Suffix for semantic label files, by default "semantic"
216
+ dataset_file : str, optional
217
+ Output HDF5 filename, by default "stanford_2d3ds_dataset.h5"
218
+ downsampling_factor : int, optional
219
+ Factor by which to downsample images, by default 16
220
+ remove_alpha_channel : bool, optional
221
+ Whether to remove alpha channel from RGB images, by default True
222
+
223
+ Returns
224
+ -------
225
+ str
226
+ Path to the created HDF5 dataset file
227
+ """
228
+ converted_dataset_path = os.path.join(self.local_dir, dataset_file)
229
+
230
+ from PIL import Image
231
+ from tqdm import tqdm
232
+ import h5py as h5
233
+
234
+ file_paths = []
235
+
236
+ min_vals = None
237
+ max_vals = None
238
+
239
+ # condition class labels first:
240
+ class_labels_map = [label.split("_")[0] for label in class_labels]
241
+ class_labels_indices = sorted(list(set(class_labels_map)))
242
+
243
+ # get all the file path input, output pairs
244
+ for base_path in data_folders:
245
+
246
+ rgb_dir = os.path.join(self.local_dir, base_path, rgb_path)
247
+ semantic_dir = os.path.join(self.local_dir, base_path, semantic_path)
248
+ depth_dir = os.path.join(self.local_dir, base_path, depth_path)
249
+
250
+ if os.path.exists(rgb_dir) and os.path.exists(semantic_dir) and os.path.exists(depth_dir):
251
+ for file_input in os.listdir(rgb_dir):
252
+ if not file_input.endswith(".png"):
253
+ continue
254
+ rgb_filepath = os.path.join(rgb_dir, file_input)
255
+ semantic_filepath = "_".join(os.path.splitext(os.path.basename(rgb_filepath))[0].split("_")[:-1]) + f"_{output_filename}.png"
256
+ semantic_filepath = os.path.join(semantic_dir, semantic_filepath)
257
+ depth_filepath = "_".join(os.path.splitext(os.path.basename(rgb_filepath))[0].split("_")[:-1]) + f"_depth.png"
258
+ depth_filepath = os.path.join(depth_dir, depth_filepath)
259
+ if not os.path.exists(semantic_filepath):
260
+ print(f"Warning: Couldn't find output file in pair: ({rgb_filepath},{semantic_filepath})")
261
+ continue
262
+
263
+ if not os.path.exists(depth_filepath):
264
+ print(f"Warning: Couldn't find depth file in pair: ({rgb_filepath},{depth_filepath})")
265
+ continue
266
+
267
+ file_paths.append((rgb_filepath, semantic_filepath, depth_filepath))
268
+ elif not os.path.exists(rgb_dir):
269
+ print("Warning: RGB dir doesn't exist: ", rgb_dir)
270
+ continue
271
+ elif not os.path.exists(semantic_dir):
272
+ print("Warning: Semantic dir doesn't exist: ", semantic_dir)
273
+ continue
274
+ elif not os.path.exists(depth_dir):
275
+ print("Warning: Depth dir doesn't exist: ", depth_dir)
276
+ continue
277
+
278
+ num_samples = len(file_paths)
279
+
280
+ if num_samples > 0:
281
+ first_rgb, first_semantic, first_depth = file_paths[0]
282
+ first_rgb = np.array(Image.open(first_rgb))
283
+ # first_semantic = np.array(Image.open(first_semantic))
284
+ # first_depth = np.array(Image.open(first_depth))
285
+
286
+ rgb_shape = first_rgb.shape
287
+ img_shape = (rgb_shape[0] // downsampling_factor, rgb_shape[1] // downsampling_factor)
288
+ rgb_channels = rgb_shape[2]
289
+
290
+ if remove_alpha_channel:
291
+ rgb_channels = 3
292
+ else:
293
+ raise ValueError(f"No samples found")
294
+
295
+ # create the dataset file
296
+ with h5.File(converted_dataset_path, "w") as h5file:
297
+ rgb_data = h5file.create_dataset("rgb", (num_samples, rgb_channels, *img_shape), "f4")
298
+ semantic_data = h5file.create_dataset("semantic", (num_samples, *img_shape), "i8")
299
+ depth_data = h5file.create_dataset("depth", (num_samples, *img_shape), "f4")
300
+ classes = h5file.create_dataset("class_labels", data=class_labels_indices)
301
+ num_classes = len(set(class_labels_indices))
302
+ data_source_path = h5file.create_dataset("data_source_path", (num_samples,), dtype=h5.string_dtype(encoding="utf-8"))
303
+ data_target_path = h5file.create_dataset("data_target_path", (num_samples,), dtype=h5.string_dtype(encoding="utf-8"))
304
+
305
+ # prepare computation of the class histogram
306
+ class_histogram = np.zeros(num_classes)
307
+ _, quad_weights = _precompute_latitudes(nlat=img_shape[0], grid="equiangular")
308
+ quad_weights = quad_weights.reshape(-1, 1) * 2 * torch.pi / float(img_shape[1])
309
+ quad_weights = quad_weights.tile(1, img_shape[1])
310
+ quad_weights /= torch.sum(quad_weights)
311
+ quad_weights = quad_weights.numpy()
312
+
313
+ for count in tqdm(range(num_samples), desc="preparing dataset"):
314
+ # open image
315
+ img = Image.open(file_paths[count][0])
316
+
317
+ # downsampling
318
+ if downsampling_factor != 1:
319
+ # first width, then weight, weird
320
+ img = img.resize(size=(img_shape[1], img_shape[0]), resample=Image.BILINEAR)
321
+
322
+ # remove alpha channel if requested
323
+ if remove_alpha_channel:
324
+ img = img.convert("RGBA")
325
+ background = Image.new("RGBA", img.size, (255, 255, 255))
326
+ # compoe foreground and background and remove alpha channel
327
+ img = np.array(Image.alpha_composite(background, img))
328
+ r_data = img[:, :, :3]
329
+ else:
330
+ r_data = np.array(img)
331
+
332
+ # transpose to channels first
333
+ r_data = np.transpose(r_data / 255.0, axes=(2, 0, 1))
334
+
335
+ # write to disk
336
+ rgb_data[count, ...] = r_data[...]
337
+ data_source_path[count] = file_paths[count][0]
338
+
339
+ # compute stats -> segmentation
340
+ # min/max
341
+ tmp_min = np.min(r_data, axis=(1, 2))
342
+ tmp_max = np.max(r_data, axis=(1, 2))
343
+ # mean/var
344
+ tmp_mean = np.sum(r_data * quad_weights[np.newaxis, :, :], axis=(1, 2))
345
+ tmp_m2 = np.sum(np.square(r_data - tmp_mean[:, np.newaxis, np.newaxis]) * quad_weights[np.newaxis, :, :])
346
+ if count == 0:
347
+ # min/max
348
+ min_vals = tmp_min
349
+ max_vals = tmp_max
350
+ # mean/var
351
+ mean_vals = tmp_mean
352
+ m2_vals = tmp_m2
353
+ else:
354
+ # min/max
355
+ min_vals = np.minimum(min_vals, tmp_min)
356
+ max_vals = np.minimum(max_vals, tmp_max)
357
+ # mean/var
358
+ delta = tmp_mean - mean_vals
359
+ mean_vals += delta / float(count + 1)
360
+ m2_vals += tmp_m2 + delta * delta * float(count / (count + 1))
361
+
362
+ # get the target
363
+ sem = Image.open(file_paths[count][1])
364
+
365
+ # downsampling
366
+ if downsampling_factor != 1:
367
+ sem = sem.resize(size=(img_shape[1], img_shape[0]), resample=Image.NEAREST)
368
+
369
+ sem_data = np.array(sem, dtype=np.uint32)
370
+
371
+ # map to classes
372
+ sem_data = self._rgb_to_id(sem_data, class_labels_map, class_labels_indices)
373
+
374
+ # write to file
375
+ semantic_data[count, ...] = sem_data[...]
376
+ data_target_path[count] = file_paths[count][1]
377
+
378
+ # Here we want depth
379
+ dep = Image.open(file_paths[count][2])
380
+
381
+ if downsampling_factor != 1:
382
+ dep = dep.resize(size=(img_shape[1], img_shape[0]), resample=Image.NEAREST)
383
+ dep_data = np.array(dep)
384
+
385
+ depth_data[count, ...] = dep_data[...] / 65536.0
386
+
387
+ # compute stats -> depth
388
+ # min/max
389
+ tmp_min_depth = np.min(dep_data, axis=(0, 1))
390
+ tmp_max_depth = np.max(dep_data, axis=(0, 1))
391
+ # mean/var
392
+ tmp_mean_depth = np.sum(dep_data * quad_weights[:, :])
393
+ tmp_m2_depth = np.sum(np.square(dep_data - tmp_mean_depth) * quad_weights[:, :])
394
+ if count == 0:
395
+ min_vals_depth = tmp_min_depth
396
+ max_vals_depth = tmp_max_depth
397
+ mean_vals_depth = tmp_mean_depth
398
+ m2_vals_depth = tmp_m2_depth
399
+ else:
400
+ min_vals_depth = np.minimum(min_vals_depth, tmp_min_depth)
401
+ max_vals_depth = np.minimum(max_vals_depth, tmp_max_depth)
402
+ delta = tmp_mean_depth - mean_vals_depth
403
+ mean_vals_depth += delta / float(count + 1)
404
+ m2_vals_depth += tmp_m2_depth + delta * delta * float(count / (count + 1))
405
+
406
+ # update the class histogram
407
+ for c in range(num_classes):
408
+ class_histogram[c] += quad_weights[sem_data == c].sum()
409
+
410
+ # record min/max
411
+ h5file.create_dataset("min_rgb", data=min_vals.astype(np.float32))
412
+ h5file.create_dataset("max_rgb", data=max_vals.astype(np.float32))
413
+ h5file.create_dataset("mean_rgb", data=mean_vals.astype(np.float32))
414
+ std_vals = np.sqrt(m2_vals / float(num_samples - 1))
415
+ h5file.create_dataset("std_rgb", data=std_vals.astype(np.float32))
416
+
417
+ # record min/max
418
+ h5file.create_dataset("min_depth", data=min_vals_depth.astype(np.float32))
419
+ h5file.create_dataset("max_depth", data=max_vals_depth.astype(np.float32))
420
+ h5file.create_dataset("mean_depth", data=mean_vals_depth.astype(np.float32))
421
+ std_vals_depth = np.sqrt(m2_vals_depth / float(num_samples - 1))
422
+ h5file.create_dataset("std_depth", data=std_vals_depth.astype(np.float32))
423
+
424
+ # record class histogram
425
+ class_histogram = class_histogram / num_samples
426
+ h5file.create_dataset("class_histogram", data=class_histogram.astype(np.float32))
427
+
428
+ return converted_dataset_path
429
+
430
+ def prepare_dataset(self, file_extracted_directory_pairs=DEFAULT_TAR_FILE_PAIRS, dataset_file: str = "stanford_2d3ds_dataset.h5", downsampling_factor: int = 16):
431
+
432
+ converted_dataset_path = os.path.join(self.local_dir, dataset_file)
433
+ if os.path.exists(converted_dataset_path):
434
+ print(
435
+ f"Dataset file at {converted_dataset_path} already exists. Skipping download and conversion. If you want to create a new dataset file, delete or rename the existing file."
436
+ )
437
+ return converted_dataset_path
438
+
439
+ data_folders, class_labels = self.download_dataset(file_extracted_directory_pairs=file_extracted_directory_pairs)
440
+ converted_dataset_path = self.convert_dataset(data_folders=data_folders, class_labels=class_labels, dataset_file=dataset_file, downsampling_factor=downsampling_factor)
441
+
442
+ self.converted_dataset_path = converted_dataset_path
443
+
444
+ return self.converted_dataset_path
445
+
446
+
447
+ class StanfordSegmentationDataset(Dataset):
448
+ """
449
+ Spherical segmentation dataset from [1].
450
+
451
+ Parameters
452
+ ----------
453
+ dataset_file : str
454
+ Path to the HDF5 dataset file
455
+ ignore_alpha_channel : bool, optional
456
+ Whether to ignore the alpha channel in the RGB images, by default True
457
+ log_depth : bool, optional
458
+ Whether to log the depth values, by default False
459
+ exclude_polar_fraction : float, optional
460
+ Fraction of polar points to exclude, by default 0.0
461
+
462
+ Returns
463
+ -------
464
+ StanfordSegmentationDataset
465
+ Dataset object
466
+
467
+ References
468
+ ----------
469
+ .. [1] Armeni, I., Sax, S., Zamir, A. R., Savarese, S.;
470
+ "Joint 2D-3D-Semantic Data for Indoor Scene Understanding" (2017).
471
+ https://arxiv.org/abs/1702.01105.
472
+ """
473
+
474
+ def __init__(
475
+ self,
476
+ dataset_file,
477
+ ignore_alpha_channel=True,
478
+ exclude_polar_fraction=0,
479
+ ):
480
+
481
+ import h5py as h5
482
+
483
+ self.dataset_file = dataset_file
484
+ self.exclude_polar_fraction = exclude_polar_fraction
485
+
486
+ with h5.File(self.dataset_file, "r") as h5file:
487
+ self.img_rgb = h5file["rgb"][0].shape
488
+ self.img_seg = h5file["semantic"][0].shape
489
+ self.num_samples = h5file["rgb"].shape[0]
490
+ self.num_classes = h5file["class_labels"].shape[0]
491
+
492
+ self.class_labels = [class_name.decode("utf-8") for class_name in h5file["class_labels"][...].tolist()]
493
+ self.class_histogram = np.array(h5file["class_histogram"][...])
494
+ self.class_histogram = self.class_histogram / self.class_histogram.sum()
495
+
496
+ self.mean = h5file["mean_rgb"][...]
497
+ self.std = h5file["std_rgb"][...]
498
+ self.min = h5file["min_rgb"][...]
499
+ self.max = h5file["max_rgb"][...]
500
+
501
+ self.img_filepath = h5file["data_source_path"][...]
502
+ self.tar_filepath = h5file["data_target_path"][...]
503
+
504
+ if ignore_alpha_channel:
505
+ self.img_rgb = (3, self.img_rgb[1], self.img_rgb[2])
506
+
507
+ # open file and check for
508
+ self.h5file = None
509
+ self.rgb = None
510
+ self.semantic = None
511
+
512
+ # return index set to false by default
513
+ # when true, the __getitem__ method will return the index of the input,target pair
514
+ self.return_index = False
515
+
516
+ @property
517
+ def target_shape(self):
518
+ return self.img_seg
519
+
520
+ @property
521
+ def input_shape(self):
522
+ return self.img_rgb
523
+
524
+ def set_return_index(self, return_index: bool):
525
+ self.return_index = return_index
526
+
527
+ def get_img_filepath(self, idx: int):
528
+ return self.img_filepath[idx]
529
+
530
+ def get_tar_filepath(self, idx: int):
531
+ return self.tar_filepath[idx]
532
+
533
+ def _id_to_class(self, class_id):
534
+ if class_id > self.num_classes:
535
+ print("WARNING: ID > number of classes!")
536
+ return None
537
+ return self.segmentation_classes[class_id]
538
+
539
+ def _mask_invalid(self, tar):
540
+ return np.where(tar >= self.num_classes, -100, tar)
541
+
542
+ def __len__(self):
543
+ return self.num_samples
544
+
545
+ def _init_files(self):
546
+ import h5py as h5
547
+
548
+ self.h5file = h5.File(self.dataset_file, "r")
549
+ self.rgb = self.h5file["rgb"]
550
+ self.semantic = self.h5file["semantic"]
551
+
552
+ def reset(self):
553
+ self.rgb = None
554
+ self.semantic = None
555
+ if self.h5file is not None:
556
+ self.h5file.close()
557
+ del self.h5file
558
+ self.h5file = None
559
+
560
+ def __getitem__(self, idx, mask_invalid=True):
561
+
562
+ if self.h5file is None:
563
+ # init files
564
+ self._init_files()
565
+
566
+ rgb = self.rgb[idx, 0 : self.img_rgb[0], 0 : self.img_rgb[1], 0 : self.img_rgb[2]]
567
+ sem = self.semantic[idx, 0 : self.img_seg[0], 0 : self.img_seg[1]]
568
+ if mask_invalid:
569
+ sem = self._mask_invalid(sem)
570
+
571
+ if self.exclude_polar_fraction > 0:
572
+ hcut = int(self.exclude_polar_fraction * sem.shape[0])
573
+ if hcut > 0:
574
+ sem[0:hcut, :] = -100
575
+ sem[-hcut:, :] = -100
576
+
577
+ return rgb, sem
578
+
579
+
580
+ class StanfordDatasetSubset(Subset):
581
+ def __init__(self, dataset, indices, return_index=False):
582
+ super().__init__(dataset, indices)
583
+ self.return_index = return_index
584
+ self.dataset = dataset
585
+
586
+ def set_return_index(self, value):
587
+ self.return_index = value
588
+
589
+ def __getitem__(self, index):
590
+ real_index = self.indices[index]
591
+ data = self.dataset[real_index]
592
+
593
+ if self.return_index:
594
+ return data[0], data[1], real_index
595
+ else:
596
+ # Otherwise, return only (data, target)
597
+ return data[0], data[1]
598
+
599
+
600
+ class StanfordDepthDataset(Dataset):
601
+ """
602
+ Spherical segmentation dataset from [1].
603
+
604
+ Parameters
605
+ ----------
606
+ dataset_file : str
607
+ Path to the HDF5 dataset file
608
+ ignore_alpha_channel : bool, optional
609
+ Whether to ignore the alpha channel in the RGB images, by default True
610
+ log_depth : bool, optional
611
+ Whether to log the depth values, by default False
612
+ exclude_polar_fraction : float, optional
613
+ Fraction of polar points to exclude, by default 0.0
614
+
615
+ References
616
+ ----------
617
+ .. [1] Armeni, I., Sax, S., Zamir, A. R., Savarese, S.;
618
+ "Joint 2D-3D-Semantic Data for Indoor Scene Understanding" (2017).
619
+ https://arxiv.org/abs/1702.01105.
620
+ """
621
+
622
+ def __init__(self, dataset_file, ignore_alpha_channel=True, log_depth=False, exclude_polar_fraction=0.0):
623
+
624
+ import h5py as h5
625
+
626
+ self.dataset_file = dataset_file
627
+ self.log_depth = log_depth
628
+ self.exclude_polar_fraction = exclude_polar_fraction
629
+ with h5.File(self.dataset_file, "r") as h5file:
630
+ self.img_rgb = h5file["rgb"][0].shape
631
+ self.img_depth = h5file["depth"][0].shape
632
+ self.num_samples = h5file["rgb"].shape[0]
633
+
634
+ self.mean_in = h5file["mean_rgb"][...]
635
+ self.std_in = h5file["std_rgb"][...]
636
+ self.min_in = h5file["min_rgb"][...]
637
+ self.max_in = h5file["max_rgb"][...]
638
+
639
+ self.mean_out = h5file["mean_depth"][...]
640
+ self.std_out = h5file["std_depth"][...]
641
+ self.min_out = h5file["min_depth"][...]
642
+ self.max_out = h5file["max_depth"][...]
643
+
644
+ if ignore_alpha_channel:
645
+ self.img_rgb = (3, self.img_rgb[1], self.img_rgb[2])
646
+
647
+ # open file and check for
648
+ self.h5file = None
649
+ self.rgb = None
650
+ self.depth = None
651
+
652
+ @property
653
+ def target_shape(self):
654
+ return self.img_depth
655
+
656
+ @property
657
+ def input_shape(self):
658
+ return self.img_rgb
659
+
660
+ def __len__(self):
661
+ return self.num_samples
662
+
663
+ def _init_files(self):
664
+ import h5py as h5
665
+
666
+ self.h5file = h5.File(self.dataset_file, "r")
667
+ self.rgb = self.h5file["rgb"]
668
+ self.depth = self.h5file["depth"]
669
+
670
+ def reset(self):
671
+ self.rgb = None
672
+ self.depth = None
673
+ if self.h5file is not None:
674
+ self.h5file.close()
675
+ del self.h5file
676
+ self.h5file = None
677
+
678
+ def _mask_invalid(self, tar):
679
+ return tar * np.where(tar == tar.max(), 0, 1)
680
+
681
+ def __getitem__(self, idx, mask_invalid=True):
682
+
683
+ if self.h5file is None:
684
+ # init files
685
+ self._init_files()
686
+
687
+ rgb = self.rgb[idx, 0 : self.img_rgb[0], 0 : self.img_rgb[1], 0 : self.img_rgb[2]]
688
+
689
+ depth = self.depth[idx, 0 : self.img_depth[0], 0 : self.img_depth[1]]
690
+ if mask_invalid:
691
+ depth = self._mask_invalid(depth)
692
+
693
+ if self.exclude_polar_fraction > 0:
694
+ hcut = int(self.exclude_polar_fraction * depth.shape[0])
695
+ if hcut > 0:
696
+ depth[0:hcut, :] = 0
697
+ depth[-hcut:, :] = 0
698
+
699
+ if self.log_depth:
700
+ depth = np.log(1 + depth)
701
+
702
+ return rgb, depth
703
+
704
+
705
+ def compute_stats_s2(dataset: Dataset, normalize_target: bool = False):
706
+ """
707
+ Compute stats using parallel welford reduction and quadrature on the sphere.
708
+ The parallel welford reduction follows this article (parallel algorithm): https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance
709
+ """
710
+
711
+ nexamples = len(dataset)
712
+ count = 0
713
+ for isample in range(nexamples):
714
+ token = dataset[isample]
715
+
716
+ # dimension of inp and tar are (3, nlat, nlon)
717
+ inp, tar = token
718
+ nlat = tar.shape[-2]
719
+ nlon = tar.shape[-1]
720
+
721
+ # pre-compute quadrature weights
722
+ if isample == 0:
723
+ quad_weights = get_quadrature_weights(nlat=inp.shape[1], nlon=inp.shape[2], grid="equiangular", tile=True).numpy().astype(np.float64)
724
+
725
+ # this is a special case for the depth dataset
726
+ # TODO: maybe make this an argument
727
+ if normalize_target:
728
+ mask = np.where(tar == 0, 0, 1)
729
+ masked_area = np.sum(mask * quad_weights[np.newaxis, :, :], axis=(-2, -1))
730
+
731
+ # get initial welford values
732
+ if isample == 0:
733
+ # input
734
+ inp_means = np.sum(inp * quad_weights[np.newaxis, :, :], axis=(-2, -1))
735
+ inp_m2s = np.sum(np.square(inp - inp_means[:, np.newaxis, np.newaxis]) * quad_weights[np.newaxis, :, :], axis=(-2, -1))
736
+
737
+ # target
738
+ if normalize_target:
739
+ tar_means = np.sum(mask * tar * quad_weights[np.newaxis, :, :], axis=(-2, -1)) / masked_area
740
+ tar_m2s = np.sum(mask * np.square(tar - tar_means[:, np.newaxis, np.newaxis]) * quad_weights[np.newaxis, :, :], axis=(-2, -1)) / masked_area
741
+
742
+ # update count
743
+ count = 1
744
+
745
+ # do welford update
746
+ else:
747
+ # input
748
+ # get new mean and m2
749
+ inp_mean = np.sum(inp * quad_weights[np.newaxis, :, :], axis=(-2, -1))
750
+ inp_m2 = np.sum(np.square(inp - inp_mean[:, np.newaxis, np.newaxis]) * quad_weights[np.newaxis, :, :], axis=(-2, -1))
751
+ # update welford values
752
+ inp_delta = inp_mean - inp_means
753
+ inp_m2s = inp_m2s + inp_m2 + inp_delta**2 * count / float(count + 1)
754
+ inp_means = inp_means + inp_delta / float(count + 1)
755
+
756
+ # target
757
+ if normalize_target:
758
+ # get new mean and m2
759
+ tar_mean = np.sum(mask * tar * quad_weights[np.newaxis, :, :], axis=(-2, -1)) / masked_area
760
+ tar_m2 = np.sum(mask * np.square(tar - tar_mean[:, np.newaxis, np.newaxis]) * quad_weights[np.newaxis, :, :], axis=(-2, -1)) / masked_area
761
+ # update welford values
762
+ tar_delta = tar_mean - tar_means
763
+ tar_m2s = tar_m2s + tar_m2 + tar_delta**2 * count / float(count + 1)
764
+ tar_means = tar_means + tar_delta / float(count + 1)
765
+
766
+ # update count
767
+ count += 1
768
+
769
+ # finalize
770
+ inp_stds = np.sqrt(inp_m2s / float(count))
771
+ result = (inp_means.astype(np.float32), inp_stds.astype(np.float32))
772
+
773
+ if normalize_target:
774
+ tar_stds = np.sqrt(tar_m2s / float(count))
775
+ result += (tar_means.astype(np.float32), tar_stds.astype(np.float32))
776
+
777
+ return result
.deps/torch_harmonics/filter_basis.py ADDED
@@ -0,0 +1,326 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2022 The torch-harmonics Authors. All rights reserved.
4
+ # SPDX-License-Identifier: BSD-3-Clause
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are met:
8
+ #
9
+ # 1. Redistributions of source code must retain the above copyright notice, this
10
+ # list of conditions and the following disclaimer.
11
+ #
12
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ # this list of conditions and the following disclaimer in the documentation
14
+ # and/or other materials provided with the distribution.
15
+ #
16
+ # 3. Neither the name of the copyright holder nor the names of its
17
+ # contributors may be used to endorse or promote products derived from
18
+ # this software without specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+ #
31
+
32
+ import abc
33
+ from typing import Tuple, Union, Optional
34
+ import math
35
+
36
+ import torch
37
+
38
+ from torch_harmonics.cache import lru_cache
39
+
40
+
41
+ def _circle_dist(x1: torch.Tensor, x2: torch.Tensor):
42
+ return torch.minimum(torch.abs(x1 - x2), torch.abs(2 * math.pi - torch.abs(x1 - x2)))
43
+
44
+
45
+ def _log_factorial(x: torch.Tensor):
46
+ return torch.lgamma(x + 1)
47
+
48
+
49
+ def _factorial(x: torch.Tensor):
50
+ return torch.exp(_log_factorial(x))
51
+
52
+
53
+ class FilterBasis(metaclass=abc.ABCMeta):
54
+ """Abstract base class for a filter basis"""
55
+
56
+ def __init__(
57
+ self,
58
+ kernel_shape: Union[int, Tuple[int], Tuple[int, int]],
59
+ ):
60
+
61
+ self.kernel_shape = kernel_shape
62
+
63
+ @property
64
+ @abc.abstractmethod
65
+ def kernel_size(self):
66
+
67
+ raise NotImplementedError
68
+
69
+ # @abc.abstractmethod
70
+ # def compute_vals(self, r: torch.Tensor, phi: torch.Tensor, r_cutoff: float):
71
+ # """
72
+ # Computes the values of the filter basis
73
+ # """
74
+ # raise NotImplementedError
75
+
76
+ @abc.abstractmethod
77
+ def compute_support_vals(self, r: torch.Tensor, phi: torch.Tensor, r_cutoff: float):
78
+
79
+ raise NotImplementedError
80
+
81
+
82
+ @lru_cache(typed=True, copy=False)
83
+ def get_filter_basis(kernel_shape: Union[int, Tuple[int], Tuple[int, int]], basis_type: str) -> FilterBasis:
84
+ """Factory function to generate the appropriate filter basis"""
85
+
86
+ if basis_type == "piecewise linear":
87
+ return PiecewiseLinearFilterBasis(kernel_shape=kernel_shape)
88
+ elif basis_type == "morlet":
89
+ return MorletFilterBasis(kernel_shape=kernel_shape)
90
+ elif basis_type == "zernike":
91
+ return ZernikeFilterBasis(kernel_shape=kernel_shape)
92
+ else:
93
+ raise ValueError(f"Unknown basis_type {basis_type}")
94
+
95
+
96
+ class PiecewiseLinearFilterBasis(FilterBasis):
97
+ """Tensor-product basis on a disk constructed from piecewise linear basis functions."""
98
+
99
+ def __init__(
100
+ self,
101
+ kernel_shape: Union[int, Tuple[int], Tuple[int, int]],
102
+ ):
103
+
104
+ if isinstance(kernel_shape, int):
105
+ kernel_shape = [kernel_shape]
106
+ if len(kernel_shape) == 1:
107
+ kernel_shape = [kernel_shape[0], 1]
108
+ elif len(kernel_shape) != 2:
109
+ raise ValueError(f"expected kernel_shape to be a list or tuple of length 1 or 2 but got {kernel_shape} instead.")
110
+
111
+ super().__init__(kernel_shape=kernel_shape)
112
+
113
+ @property
114
+ def kernel_size(self):
115
+ """Compute the number of basis functions in the kernel."""
116
+ return (self.kernel_shape[0] // 2) * self.kernel_shape[1] + self.kernel_shape[0] % 2
117
+
118
+ def _compute_support_vals_isotropic(self, r: torch.Tensor, phi: torch.Tensor, r_cutoff: float):
119
+
120
+ # enumerator for basis function
121
+ ikernel = torch.arange(self.kernel_size, device=r.device).reshape(-1, 1, 1)
122
+
123
+ # collocation points
124
+ nr = self.kernel_shape[0]
125
+ dr = 2 * r_cutoff / (nr + 1)
126
+
127
+ # compute the support
128
+ if nr % 2 == 1:
129
+ ir = ikernel * dr
130
+ else:
131
+ ir = (ikernel + 0.5) * dr
132
+
133
+ # find the indices where the rotated position falls into the support of the kernel
134
+ iidx = torch.argwhere(((r - ir).abs() <= dr) & (r <= r_cutoff))
135
+ vals = 1 - (r[iidx[:, 1], iidx[:, 2]] - ir[iidx[:, 0], 0, 0]).abs() / dr
136
+
137
+ return iidx, vals
138
+
139
+ def _compute_support_vals_anisotropic(self, r: torch.Tensor, phi: torch.Tensor, r_cutoff: float):
140
+
141
+ # enumerator for basis function
142
+ ikernel = torch.arange(self.kernel_size, device=r.device).reshape(-1, 1, 1)
143
+
144
+ # collocation points
145
+ nr = self.kernel_shape[0]
146
+ nphi = self.kernel_shape[1]
147
+ dr = 2 * r_cutoff / (nr + 1)
148
+ dphi = 2.0 * math.pi / nphi
149
+
150
+ # disambiguate even and uneven cases and compute the support
151
+ if nr % 2 == 1:
152
+ ir = ((ikernel - 1) // nphi + 1) * dr
153
+ iphi = ((ikernel - 1) % nphi) * dphi - math.pi
154
+ else:
155
+ ir = (ikernel // nphi + 0.5) * dr
156
+ iphi = (ikernel % nphi) * dphi - math.pi
157
+
158
+ # find the indices where the rotated position falls into the support of the kernel
159
+ if nr % 2 == 1:
160
+ # find the support
161
+ cond_r = ((r - ir).abs() <= dr) & (r <= r_cutoff)
162
+ cond_phi = (ikernel == 0) | (_circle_dist(phi, iphi).abs() <= dphi)
163
+ # find indices where conditions are met
164
+ iidx = torch.argwhere(cond_r & cond_phi)
165
+ # compute the distance to the collocation points
166
+ dist_r = (r[iidx[:, 1], iidx[:, 2]] - ir[iidx[:, 0], 0, 0]).abs()
167
+ dist_phi = _circle_dist(phi[iidx[:, 1], iidx[:, 2]], iphi[iidx[:, 0], 0, 0])
168
+ # compute the value of the basis functions
169
+ vals = 1 - dist_r / dr
170
+ vals *= torch.where((iidx[:, 0] > 0), (1 - dist_phi / dphi), 1.0)
171
+
172
+ else:
173
+ # in the even case, the inner basis functions overlap into areas with a negative areas
174
+ rn = -r
175
+ phin = torch.where(phi + math.pi >= math.pi, phi - math.pi, phi + math.pi)
176
+ # find the support
177
+ cond_r = ((r - ir).abs() <= dr) & (r <= r_cutoff)
178
+ cond_phi = _circle_dist(phi, iphi).abs() <= dphi
179
+ cond_rn = ((rn - ir).abs() <= dr) & (rn <= r_cutoff)
180
+ cond_phin = _circle_dist(phin, iphi) <= dphi
181
+ # find indices where conditions are met
182
+ iidx = torch.argwhere((cond_r & cond_phi) | (cond_rn & cond_phin))
183
+
184
+ dist_r = (r[iidx[:, 1], iidx[:, 2]] - ir[iidx[:, 0], 0, 0]).abs()
185
+ dist_phi = _circle_dist(phi[iidx[:, 1], iidx[:, 2]], iphi[iidx[:, 0], 0, 0])
186
+ dist_rn = (rn[iidx[:, 1], iidx[:, 2]] - ir[iidx[:, 0], 0, 0]).abs()
187
+ dist_phin = _circle_dist(phin[iidx[:, 1], iidx[:, 2]], iphi[iidx[:, 0], 0, 0])
188
+ # compute the value of the basis functions
189
+ vals = cond_r[iidx[:, 0], iidx[:, 1], iidx[:, 2]] * (1 - dist_r / dr)
190
+ vals *= cond_phi[iidx[:, 0], iidx[:, 1], iidx[:, 2]] * (1 - dist_phi / dphi)
191
+ valsn = cond_rn[iidx[:, 0], iidx[:, 1], iidx[:, 2]] * (1 - dist_rn / dr)
192
+ valsn *= cond_phin[iidx[:, 0], iidx[:, 1], iidx[:, 2]] * (1 - dist_phin / dphi)
193
+ vals += valsn
194
+
195
+ return iidx, vals
196
+
197
+ def compute_support_vals(self, r: torch.Tensor, phi: torch.Tensor, r_cutoff: float):
198
+ """Computes the index set that falls into the kernel's support and returns both indices and values."""
199
+
200
+ if self.kernel_shape[1] > 1:
201
+ return self._compute_support_vals_anisotropic(r, phi, r_cutoff=r_cutoff)
202
+ else:
203
+ return self._compute_support_vals_isotropic(r, phi, r_cutoff=r_cutoff)
204
+
205
+
206
+ class MorletFilterBasis(FilterBasis):
207
+ """Morlet-style filter basis on the disk. A Gaussian is multiplied with a Fourier basis in x and y directions."""
208
+
209
+ def __init__(
210
+ self,
211
+ kernel_shape: Union[int, Tuple[int], Tuple[int, int]],
212
+ ):
213
+
214
+ if isinstance(kernel_shape, int):
215
+ kernel_shape = [kernel_shape, kernel_shape]
216
+ if len(kernel_shape) != 2:
217
+ raise ValueError(f"expected kernel_shape to be a list or tuple of 2 but got {kernel_shape} instead.")
218
+
219
+ super().__init__(kernel_shape=kernel_shape)
220
+
221
+ @property
222
+ def kernel_size(self):
223
+
224
+ return self.kernel_shape[0] * self.kernel_shape[1]
225
+
226
+ def gaussian_window(self, r: torch.Tensor, width: float = 1.0):
227
+
228
+ return 1 / (2 * math.pi * width**2) * torch.exp(-0.5 * r**2 / (width**2))
229
+
230
+ def hann_window(self, r: torch.Tensor, width: float = 1.0):
231
+
232
+ return torch.cos(0.5 * torch.pi * r / width) ** 2
233
+
234
+ def compute_support_vals(self, r: torch.Tensor, phi: torch.Tensor, r_cutoff: float, width: float = 1.0):
235
+
236
+ # enumerator for basis function
237
+ ikernel = torch.arange(self.kernel_size, device=r.device).reshape(-1, 1, 1)
238
+ nkernel = ikernel % self.kernel_shape[1]
239
+ mkernel = ikernel // self.kernel_shape[1]
240
+
241
+ # get relevant indices
242
+ iidx = torch.argwhere((r <= r_cutoff) & torch.full_like(ikernel, True, dtype=torch.bool, device=r.device))
243
+
244
+ # get corresponding r, phi, x and y coordinates
245
+ r = r[iidx[:, 1], iidx[:, 2]] / r_cutoff
246
+ phi = phi[iidx[:, 1], iidx[:, 2]]
247
+ x = r * torch.sin(phi)
248
+ y = r * torch.cos(phi)
249
+ n = nkernel[iidx[:, 0], 0, 0]
250
+ m = mkernel[iidx[:, 0], 0, 0]
251
+
252
+ harmonic = torch.where(n % 2 == 1, torch.sin(torch.ceil(n / 2) * math.pi * x / width), torch.cos(torch.ceil(n / 2) * math.pi * x / width))
253
+ harmonic *= torch.where(m % 2 == 1, torch.sin(torch.ceil(m / 2) * math.pi * y / width), torch.cos(torch.ceil(m / 2) * math.pi * y / width))
254
+
255
+ # computes the envelope. To ensure that the curve is roughly 0 at the boundary, we rescale the Gaussian by 0.25
256
+ vals = self.hann_window(r, width=width) * harmonic
257
+
258
+ return iidx, vals
259
+
260
+
261
+ class ZernikeFilterBasis(FilterBasis):
262
+ """Zernike polynomials which are defined on the disk. See https://en.wikipedia.org/wiki/Zernike_polynomials"""
263
+
264
+ def __init__(
265
+ self,
266
+ kernel_shape: Union[int, Tuple[int]],
267
+ ):
268
+
269
+ if isinstance(kernel_shape, tuple) or isinstance(kernel_shape, list):
270
+ kernel_shape = kernel_shape[0]
271
+ if not isinstance(kernel_shape, int):
272
+ raise ValueError(f"expected kernel_shape to be an integer but got {kernel_shape} instead.")
273
+
274
+ super().__init__(kernel_shape=kernel_shape)
275
+
276
+ @property
277
+ def kernel_size(self):
278
+
279
+ return (self.kernel_shape * (self.kernel_shape + 1)) // 2
280
+
281
+ def zernikeradial(self, r: torch.Tensor, n: torch.Tensor, m: torch.Tensor):
282
+
283
+ out = torch.zeros_like(r)
284
+ bound = (n - m) // 2 + 1
285
+ max_bound = bound.max().item()
286
+
287
+ for k in range(max_bound):
288
+
289
+ inc = (-1) ** k * _factorial(n - k) * r ** (n - 2 * k) / (math.factorial(k) * _factorial((n + m) // 2 - k) * _factorial((n - m) // 2 - k))
290
+ out += torch.where(k < bound, inc, 0.0)
291
+
292
+ return out
293
+
294
+ def zernikepoly(self, r: torch.Tensor, phi: torch.Tensor, n: torch.Tensor, l: torch.Tensor):
295
+
296
+ m = 2 * l - n
297
+ return torch.where(m < 0, self.zernikeradial(r, n, -m) * torch.sin(m * phi), self.zernikeradial(r, n, m) * torch.cos(m * phi))
298
+
299
+ def compute_support_vals(self, r: torch.Tensor, phi: torch.Tensor, r_cutoff: float, width: float = 0.25):
300
+
301
+ # enumerator for basis function
302
+ ikernel = torch.arange(self.kernel_size, device=r.device).reshape(-1, 1, 1)
303
+
304
+ # get relevant indices
305
+ iidx = torch.argwhere((r <= r_cutoff) & torch.full_like(ikernel, True, dtype=torch.bool, device=r.device))
306
+
307
+ # indexing logic for zernike polynomials
308
+ # the total index is given by (n * (n + 2) + l ) // 2 which needs to be reversed
309
+ # precompute shifts in the level of the "pyramid"
310
+ nshifts = torch.arange(self.kernel_shape, device=r.device)
311
+ nshifts = (nshifts + 1) * nshifts // 2
312
+ # find the level and position within the pyramid
313
+ nkernel = torch.searchsorted(nshifts, ikernel, right=True) - 1
314
+ lkernel = ikernel - nshifts[nkernel]
315
+ # mkernel = 2 * ikernel - nkernel * (nkernel + 2)
316
+
317
+ # get corresponding coordinates and n and l indices
318
+ r = r[iidx[:, 1], iidx[:, 2]] / r_cutoff
319
+ phi = phi[iidx[:, 1], iidx[:, 2]]
320
+ n = nkernel[iidx[:, 0], 0, 0]
321
+ l = lkernel[iidx[:, 0], 0, 0]
322
+
323
+ # computes the Zernike polynomials using helper functions
324
+ vals = self.zernikepoly(r, phi, n, l)
325
+
326
+ return iidx, vals
.deps/torch_harmonics/legendre.py ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2022 The torch-harmonics Authors. All rights reserved.
4
+ # SPDX-License-Identifier: BSD-3-Clause
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are met:
8
+ #
9
+ # 1. Redistributions of source code must retain the above copyright notice, this
10
+ # list of conditions and the following disclaimer.
11
+ #
12
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ # this list of conditions and the following disclaimer in the documentation
14
+ # and/or other materials provided with the distribution.
15
+ #
16
+ # 3. Neither the name of the copyright holder nor the names of its
17
+ # contributors may be used to endorse or promote products derived from
18
+ # this software without specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+ #
31
+
32
+ from typing import Optional
33
+ import math
34
+ import torch
35
+
36
+ from torch_harmonics.cache import lru_cache
37
+
38
+
39
+ def clm(l: int, m: int) -> float:
40
+ """Defines the normalization factor to orthonormalize the Spherical Harmonics."""
41
+ return math.sqrt((2*l + 1) / 4 / math.pi) * math.sqrt(math.factorial(l-m) / math.factorial(l+m))
42
+
43
+ def legpoly(mmax: int, lmax: int, x: torch.Tensor, norm: Optional[str]="ortho", inverse: Optional[bool]=False, csphase: Optional[bool]=True) -> torch.Tensor:
44
+ """
45
+ Computes the values of (-1)^m c^l_m P^l_m(x) at the positions specified by x.
46
+ The resulting tensor has shape (mmax, lmax, len(x)). The Condon-Shortley Phase (-1)^m
47
+ can be turned off optionally.
48
+
49
+ Parameters
50
+ -----------
51
+ mmax: int
52
+ Maximum order of the spherical harmonics
53
+ lmax: int
54
+ Maximum degree of the spherical harmonics
55
+ x: torch.Tensor
56
+ Tensor of positions at which to evaluate the Legendre polynomials
57
+ norm: Optional[str]
58
+ Normalization of the Legendre polynomials
59
+ inverse: Optional[bool]
60
+ Whether to compute the inverse Legendre polynomials
61
+ csphase: Optional[bool]
62
+ Whether to apply the Condon-Shortley phase (-1)^m
63
+
64
+ Returns
65
+ -------
66
+ out: torch.Tensor
67
+ Tensor of Legendre polynomial values
68
+
69
+ References
70
+ ----------
71
+ [1] Schaeffer, N.; Efficient spherical harmonic transforms aimed at pseudospectral numerical simulations, G3: Geochemistry, Geophysics, Geosystems.
72
+ [2] Rapp, R.H.; A Fortran Program for the Computation of Gravimetric Quantities from High Degree Spherical Harmonic Expansions, Ohio State University Columbus; report; 1982;
73
+ https://apps.dtic.mil/sti/citations/ADA123406
74
+ [3] Schrama, E.; Orbit integration based upon interpolated gravitational gradients
75
+ """
76
+
77
+ # compute the tensor P^m_n:
78
+ nmax = max(mmax,lmax)
79
+ vdm = torch.zeros((nmax, nmax, len(x)), dtype=torch.float64, device=x.device, requires_grad=False)
80
+
81
+ norm_factor = 1.0 if norm == "ortho" else math.sqrt(4 * math.pi)
82
+ norm_factor = 1.0 / norm_factor if inverse else norm_factor
83
+
84
+ # initial values to start the recursion
85
+ vdm[0,0,:] = norm_factor / math.sqrt(4 * math.pi)
86
+
87
+ # fill the diagonal and the lower diagonal
88
+ for l in range(1, nmax):
89
+ vdm[l-1, l, :] = math.sqrt(2*l + 1) * x * vdm[l-1, l-1, :]
90
+ vdm[l, l, :] = torch.sqrt( (2*l + 1) * (1 + x) * (1 - x) / 2 / l ) * vdm[l-1, l-1, :]
91
+
92
+ # fill the remaining values on the upper triangle and multiply b
93
+ for l in range(2, nmax):
94
+ for m in range(0, l-1):
95
+ vdm[m, l, :] = x * math.sqrt((2*l - 1) / (l - m) * (2*l + 1) / (l + m)) * vdm[m, l-1, :] \
96
+ - math.sqrt((l + m - 1) / (l - m) * (2*l + 1) / (2*l - 3) * (l - m - 1) / (l + m)) * vdm[m, l-2, :]
97
+
98
+ if norm == "schmidt":
99
+ for l in range(0, nmax):
100
+ if inverse:
101
+ vdm[:, l, : ] = vdm[:, l, : ] * math.sqrt(2*l + 1)
102
+ else:
103
+ vdm[:, l, : ] = vdm[:, l, : ] / math.sqrt(2*l + 1)
104
+
105
+ vdm = vdm[:mmax, :lmax]
106
+
107
+ if csphase:
108
+ for m in range(1, mmax, 2):
109
+ vdm[m] *= -1
110
+
111
+ return vdm
112
+
113
+ @lru_cache(typed=True, copy=True)
114
+ def _precompute_legpoly(mmax: int , lmax: int, t: torch.Tensor,
115
+ norm: Optional[str]="ortho", inverse: Optional[bool]=False, csphase: Optional[bool]=True) -> torch.Tensor:
116
+ """
117
+ Computes the values of (-1)^m c^l_m P^l_m(\cos \theta) at the positions specified by t (theta).
118
+ The resulting tensor has shape (mmax, lmax, len(x)). The Condon-Shortley Phase (-1)^m
119
+ can be turned off optionally.
120
+
121
+ Parameters
122
+ -----------
123
+ mmax: int
124
+ Maximum order of the spherical harmonics
125
+ lmax: int
126
+ Maximum degree of the spherical harmonics
127
+ t: torch.Tensor
128
+ Tensor of positions at which to evaluate the Legendre polynomials
129
+ norm: Optional[str]
130
+ Normalization of the Legendre polynomials
131
+ inverse: Optional[bool]
132
+ Whether to compute the inverse Legendre polynomials
133
+ csphase: Optional[bool]
134
+ Whether to apply the Condon-Shortley phase (-1)^m
135
+
136
+ Returns
137
+ -------
138
+ out: torch.Tensor
139
+ Tensor of Legendre polynomial values
140
+
141
+ References
142
+ ----------
143
+ [1] Schaeffer, N.; Efficient spherical harmonic transforms aimed at pseudospectral numerical simulations, G3: Geochemistry, Geophysics, Geosystems.
144
+ [2] Rapp, R.H.; A Fortran Program for the Computation of Gravimetric Quantities from High Degree Spherical Harmonic Expansions, Ohio State University Columbus; report; 1982;
145
+ https://apps.dtic.mil/sti/citations/ADA123406
146
+ [3] Schrama, E.; Orbit integration based upon interpolated gravitational gradients
147
+ """
148
+
149
+ return legpoly(mmax, lmax, torch.cos(t), norm=norm, inverse=inverse, csphase=csphase)
150
+
151
+ @lru_cache(typed=True, copy=True)
152
+ def _precompute_dlegpoly(mmax: int, lmax: int, t: torch.Tensor,
153
+ norm: Optional[str]="ortho", inverse: Optional[bool]=False, csphase: Optional[bool]=True) -> torch.Tensor:
154
+ """
155
+ Computes the values of the derivatives $\frac{d}{d \theta} P^m_l(\cos \theta)$
156
+ at the positions specified by t (theta), as well as $\frac{1}{\sin \theta} P^m_l(\cos \theta)$,
157
+ needed for the computation of the vector spherical harmonics. The resulting tensor has shape
158
+ (2, mmax, lmax, len(t)).
159
+
160
+ Parameters
161
+ -----------
162
+ mmax: int
163
+ Maximum order of the spherical harmonics
164
+ lmax: int
165
+ Maximum degree of the spherical harmonics
166
+ t: torch.Tensor
167
+ Tensor of positions at which to evaluate the Legendre polynomials
168
+ norm: Optional[str]
169
+ Normalization of the Legendre polynomials
170
+ inverse: Optional[bool]
171
+ Whether to compute the inverse Legendre polynomials
172
+ csphase: Optional[bool]
173
+ Whether to apply the Condon-Shortley phase (-1)^m
174
+
175
+ Returns
176
+ -------
177
+ out: torch.Tensor
178
+ Tensor of Legendre polynomial values
179
+
180
+ References
181
+ ----------
182
+ [2] Wang, B., Wang, L., Xie, Z.; Accurate calculation of spherical and vector spherical harmonic expansions via spectral element grids; Adv Comput Math.
183
+ """
184
+
185
+ pct = _precompute_legpoly(mmax+1, lmax+1, t, norm=norm, inverse=inverse, csphase=False)
186
+
187
+ dpct = torch.zeros((2, mmax, lmax, len(t)), dtype=torch.float64, device=t.device, requires_grad=False)
188
+
189
+ # fill the derivative terms wrt theta
190
+ for l in range(0, lmax):
191
+
192
+ # m = 0
193
+ dpct[0, 0, l] = - math.sqrt(l*(l+1)) * pct[1, l]
194
+
195
+ # 0 < m < l
196
+ for m in range(1, min(l, mmax)):
197
+ dpct[0, m, l] = 0.5 * ( math.sqrt((l+m)*(l-m+1)) * pct[m-1, l] - math.sqrt((l-m)*(l+m+1)) * pct[m+1, l] )
198
+
199
+ # m == l
200
+ if mmax > l:
201
+ dpct[0, l, l] = math.sqrt(l/2) * pct[l-1, l]
202
+
203
+ # fill the - 1j m P^m_l / sin(phi). as this component is purely imaginary,
204
+ # we won't store it explicitly in a complex array
205
+ for m in range(1, min(l+1, mmax)):
206
+ # this component is implicitly complex
207
+ # we do not divide by m here as this cancels with the derivative of the exponential
208
+ dpct[1, m, l] = 0.5 * math.sqrt((2*l+1)/(2*l+3)) * \
209
+ ( math.sqrt((l-m+1)*(l-m+2)) * pct[m-1, l+1] + math.sqrt((l+m+1)*(l+m+2)) * pct[m+1, l+1] )
210
+
211
+ if csphase:
212
+ for m in range(1, mmax, 2):
213
+ dpct[:, m] *= -1
214
+
215
+ return dpct
.deps/torch_harmonics/plotting.py ADDED
@@ -0,0 +1,265 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+
3
+ # SPDX-FileCopyrightText: Copyright (c) 2025 The torch-harmonics Authors. All rights reserved.
4
+ # SPDX-License-Identifier: BSD-3-Clause
5
+ #
6
+ # Redistribution and use in source and binary forms, with or without
7
+ # modification, are permitted provided that the following conditions are met:
8
+ #
9
+ # 1. Redistributions of source code must retain the above copyright notice, this
10
+ # list of conditions and the following disclaimer.
11
+ #
12
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ # this list of conditions and the following disclaimer in the documentation
14
+ # and/or other materials provided with the distribution.
15
+ #
16
+ # 3. Neither the name of the copyright holder nor the names of its
17
+ # contributors may be used to endorse or promote products derived from
18
+ # this software without specific prior written permission.
19
+ #
20
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+ #
31
+
32
+ import numpy as np
33
+ import os
34
+
35
+ # guarded imports
36
+ try:
37
+ import matplotlib.pyplot as plt
38
+ except ImportError as err:
39
+ plt = None
40
+
41
+ try:
42
+ import cartopy
43
+ import cartopy.crs as ccrs
44
+ except ImportError as err:
45
+ cartopy = None
46
+ ccrs = None
47
+
48
+
49
+ def check_plotting_dependencies():
50
+ if plt is None:
51
+ raise ImportError("matplotlib is required for plotting functions. Install it with 'pip install matplotlib'")
52
+ if cartopy is None:
53
+ raise ImportError("cartopy is required for map plotting. Install it with 'pip install cartopy'")
54
+
55
+
56
+ def get_projection(
57
+ projection,
58
+ central_latitude=0,
59
+ central_longitude=0,
60
+ ):
61
+ """
62
+ Get a cartopy projection object for map plotting.
63
+
64
+ Parameters
65
+ -----------
66
+ projection : str
67
+ Projection type ("orthographic", "robinson", "platecarree", "mollweide")
68
+ central_latitude : float, optional
69
+ Central latitude for the projection, by default 0
70
+ central_longitude : float, optional
71
+ Central longitude for the projection, by default 0
72
+
73
+ Returns
74
+ -------
75
+ cartopy.crs.Projection
76
+ Cartopy projection object
77
+
78
+ Raises
79
+ ------
80
+ ValueError
81
+ If projection type is not supported
82
+ """
83
+ if projection == "orthographic":
84
+ proj = ccrs.Orthographic(central_latitude=central_latitude, central_longitude=central_longitude)
85
+ elif projection == "robinson":
86
+ proj = ccrs.Robinson(central_longitude=central_longitude)
87
+ elif projection == "platecarree":
88
+ proj = ccrs.PlateCarree(central_longitude=central_longitude)
89
+ elif projection == "mollweide":
90
+ proj = ccrs.Mollweide(central_longitude=central_longitude)
91
+ else:
92
+ raise ValueError(f"Unknown projection mode {projection}")
93
+
94
+ return proj
95
+
96
+
97
+ def plot_sphere(
98
+ data, fig=None, projection="robinson", cmap="RdBu", title=None, colorbar=False, coastlines=False, gridlines=False, central_latitude=0, central_longitude=0, lon=None, lat=None, **kwargs
99
+ ):
100
+ """
101
+ Plots a function defined on the sphere using pcolormesh
102
+
103
+ Parameters
104
+ -----------
105
+ data : numpy.ndarray or torch.Tensor
106
+ Data to plot with shape (nlat, nlon)
107
+ fig : matplotlib.figure.Figure, optional
108
+ Figure to plot on, by default None (creates new figure)
109
+ projection : str, optional
110
+ Map projection type, by default "robinson"
111
+ cmap : str, optional
112
+ Colormap name, by default "RdBu"
113
+ title : str, optional
114
+ Plot title, by default None
115
+ colorbar : bool, optional
116
+ Whether to add a colorbar, by default False
117
+ coastlines : bool, optional
118
+ Whether to add coastlines, by default False
119
+ gridlines : bool, optional
120
+ Whether to add gridlines, by default False
121
+ central_latitude : float, optional
122
+ Central latitude for projection, by default 0
123
+ central_longitude : float, optional
124
+ Central longitude for projection, by default 0
125
+ lon : numpy.ndarray, optional
126
+ Longitude coordinates, by default None (auto-generated)
127
+ lat : numpy.ndarray, optional
128
+ Latitude coordinates, by default None (auto-generated)
129
+ **kwargs
130
+ Additional arguments passed to pcolormesh
131
+
132
+ Returns
133
+ -------
134
+ matplotlib.collections.QuadMesh
135
+ The plotted image object
136
+ """
137
+
138
+ # make sure cartopy exist
139
+ check_plotting_dependencies()
140
+
141
+ if fig == None:
142
+ fig = plt.figure()
143
+
144
+ nlat = data.shape[-2]
145
+ nlon = data.shape[-1]
146
+ if lon is None:
147
+ lon = np.linspace(0, 2 * np.pi, nlon + 1)[:-1]
148
+ if lat is None:
149
+ lat = np.linspace(np.pi / 2.0, -np.pi / 2.0, nlat)
150
+ Lon, Lat = np.meshgrid(lon, lat)
151
+
152
+ # convert radians to degrees
153
+ Lon = Lon * 180 / np.pi
154
+ Lat = Lat * 180 / np.pi
155
+
156
+ # get the projection. Latitude is shifted to match plot_sphere
157
+ proj = get_projection(projection, central_latitude=central_latitude, central_longitude=central_longitude)
158
+
159
+ ax = fig.add_subplot(projection=proj)
160
+
161
+ # contour data over the map.
162
+ im = ax.pcolormesh(Lon, Lat, data, cmap=cmap, transform=ccrs.PlateCarree(), antialiased=False, **kwargs)
163
+
164
+ # add features if requested
165
+ if coastlines:
166
+ ax.add_feature(cartopy.feature.COASTLINE, edgecolor="white", facecolor="none", linewidth=1.5)
167
+
168
+ # add colorbar if requested
169
+ if colorbar:
170
+ plt.colorbar(im)
171
+
172
+ # add gridlines
173
+ if gridlines:
174
+ gl = ax.gridlines(crs=ccrs.PlateCarree(), draw_labels=False, linewidth=1, color="gray", alpha=0.6, linestyle="--")
175
+
176
+ # add title with smaller font
177
+ plt.title(title, y=1.05, fontsize=8)
178
+
179
+ return im
180
+
181
+
182
+ def imshow_sphere(data, fig=None, projection="robinson", title=None, central_latitude=0, central_longitude=0, **kwargs):
183
+ """
184
+ Displays an image on the sphere
185
+
186
+ Parameters
187
+ -----------
188
+ data : numpy.ndarray or torch.Tensor
189
+ Data to display with shape (nlat, nlon)
190
+ fig : matplotlib.figure.Figure, optional
191
+ Figure to plot on, by default None (creates new figure)
192
+ projection : str, optional
193
+ Map projection type, by default "robinson"
194
+ title : str, optional
195
+ Plot title, by default None
196
+ central_latitude : float, optional
197
+ Central latitude for projection, by default 0
198
+ central_longitude : float, optional
199
+ Central longitude for projection, by default 0
200
+ **kwargs
201
+ Additional arguments passed to imshow
202
+
203
+ Returns
204
+ -------
205
+ matplotlib.image.AxesImage
206
+ The displayed image object
207
+ """
208
+
209
+ # make sure cartopy exist
210
+ check_plotting_dependencies()
211
+
212
+ if fig == None:
213
+ fig = plt.figure()
214
+
215
+ # get the projection. Latitude is shifted to match plot_sphere
216
+ proj = get_projection(projection, central_latitude=central_latitude, central_longitude=central_longitude + 180)
217
+
218
+ ax = fig.add_subplot(projection=proj)
219
+
220
+ # contour data over the map.
221
+ im = ax.imshow(data, transform=ccrs.PlateCarree(), **kwargs)
222
+
223
+ # add title
224
+ plt.title(title, y=1.05)
225
+
226
+ return im
227
+
228
+
229
+ # def plot_data(data,
230
+ # fig=None,
231
+ # cmap="RdBu",
232
+ # title=None,
233
+ # colorbar=False,
234
+ # coastlines=False,
235
+ # central_longitude=0,
236
+ # lon=None,
237
+ # lat=None,
238
+ # **kwargs):
239
+ # if fig == None:
240
+ # fig = plt.figure()
241
+
242
+ # nlat = data.shape[-2]
243
+ # nlon = data.shape[-1]
244
+ # if lon is None:
245
+ # lon = np.linspace(0, 2*np.pi, nlon+1)[:-1]
246
+ # if lat is None:
247
+ # lat = np.linspace(np.pi/2., -np.pi/2., nlat)
248
+ # Lon, Lat = np.meshgrid(lon, lat)
249
+
250
+ # proj = ccrs.Robinson(central_longitude=central_longitude)
251
+ # # proj = ccrs.Mollweide(central_longitude=central_longitude)
252
+
253
+ # ax = fig.add_subplot(projection=proj)
254
+ # Lon = Lon*180/np.pi
255
+ # Lat = Lat*180/np.pi
256
+
257
+ # # contour data over the map.
258
+ # im = ax.pcolormesh(Lon, Lat, data, cmap=cmap, transform=ccrs.PlateCarree(), antialiased=False, **kwargs)
259
+ # if coastlines:
260
+ # ax.add_feature(cartopy.feature.COASTLINE, edgecolor='white', facecolor='none', linewidth=1.5)
261
+ # if colorbar:
262
+ # plt.colorbar(im)
263
+ # plt.title(title, y=1.05)
264
+
265
+ # return im