| import torch
|
| import torch.nn as nn
|
| import math
|
| import copy
|
|
|
| from torch.nn import Linear
|
| from .so3 import SO3_Embedding
|
| from .radial_function import RadialFunction
|
|
|
|
|
| class SO2_m_Convolution(torch.nn.Module):
|
| """
|
| SO(2) Conv: Perform an SO(2) convolution on features corresponding to +- m
|
|
|
| Args:
|
| m (int): Order of the spherical harmonic coefficients
|
| sphere_channels (int): Number of spherical channels
|
| m_output_channels (int): Number of output channels used during the SO(2) conv
|
| lmax_list (list:int): List of degrees (l) for each resolution
|
| mmax_list (list:int): List of orders (m) for each resolution
|
| """
|
| def __init__(
|
| self,
|
| m,
|
| sphere_channels,
|
| m_output_channels,
|
| lmax_list,
|
| mmax_list
|
| ):
|
| super(SO2_m_Convolution, self).__init__()
|
|
|
| self.m = m
|
| self.sphere_channels = sphere_channels
|
| self.m_output_channels = m_output_channels
|
| self.lmax_list = lmax_list
|
| self.mmax_list = mmax_list
|
| self.num_resolutions = len(self.lmax_list)
|
|
|
| num_channels = 0
|
| for i in range(self.num_resolutions):
|
| num_coefficents = 0
|
| if self.mmax_list[i] >= self.m:
|
| num_coefficents = self.lmax_list[i] - self.m + 1
|
| num_channels = num_channels + num_coefficents * self.sphere_channels
|
| assert num_channels > 0
|
|
|
| self.fc = Linear(num_channels,
|
| 2 * self.m_output_channels * (num_channels // self.sphere_channels),
|
| bias=False)
|
| self.fc.weight.data.mul_(1 / math.sqrt(2))
|
|
|
|
|
| def forward(self, x_m):
|
| x_m = self.fc(x_m)
|
| x_r = x_m.narrow(2, 0, self.fc.out_features // 2)
|
| x_i = x_m.narrow(2, self.fc.out_features // 2, self.fc.out_features // 2)
|
| x_m_r = x_r.narrow(1, 0, 1) - x_i.narrow(1, 1, 1)
|
| x_m_i = x_r.narrow(1, 1, 1) + x_i.narrow(1, 0, 1)
|
| x_out = torch.cat((x_m_r, x_m_i), dim=1)
|
|
|
| return x_out
|
|
|
|
|
| class SO2_Convolution(torch.nn.Module):
|
| """
|
| SO(2) Block: Perform SO(2) convolutions for all m (orders)
|
|
|
| Args:
|
| sphere_channels (int): Number of spherical channels
|
| m_output_channels (int): Number of output channels used during the SO(2) conv
|
| lmax_list (list:int): List of degrees (l) for each resolution
|
| mmax_list (list:int): List of orders (m) for each resolution
|
| mappingReduced (CoefficientMappingModule): Used to extract a subset of m components
|
| internal_weights (bool): If True, not using radial function to multiply inputs features
|
| edge_channels_list (list:int): List of sizes of invariant edge embedding. For example, [input_channels, hidden_channels, hidden_channels].
|
| extra_m0_output_channels (int): If not None, return `out_embedding` (SO3_Embedding) and `extra_m0_features` (Tensor).
|
| """
|
| def __init__(
|
| self,
|
| sphere_channels,
|
| m_output_channels,
|
| lmax_list,
|
| mmax_list,
|
| mappingReduced,
|
| internal_weights=True,
|
| edge_channels_list=None,
|
| extra_m0_output_channels=None
|
| ):
|
| super(SO2_Convolution, self).__init__()
|
| self.sphere_channels = sphere_channels
|
| self.m_output_channels = m_output_channels
|
| self.lmax_list = lmax_list
|
| self.mmax_list = mmax_list
|
| self.mappingReduced = mappingReduced
|
| self.num_resolutions = len(lmax_list)
|
| self.internal_weights = internal_weights
|
| self.edge_channels_list = copy.deepcopy(edge_channels_list)
|
| self.extra_m0_output_channels = extra_m0_output_channels
|
|
|
| num_channels_rad = 0
|
|
|
| num_channels_m0 = 0
|
| for i in range(self.num_resolutions):
|
| num_coefficients = self.lmax_list[i] + 1
|
| num_channels_m0 = num_channels_m0 + num_coefficients * self.sphere_channels
|
|
|
|
|
| m0_output_channels = self.m_output_channels * (num_channels_m0 // self.sphere_channels)
|
| if self.extra_m0_output_channels is not None:
|
| m0_output_channels = m0_output_channels + self.extra_m0_output_channels
|
| self.fc_m0 = Linear(num_channels_m0, m0_output_channels)
|
| num_channels_rad = num_channels_rad + self.fc_m0.in_features
|
|
|
|
|
| self.so2_m_conv = nn.ModuleList()
|
| for m in range(1, max(self.mmax_list) + 1):
|
| self.so2_m_conv.append(
|
| SO2_m_Convolution(
|
| m,
|
| self.sphere_channels,
|
| self.m_output_channels,
|
| self.lmax_list,
|
| self.mmax_list,
|
| )
|
| )
|
| num_channels_rad = num_channels_rad + self.so2_m_conv[-1].fc.in_features
|
|
|
|
|
| self.rad_func = None
|
| if not self.internal_weights:
|
| assert self.edge_channels_list is not None
|
| self.edge_channels_list.append(int(num_channels_rad))
|
| self.rad_func = RadialFunction(self.edge_channels_list)
|
|
|
|
|
| def forward(self, x, x_edge):
|
|
|
| num_edges = len(x_edge)
|
| out = []
|
|
|
|
|
| x._m_primary(self.mappingReduced)
|
|
|
|
|
| if self.rad_func is not None:
|
| x_edge = self.rad_func(x_edge)
|
| offset_rad = 0
|
|
|
|
|
| x_0 = x.embedding.narrow(1, 0, self.mappingReduced.m_size[0])
|
| x_0 = x_0.reshape(num_edges, -1)
|
| if self.rad_func is not None:
|
| x_edge_0 = x_edge.narrow(1, 0, self.fc_m0.in_features)
|
| x_0 = x_0 * x_edge_0
|
| x_0 = self.fc_m0(x_0)
|
|
|
| x_0_extra = None
|
|
|
| if self.extra_m0_output_channels is not None:
|
| x_0_extra = x_0.narrow(-1, 0, self.extra_m0_output_channels)
|
| x_0 = x_0.narrow(-1, self.extra_m0_output_channels, (self.fc_m0.out_features - self.extra_m0_output_channels))
|
|
|
| x_0 = x_0.view(num_edges, -1, self.m_output_channels)
|
|
|
| out.append(x_0)
|
| offset_rad = offset_rad + self.fc_m0.in_features
|
|
|
|
|
| offset = self.mappingReduced.m_size[0]
|
| for m in range(1, max(self.mmax_list) + 1):
|
|
|
| x_m = x.embedding.narrow(1, offset, 2 * self.mappingReduced.m_size[m])
|
| x_m = x_m.reshape(num_edges, 2, -1)
|
|
|
|
|
| if self.rad_func is not None:
|
| x_edge_m = x_edge.narrow(1, offset_rad, self.so2_m_conv[m - 1].fc.in_features)
|
| x_edge_m = x_edge_m.reshape(num_edges, 1, self.so2_m_conv[m - 1].fc.in_features)
|
| x_m = x_m * x_edge_m
|
| x_m = self.so2_m_conv[m - 1](x_m)
|
| x_m = x_m.view(num_edges, -1, self.m_output_channels)
|
|
|
| out.append(x_m)
|
| offset = offset + 2 * self.mappingReduced.m_size[m]
|
| offset_rad = offset_rad + self.so2_m_conv[m - 1].fc.in_features
|
|
|
| out = torch.cat(out, dim=1)
|
| out_embedding = SO3_Embedding(
|
| 0,
|
| x.lmax_list.copy(),
|
| self.m_output_channels,
|
| device=x.device,
|
| dtype=x.dtype
|
| )
|
| out_embedding.set_embedding(out)
|
| out_embedding.set_lmax_mmax(self.lmax_list.copy(), self.mmax_list.copy())
|
|
|
|
|
| out_embedding._l_primary(self.mappingReduced)
|
|
|
| if self.extra_m0_output_channels is not None:
|
| return out_embedding, x_0_extra
|
| else:
|
| return out_embedding
|
|
|
|
|
| class SO2_Linear(torch.nn.Module):
|
| """
|
| SO(2) Linear: Perform SO(2) linear for all m (orders).
|
|
|
| Args:
|
| sphere_channels (int): Number of spherical channels
|
| m_output_channels (int): Number of output channels used during the SO(2) conv
|
| lmax_list (list:int): List of degrees (l) for each resolution
|
| mmax_list (list:int): List of orders (m) for each resolution
|
| mappingReduced (CoefficientMappingModule): Used to extract a subset of m components
|
| internal_weights (bool): If True, not using radial function to multiply inputs features
|
| edge_channels_list (list:int): List of sizes of invariant edge embedding. For example, [input_channels, hidden_channels, hidden_channels].
|
| """
|
| def __init__(
|
| self,
|
| sphere_channels,
|
| m_output_channels,
|
| lmax_list,
|
| mmax_list,
|
| mappingReduced,
|
| internal_weights=False,
|
| edge_channels_list=None,
|
| ):
|
| super(SO2_Linear, self).__init__()
|
| self.sphere_channels = sphere_channels
|
| self.m_output_channels = m_output_channels
|
| self.lmax_list = lmax_list
|
| self.mmax_list = mmax_list
|
| self.mappingReduced = mappingReduced
|
| self.internal_weights = internal_weights
|
| self.edge_channels_list = copy.deepcopy(edge_channels_list)
|
| self.num_resolutions = len(lmax_list)
|
|
|
| num_channels_rad = 0
|
|
|
| num_channels_m0 = 0
|
| for i in range(self.num_resolutions):
|
| num_coefficients = self.lmax_list[i] + 1
|
| num_channels_m0 = num_channels_m0 + num_coefficients * self.sphere_channels
|
|
|
|
|
| self.fc_m0 = Linear(num_channels_m0,
|
| self.m_output_channels * (num_channels_m0 // self.sphere_channels))
|
| num_channels_rad = num_channels_rad + self.fc_m0.in_features
|
|
|
|
|
| self.so2_m_fc = nn.ModuleList()
|
| for m in range(1, max(self.mmax_list) + 1):
|
| num_in_channels = 0
|
| for i in range(self.num_resolutions):
|
| num_coefficents = 0
|
| if self.mmax_list[i] >= m:
|
| num_coefficents = self.lmax_list[i] - m + 1
|
| num_in_channels = num_in_channels + num_coefficents * self.sphere_channels
|
| assert num_in_channels > 0
|
| fc = Linear(num_in_channels,
|
| self.m_output_channels * (num_in_channels // self.sphere_channels),
|
| bias=False)
|
| num_channels_rad = num_channels_rad + fc.in_features
|
| self.so2_m_fc.append(fc)
|
|
|
|
|
| self.rad_func = None
|
| if not self.internal_weights:
|
| assert self.edge_channels_list is not None
|
| self.edge_channels_list.append(int(num_channels_rad))
|
| self.rad_func = RadialFunction(self.edge_channels_list)
|
|
|
|
|
| def forward(self, x, x_edge):
|
|
|
| batch_size = x.embedding.shape[0]
|
| out = []
|
|
|
|
|
| x._m_primary(self.mappingReduced)
|
|
|
|
|
| if self.rad_func is not None:
|
| x_edge = self.rad_func(x_edge)
|
| offset_rad = 0
|
|
|
|
|
| x_0 = x.embedding.narrow(1, 0, self.mappingReduced.m_size[0])
|
| x_0 = x_0.reshape(batch_size, -1)
|
| if self.rad_func is not None:
|
| x_edge_0 = x_edge.narrow(1, 0, self.fc_m0.in_features)
|
| x_0 = x_0 * x_edge_0
|
| x_0 = self.fc_m0(x_0)
|
| x_0 = x_0.view(batch_size, -1, self.m_output_channels)
|
| out.append(x_0)
|
| offset_rad = offset_rad + self.fc_m0.in_features
|
|
|
|
|
| offset = self.mappingReduced.m_size[0]
|
| for m in range(1, max(self.mmax_list) + 1):
|
|
|
| x_m = x.embedding.narrow(1, offset, 2 * self.mappingReduced.m_size[m])
|
| x_m = x_m.reshape(batch_size, 2, -1)
|
| if self.rad_func is not None:
|
| x_edge_m = x_edge.narrow(1, offset_rad, self.so2_m_fc[m - 1].in_features)
|
| x_edge_m = x_edge_m.reshape(batch_size, 1, self.so2_m_fc[m - 1].in_features)
|
| x_m = x_m * x_edge_m
|
|
|
|
|
| x_m = self.so2_m_fc[m - 1](x_m)
|
| x_m = x_m.view(batch_size, -1, self.m_output_channels)
|
| out.append(x_m)
|
|
|
| offset = offset + 2 * self.mappingReduced.m_size[m]
|
| offset_rad = offset_rad + self.so2_m_fc[m - 1].in_features
|
|
|
| out = torch.cat(out, dim=1)
|
| out_embedding = SO3_Embedding(
|
| 0,
|
| x.lmax_list.copy(),
|
| self.m_output_channels,
|
| device=x.device,
|
| dtype=x.dtype
|
| )
|
| out_embedding.set_embedding(out)
|
| out_embedding.set_lmax_mmax(self.lmax_list.copy(), self.mmax_list.copy())
|
|
|
|
|
| out_embedding._l_primary(self.mappingReduced)
|
|
|
| return out_embedding |