MagNET / magnet /eqV2 /so2_ops.py
ekwan16's picture
Add files using upload-large-folder tool
64c992d verified
Raw
History Blame Contribute Delete
13.6 kB
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_r[:, 0] - x_i[:, 1]
x_m_i = x_r.narrow(1, 1, 1) + x_i.narrow(1, 0, 1) #x_r[:, 1] + x_i[:, 0]
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 # for radial function
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
# SO(2) convolution for m = 0
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
# SO(2) convolution for non-zero m
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
# Embedding function of distance
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 = []
# Reshape the spherical harmonics based on m (order)
x._m_primary(self.mappingReduced)
# radial function
if self.rad_func is not None:
x_edge = self.rad_func(x_edge)
offset_rad = 0
# Compute m=0 coefficients separately since they only have real values (no imaginary)
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
# extract extra m0 features
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)
#x.embedding[:, 0 : self.mappingReduced.m_size[0]] = x_0
out.append(x_0)
offset_rad = offset_rad + self.fc_m0.in_features
# Compute the values for the m > 0 coefficients
offset = self.mappingReduced.m_size[0]
for m in range(1, max(self.mmax_list) + 1):
# Get the m order coefficients
x_m = x.embedding.narrow(1, offset, 2 * self.mappingReduced.m_size[m])
x_m = x_m.reshape(num_edges, 2, -1)
# Perform SO(2) convolution
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)
#x.embedding[:, offset : offset + 2 * self.mappingReduced.m_size[m]] = x_m
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())
# Reshape the spherical harmonics based on l (degree)
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
# SO(2) linear for m = 0
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
# SO(2) linear for non-zero m
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)
# Embedding function of distance
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 = []
# Reshape the spherical harmonics based on m (order)
x._m_primary(self.mappingReduced)
# radial function
if self.rad_func is not None:
x_edge = self.rad_func(x_edge)
offset_rad = 0
# Compute m=0 coefficients separately since they only have real values (no imaginary)
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
# Compute the values for the m > 0 coefficients
offset = self.mappingReduced.m_size[0]
for m in range(1, max(self.mmax_list) + 1):
# Get the m order coefficients
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
# Perform SO(2) linear
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())
# Reshape the spherical harmonics based on l (degree)
out_embedding._l_primary(self.mappingReduced)
return out_embedding