import numpy as np import torch import torch.nn as nn import torch.nn.functional as F from torch_geometric.nn import radius_graph, knn_graph from torch_scatter import scatter_softmax, scatter_sum from .common import GaussianSmearing, MLP, batch_hybrid_edge_connection, outer_product class BaseX2HAttLayer(nn.Module): def __init__(self, input_dim, hidden_dim, output_dim, n_heads, edge_feat_dim, r_feat_dim, act_fn='relu', norm=True, ew_net_type='r', out_fc=True): super().__init__() self.input_dim = input_dim self.hidden_dim = hidden_dim self.output_dim = output_dim self.n_heads = n_heads self.act_fn = act_fn self.edge_feat_dim = edge_feat_dim self.r_feat_dim = r_feat_dim self.ew_net_type = ew_net_type self.out_fc = out_fc # attention key func kv_input_dim = input_dim * 2 + edge_feat_dim + r_feat_dim self.hk_func = MLP(kv_input_dim, output_dim, hidden_dim, norm=norm, act_fn=act_fn) # attention value func self.hv_func = MLP(kv_input_dim, output_dim, hidden_dim, norm=norm, act_fn=act_fn) # attention query func self.hq_func = MLP(input_dim, output_dim, hidden_dim, norm=norm, act_fn=act_fn) if ew_net_type == 'r': self.ew_net = nn.Sequential(nn.Linear(r_feat_dim, 1), nn.Sigmoid()) elif ew_net_type == 'm': self.ew_net = nn.Sequential(nn.Linear(output_dim, 1), nn.Sigmoid()) if self.out_fc: self.node_output = MLP(2 * hidden_dim, hidden_dim, hidden_dim, norm=norm, act_fn=act_fn) def forward(self, h, r_feat, edge_feat, edge_index, e_w=None): N = h.size(0) src, dst = edge_index hi, hj = h[dst], h[src] # multi-head attention # decide inputs of k_func and v_func kv_input = torch.cat([r_feat, hi, hj], -1) if edge_feat is not None: kv_input = torch.cat([edge_feat, kv_input], -1) # compute k k = self.hk_func(kv_input).view(-1, self.n_heads, self.output_dim // self.n_heads) # compute v v = self.hv_func(kv_input) if self.ew_net_type == 'r': e_w = self.ew_net(r_feat) elif self.ew_net_type == 'm': e_w = self.ew_net(v[..., :self.hidden_dim]) elif e_w is not None: e_w = e_w.view(-1, 1) else: e_w = 1. v = v * e_w v = v.view(-1, self.n_heads, self.output_dim // self.n_heads) # compute q q = self.hq_func(h).view(-1, self.n_heads, self.output_dim // self.n_heads) # compute attention weights alpha = scatter_softmax((q[dst] * k / np.sqrt(k.shape[-1])).sum(-1), dst, dim=0, dim_size=N) # [num_edges, n_heads] # perform attention-weighted message-passing m = alpha.unsqueeze(-1) * v # (E, heads, H_per_head) output = scatter_sum(m, dst, dim=0, dim_size=N) # (N, heads, H_per_head) output = output.view(-1, self.output_dim) if self.out_fc: output = self.node_output(torch.cat([output, h], -1)) output = output + h return output class BaseH2XAttLayer(nn.Module): def __init__(self, input_dim, hidden_dim, output_dim, n_heads, edge_feat_dim, r_feat_dim, act_fn='relu', norm=True, ew_net_type='r'): super().__init__() self.input_dim = input_dim self.hidden_dim = hidden_dim self.output_dim = output_dim self.n_heads = n_heads self.edge_feat_dim = edge_feat_dim self.r_feat_dim = r_feat_dim self.act_fn = act_fn self.ew_net_type = ew_net_type kv_input_dim = input_dim * 2 + edge_feat_dim + r_feat_dim self.xk_func = MLP(kv_input_dim, output_dim, hidden_dim, norm=norm, act_fn=act_fn) self.xv_func = MLP(kv_input_dim, self.n_heads, hidden_dim, norm=norm, act_fn=act_fn) self.xq_func = MLP(input_dim, output_dim, hidden_dim, norm=norm, act_fn=act_fn) if ew_net_type == 'r': self.ew_net = nn.Sequential(nn.Linear(r_feat_dim, 1), nn.Sigmoid()) def forward(self, h, rel_x, r_feat, edge_feat, edge_index, e_w=None): N = h.size(0) src, dst = edge_index hi, hj = h[dst], h[src] # multi-head attention # decide inputs of k_func and v_func kv_input = torch.cat([r_feat, hi, hj], -1) if edge_feat is not None: kv_input = torch.cat([edge_feat, kv_input], -1) k = self.xk_func(kv_input).view(-1, self.n_heads, self.output_dim // self.n_heads) v = self.xv_func(kv_input) if self.ew_net_type == 'r': e_w = self.ew_net(r_feat) elif self.ew_net_type == 'm': e_w = 1. elif e_w is not None: e_w = e_w.view(-1, 1) else: e_w = 1. v = v * e_w v = v.unsqueeze(-1) * rel_x.unsqueeze(1) # (xi - xj) [n_edges, n_heads, 3] q = self.xq_func(h).view(-1, self.n_heads, self.output_dim // self.n_heads) # Compute attention weights alpha = scatter_softmax((q[dst] * k / np.sqrt(k.shape[-1])).sum(-1), dst, dim=0, dim_size=N) # (E, heads) # Perform attention-weighted message-passing m = alpha.unsqueeze(-1) * v # (E, heads, 3) output = scatter_sum(m, dst, dim=0, dim_size=N) # (N, heads, 3) return output.mean(1) # [num_nodes, 3] class AttentionLayerO2TwoUpdateNodeGeneral(nn.Module): def __init__(self, hidden_dim, n_heads, num_r_gaussian, edge_feat_dim, act_fn='relu', norm=True, num_x2h=1, num_h2x=1, r_min=0., r_max=10., num_node_types=8, ew_net_type='r', x2h_out_fc=True, sync_twoup=False): super().__init__() self.hidden_dim = hidden_dim self.n_heads = n_heads self.edge_feat_dim = edge_feat_dim self.num_r_gaussian = num_r_gaussian self.norm = norm self.act_fn = act_fn self.num_x2h = num_x2h self.num_h2x = num_h2x self.r_min, self.r_max = r_min, r_max self.num_node_types = num_node_types self.ew_net_type = ew_net_type self.x2h_out_fc = x2h_out_fc self.sync_twoup = sync_twoup self.distance_expansion = GaussianSmearing(self.r_min, self.r_max, num_gaussians=num_r_gaussian) self.x2h_layers = nn.ModuleList() for i in range(self.num_x2h): self.x2h_layers.append( BaseX2HAttLayer(hidden_dim, hidden_dim, hidden_dim, n_heads, edge_feat_dim, r_feat_dim=num_r_gaussian * 4, act_fn=act_fn, norm=norm, ew_net_type=self.ew_net_type, out_fc=self.x2h_out_fc) ) self.h2x_layers = nn.ModuleList() for i in range(self.num_h2x): self.h2x_layers.append( BaseH2XAttLayer(hidden_dim, hidden_dim, hidden_dim, n_heads, edge_feat_dim, r_feat_dim=num_r_gaussian * 4, act_fn=act_fn, norm=norm, ew_net_type=self.ew_net_type) ) def forward(self, h, x, edge_attr, edge_index, mask_ligand, e_w=None, fix_x=False): src, dst = edge_index if self.edge_feat_dim > 0: edge_feat = edge_attr # shape: [#edges_in_batch, #bond_types] else: edge_feat = None rel_x = x[dst] - x[src] dist = torch.norm(rel_x, p=2, dim=-1, keepdim=True) h_in = h # 4 separate distance embedding for p-p, p-l, l-p, l-l for i in range(self.num_x2h): dist_feat = self.distance_expansion(dist) dist_feat = outer_product(edge_attr, dist_feat) h_out = self.x2h_layers[i](h_in, dist_feat, edge_feat, edge_index, e_w=e_w) h_in = h_out x2h_out = h_in new_h = h if self.sync_twoup else x2h_out for i in range(self.num_h2x): dist_feat = self.distance_expansion(dist) dist_feat = outer_product(edge_attr, dist_feat) delta_x = self.h2x_layers[i](new_h, rel_x, dist_feat, edge_feat, edge_index, e_w=e_w) if not fix_x: x = x + delta_x * mask_ligand[:, None] # only ligand positions will be updated rel_x = x[dst] - x[src] dist = torch.norm(rel_x, p=2, dim=-1, keepdim=True) return x2h_out, x class UniTransformerO2TwoUpdateGeneral(nn.Module): def __init__(self, num_blocks, num_layers, hidden_dim, n_heads=1, k=32, num_r_gaussian=50, edge_feat_dim=0, num_node_types=8, act_fn='relu', norm=True, cutoff_mode='radius', ew_net_type='r', num_init_x2h=1, num_init_h2x=0, num_x2h=1, num_h2x=1, r_max=10., x2h_out_fc=True, sync_twoup=False): super().__init__() # Build the network self.num_blocks = num_blocks self.num_layers = num_layers self.hidden_dim = hidden_dim self.n_heads = n_heads self.num_r_gaussian = num_r_gaussian self.edge_feat_dim = edge_feat_dim self.act_fn = act_fn self.norm = norm self.num_node_types = num_node_types # radius graph / knn graph self.cutoff_mode = cutoff_mode # [radius, none] self.k = k self.ew_net_type = ew_net_type # [r, m, none] self.num_x2h = num_x2h self.num_h2x = num_h2x self.num_init_x2h = num_init_x2h self.num_init_h2x = num_init_h2x self.r_max = r_max self.x2h_out_fc = x2h_out_fc self.sync_twoup = sync_twoup self.distance_expansion = GaussianSmearing(0., r_max, num_gaussians=num_r_gaussian) if self.ew_net_type == 'global': self.edge_pred_layer = MLP(num_r_gaussian, 1, hidden_dim) self.init_h_emb_layer = self._build_init_h_layer() self.base_block = self._build_share_blocks() def __repr__(self): return f'UniTransformerO2(num_blocks={self.num_blocks}, num_layers={self.num_layers}, n_heads={self.n_heads}, ' \ f'act_fn={self.act_fn}, norm={self.norm}, cutoff_mode={self.cutoff_mode}, ew_net_type={self.ew_net_type}, ' \ f'init h emb: {self.init_h_emb_layer.__repr__()} \n' \ f'base block: {self.base_block.__repr__()} \n' \ f'edge pred layer: {self.edge_pred_layer.__repr__() if hasattr(self, "edge_pred_layer") else "None"}) ' def _build_init_h_layer(self): layer = AttentionLayerO2TwoUpdateNodeGeneral( self.hidden_dim, self.n_heads, self.num_r_gaussian, self.edge_feat_dim, act_fn=self.act_fn, norm=self.norm, num_x2h=self.num_init_x2h, num_h2x=self.num_init_h2x, r_max=self.r_max, num_node_types=self.num_node_types, ew_net_type=self.ew_net_type, x2h_out_fc=self.x2h_out_fc, sync_twoup=self.sync_twoup, ) return layer def _build_share_blocks(self): # Equivariant layers base_block = [] for l_idx in range(self.num_layers): layer = AttentionLayerO2TwoUpdateNodeGeneral( self.hidden_dim, self.n_heads, self.num_r_gaussian, self.edge_feat_dim, act_fn=self.act_fn, norm=self.norm, num_x2h=self.num_x2h, num_h2x=self.num_h2x, r_max=self.r_max, num_node_types=self.num_node_types, ew_net_type=self.ew_net_type, x2h_out_fc=self.x2h_out_fc, sync_twoup=self.sync_twoup, ) base_block.append(layer) return nn.ModuleList(base_block) def _connect_edge(self, x, mask_ligand, batch): if self.cutoff_mode == 'radius': edge_index = radius_graph(x, r=self.r, batch=batch, flow='source_to_target') elif self.cutoff_mode == 'knn': edge_index = knn_graph(x, k=self.k, batch=batch, flow='source_to_target') elif self.cutoff_mode == 'hybrid': edge_index = batch_hybrid_edge_connection( x, k=self.k, mask_ligand=mask_ligand, batch=batch, add_p_index=True) else: raise ValueError(f'Not supported cutoff mode: {self.cutoff_mode}') return edge_index @staticmethod def _build_edge_type(edge_index, mask_ligand): src, dst = edge_index edge_type = torch.zeros(len(src)).to(edge_index) n_src = mask_ligand[src] == 1 n_dst = mask_ligand[dst] == 1 edge_type[n_src & n_dst] = 0 edge_type[n_src & ~n_dst] = 1 edge_type[~n_src & n_dst] = 2 edge_type[~n_src & ~n_dst] = 3 edge_type = F.one_hot(edge_type, num_classes=4) return edge_type def forward(self, h, x, mask_ligand, batch, return_all=False, fix_x=False): all_x = [x] all_h = [h] for b_idx in range(self.num_blocks): edge_index = self._connect_edge(x, mask_ligand, batch) src, dst = edge_index # edge type (dim: 4) edge_type = self._build_edge_type(edge_index, mask_ligand) if self.ew_net_type == 'global': dist = torch.norm(x[dst] - x[src], p=2, dim=-1, keepdim=True) dist_feat = self.distance_expansion(dist) logits = self.edge_pred_layer(dist_feat) e_w = torch.sigmoid(logits) else: e_w = None for l_idx, layer in enumerate(self.base_block): h, x = layer(h, x, edge_type, edge_index, mask_ligand, e_w=e_w, fix_x=fix_x) all_x.append(x) all_h.append(h) outputs = {'x': x, 'h': h} if return_all: outputs.update({'all_x': all_x, 'all_h': all_h}) return outputs