Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .deps/torch_harmonics-0.8.0.dist-info/INSTALLER +1 -0
- .deps/torch_harmonics-0.8.0.dist-info/METADATA +324 -0
- .deps/torch_harmonics-0.8.0.dist-info/RECORD +86 -0
- .deps/torch_harmonics-0.8.0.dist-info/REQUESTED +0 -0
- .deps/torch_harmonics-0.8.0.dist-info/WHEEL +5 -0
- .deps/torch_harmonics-0.8.0.dist-info/licenses/AUTHORS +12 -0
- .deps/torch_harmonics-0.8.0.dist-info/licenses/LICENSE +27 -0
- .deps/torch_harmonics-0.8.0.dist-info/top_level.txt +1 -0
- .deps/torch_harmonics/__init__.py +40 -0
- .deps/torch_harmonics/_disco_convolution.py +212 -0
- .deps/torch_harmonics/_neighborhood_attention.py +589 -0
- .deps/torch_harmonics/attention.py +382 -0
- .deps/torch_harmonics/cache.py +74 -0
- .deps/torch_harmonics/convolution.py +666 -0
- .deps/torch_harmonics/csrc/attention/attention.cuh +50 -0
- .deps/torch_harmonics/csrc/attention/attention_bwd_cuda.cu +960 -0
- .deps/torch_harmonics/csrc/attention/attention_fwd_cuda.cu +525 -0
- .deps/torch_harmonics/csrc/attention/attention_interface.cu +38 -0
- .deps/torch_harmonics/csrc/attention/attention_utils.cu +180 -0
- .deps/torch_harmonics/csrc/attention/attention_utils.cuh +373 -0
- .deps/torch_harmonics/csrc/attention/cudamacro.h +47 -0
- .deps/torch_harmonics/csrc/disco/disco.h +39 -0
- .deps/torch_harmonics/csrc/disco/disco_cuda.cuh +54 -0
- .deps/torch_harmonics/csrc/disco/disco_cuda_bwd.cu +281 -0
- .deps/torch_harmonics/csrc/disco/disco_cuda_fwd.cu +265 -0
- .deps/torch_harmonics/csrc/disco/disco_helpers.cpp +149 -0
- .deps/torch_harmonics/csrc/disco/disco_interface.cu +38 -0
- .deps/torch_harmonics/distributed/__init__.py +58 -0
- .deps/torch_harmonics/distributed/distributed_convolution.py +474 -0
- .deps/torch_harmonics/distributed/distributed_resample.py +247 -0
- .deps/torch_harmonics/distributed/distributed_sht.py +612 -0
- .deps/torch_harmonics/distributed/primitives.py +470 -0
- .deps/torch_harmonics/distributed/utils.py +92 -0
- .deps/torch_harmonics/examples/__init__.py +36 -0
- .deps/torch_harmonics/examples/losses.py +417 -0
- .deps/torch_harmonics/examples/metrics.py +291 -0
- .deps/torch_harmonics/examples/models/__init__.py +36 -0
- .deps/torch_harmonics/examples/models/_layers.py +621 -0
- .deps/torch_harmonics/examples/models/lsno.py +623 -0
- .deps/torch_harmonics/examples/models/s2segformer.py +810 -0
- .deps/torch_harmonics/examples/models/s2transformer.py +589 -0
- .deps/torch_harmonics/examples/models/s2unet.py +610 -0
- .deps/torch_harmonics/examples/models/sfno.py +423 -0
- .deps/torch_harmonics/examples/pde_dataset.py +159 -0
- .deps/torch_harmonics/examples/pde_sphere.py +227 -0
- .deps/torch_harmonics/examples/shallow_water_equations.py +394 -0
- .deps/torch_harmonics/examples/stanford_2d3ds_dataset.py +777 -0
- .deps/torch_harmonics/filter_basis.py +326 -0
- .deps/torch_harmonics/legendre.py +215 -0
- .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 |
+
[](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 |
+
[](https://github.com/NVIDIA/torch-harmonics/actions/workflows/tests.yml)
|
| 76 |
+
[](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
|