class_docstr: Apply `Bidirectional Separate GNN mechanism <https://arxiv.org/abs/1808.07624>`__
  to `Graph Attention Network <https://arxiv.org/abs/1710.10903>`__ over an
  input signal. Compute node embeddings for incoming and outgoing directions
  separately, and then concatenate the two output node embeddings
  after the final layer.
  
  .. math::
      h_i^{(l+1)} = \sum_{j\in \mathcal{N}(i)} \alpha_{i,j} W^{(l)} h_j^{(l)}
  where :math:`\alpha_{ij}` is the attention score bewteen node :math:`i` and
  node :math:`j`:
  
  .. math::
      \alpha_{ij}^{l} & = \mathrm{softmax_i} (e_{ij}^{l})
      e_{ij}^{l} & = \mathrm{LeakyReLU}\left(\vec{a}^T [W h_{i} \| W h_{j}]\right)
  
  Parameters
  ----------
  input_size : int, or pair of ints
      Input feature size.
      If the layer is to be applied to a unidirectional bipartite graph, ``input_size``
      specifies the input feature size on both the source and destination nodes.  If
      a scalar is given, the source and destination node feature size would take the
      same value.
  output_size : int
      Output feature size.
  num_heads : int
      Number of heads in Multi-Head Attention.
  feat_drop : float, optional
      Dropout rate on feature, default: ``0``.
  attn_drop : float, optional
      Dropout rate on attention weight, default: ``0``.
  negative_slope : float, optional
      LeakyReLU angle of negative slope, default: ``0.2``.
  residual : bool, optional
      If True, use residual connection.
      Default: ``False``.
  activation : callable activation function/layer or None, optional.
      If not None, applies an activation function to the updated node features.
      Default: ``None``.
  allow_zero_in_degree : bool, optional
      If there are 0-in-degree nodes in the graph, output for those nodes will be invalid
      since no message will be passed to those nodes. This is harmful for some applications
      causing silent performance regression. This module will raise a DGLError if it detects
      0-in-degree nodes in input graph. By setting ``True``, it will suppress the check
      and let the users handle it by themselves. Defaults: ``False``.
docstr: Compute graph attention network layer.
  
  Parameters
  ----------
  graph : DGLGraph
      The graph.
  feat:
      A list/tuple containing incoming feature ``feat_fw``
      and outgoing feature ``feat_bw``.
      - ``feat_fw`` : torch.Tensor or pair of torch.Tensor
              If a torch.Tensor is given, the input feature of
              shape :math:`(N, D_{in})` where :math:`D_{in}` is
              size of input feature, :math:`N` is the number of
              nodes. If a pair of torch.Tensor is given, the pair
              must contain two tensors of shape :math:`(N_{in},
              D_{in_{src}})` and :math:`(N_{out}, D_{in_{dst}})`.
      - ``feat_bw``: torch.Tensor or pair of torch.Tensor
              If a torch.Tensor is given, the input feature of
              shape :math:`(N, D_{in})` where :math:`D_{in}` is
              size of input feature, :math:`N` is the number of
              nodes. If a pair of torch.Tensor is given, the pair
              must contain two tensors of shape :math:`(N_{in},
              D_{in_{src}})` and :math:`(N_{out}, D_{in_{dst}})`.
  
  Returns
  -------
  Pair of torch.Tensor
      Each output feature of shape :math:`(N, H, D_{out})` where :math:`H`
      is the number of heads, and :math:`D_{out}` is size of output feature.
docstr_tok: ["Compute","graph","attention","network","layer",".","Parameters","--","--","--","--","--","graph",":","DGLGraph","The","graph",".","feat",":","A","list\/tuple","containing","incoming","feature","``","feat_fw","``","and","outgoing","feature","``","feat_bw","``",".","-","``","feat_fw","``",":","torch.Tensor","or","pair","of","torch.Tensor","If","a","torch.Tensor","is","given",",","the","input","feature","of","shape",":","math",":","`","(","N",",","D_","{","in","}",")","`","where",":","math",":","`","D_","{","in","}","`","is","size","of","input","feature",",",":","math",":","`","N","`","is","the","number","of","nodes",".","If","a","pair","of","torch.Tensor","is","given",",","the","pair","must","contain","two","tensors","of","shape",":","math",":","`","(","N_","{","in","}",",","D_","{","in_","{","src","}","}",")","`","and",":","math",":","`","(","N_","{","out","}",",","D_","{","in_","{","dst","}","}",")","`",".","-","``","feat_bw","``",":","torch.Tensor","or","pair","of","torch.Tensor","If","a","torch.Tensor","is","given",",","the","input","feature","of","shape",":","math",":","`","(","N",",","D_","{","in","}",")","`","where",":","math",":","`","D_","{","in","}","`","is","size","of","input","feature",",",":","math",":","`","N","`","is","the","number","of","nodes",".","If","a","pair","of","torch.Tensor","is","given",",","the","pair","must","contain","two","tensors","of","shape",":","math",":","`","(","N_","{","in","}",",","D_","{","in_","{","src","}","}",")","`","and",":","math",":","`","(","N_","{","out","}",",","D_","{","in_","{","dst","}","}",")","`",".","Returns","--","--","--","-","Pair","of","torch.Tensor","Each","output","feature","of","shape",":","math",":","`","(","N",",","H",",","D_","{","out","}",")","`","where",":","math",":","`","H","`","is","the","number","of","heads",",","and",":","math",":","`","D_","{","out","}","`","is","size","of","output","feature","."]
code: def forward(self, graph, feat):
      r"""Compute graph attention network layer.
  
      Parameters
      ----------
      graph : DGLGraph
          The graph.
      feat:
          A list/tuple containing incoming feature ``feat_fw``
          and outgoing feature ``feat_bw``.
          - ``feat_fw`` : torch.Tensor or pair of torch.Tensor
                  If a torch.Tensor is given, the input feature of
                  shape :math:`(N, D_{in})` where :math:`D_{in}` is
                  size of input feature, :math:`N` is the number of
                  nodes. If a pair of torch.Tensor is given, the pair
                  must contain two tensors of shape :math:`(N_{in},
                  D_{in_{src}})` and :math:`(N_{out}, D_{in_{dst}})`.
          - ``feat_bw``: torch.Tensor or pair of torch.Tensor
                  If a torch.Tensor is given, the input feature of
                  shape :math:`(N, D_{in})` where :math:`D_{in}` is
                  size of input feature, :math:`N` is the number of
                  nodes. If a pair of torch.Tensor is given, the pair
                  must contain two tensors of shape :math:`(N_{in},
                  D_{in_{src}})` and :math:`(N_{out}, D_{in_{dst}})`.
  
      Returns
      -------
      Pair of torch.Tensor
          Each output feature of shape :math:`(N, H, D_{out})` where :math:`H`
          is the number of heads, and :math:`D_{out}` is size of output feature.
      """
      with graph.local_scope():
          if not self._allow_zero_in_degree:
              if (graph.in_degrees() == 0).any():
                  raise DGLError(
                      "There are 0-in-degree nodes in the graph, "
                      "output for those nodes will be invalid. "
                      "This is harmful for some applications, "
                      "causing silent performance regression. "
                      "Adding self-loop on the input graph by "
                      "calling `g = dgl.add_self_loop(g)` will resolve "
                      "the issue. Setting ``allow_zero_in_degree`` "
                      "to be `True` when constructing this module will "
                      "suppress the check and let the code run."
                  )
  
      feat_fw, feat_bw = feat
  
      if isinstance(feat_fw, tuple):
          h_src_fw = self.feat_drop(feat_fw[0])
          h_dst_fw = self.feat_drop(feat_fw[1])
      else:
          h_src_fw = h_dst_fw = self.feat_drop(feat_fw)
  
      if isinstance(feat_bw, tuple):
          h_src_bw = self.feat_drop(feat_bw[0])
          h_dst_bw = self.feat_drop(feat_bw[1])
      else:
          h_src_bw = h_dst_bw = self.feat_drop(feat_bw)
  
      # forward direction
      with graph.local_scope():
          if isinstance(feat_fw, tuple):
              feat_src = self.fc_src_fw(h_src_fw).view(
                  -1, self._num_heads, self._out_feats
              )
              feat_dst = self.fc_dst_fw(h_dst_fw).view(
                  -1, self._num_heads, self._out_feats
              )
          else:
              feat_src = feat_dst = self.fc_fw(h_src_fw).view(
                  -1, self._num_heads, self._out_feats
              )
          # NOTE: GAT paper uses "first concatenation then linear projection"
          # to compute attention scores, while ours is "first projection then
          # addition", the two approaches are mathematically equivalent:
          # We decompose the weight vector a mentioned in the paper into
          # [a_l || a_r], then
          # a^T [Wh_i || Wh_j] = a_l Wh_i + a_r Wh_j
          # Our implementation is much efficient because we do not need to
          # save [Wh_i || Wh_j] on edges, which is not memory-efficient. Plus,
          # addition could be optimized with DGL's built-in function u_add_v,
          # which further speeds up computation and saves memory footprint.
          el = (feat_src * self.attn_l_fw).sum(dim=-1).unsqueeze(-1)
          er = (feat_dst * self.attn_r_fw).sum(dim=-1).unsqueeze(-1)
          graph.srcdata.update({"ft": feat_src, "el": el})
          graph.dstdata.update({"er": er})
          # compute edge attention, el and er are a_l Wh_i and a_r Wh_j respectively.
          graph.apply_edges(fn.u_add_v("el", "er", "e"))
          e = self.leaky_relu_fw(graph.edata.pop("e"))
          # compute softmax
          graph.edata["a"] = self.attn_drop(edge_softmax(graph, e))
          # message passing
          graph.update_all(
              fn.u_mul_e("ft", "a", "m"), fn.sum("m", "ft")
          )
          agg_emb_fw = graph.dstdata["ft"]
  
      # backward direction
      graph = graph.reverse()
      with graph.local_scope():
          if isinstance(feat_bw, tuple):
              feat_src = self.fc_src_bw(h_src_bw).view(
                  -1, self._num_heads, self._out_feats
              )
              feat_dst = self.fc_dst_bw(h_dst_bw).view(
                  -1, self._num_heads, self._out_feats
              )
          else:
              feat_src = feat_dst = self.fc_bw(h_src_bw).view(
                  -1, self._num_heads, self._out_feats
              )
  
          el = (feat_src * self.attn_l_bw).sum(dim=-1).unsqueeze(-1)
          er = (feat_dst * self.attn_r_bw).sum(dim=-1).unsqueeze(-1)
          graph.srcdata.update({"ft": feat_src, "el": el})
          graph.dstdata.update({"er": er})
          # compute edge attention, el and er are a_l Wh_i and a_r Wh_j respectively.
          graph.apply_edges(fn.u_add_v("el", "er", "e"))
          e = self.leaky_relu_bw(graph.edata.pop("e"))
          # compute softmax
          graph.edata["a"] = self.attn_drop(edge_softmax(graph, e))
          # message passing
          graph.update_all(
              fn.u_mul_e("ft", "a", "m"), fn.sum("m", "ft")
          )
          agg_emb_bw = graph.dstdata["ft"]
  
      # residual
      if self.res_fc_fw is not None:
          resval_fw = self.res_fc_fw(h_dst_fw).view(
              h_dst_fw.shape[0], -1, self._out_feats
          )
          resval_bw = self.res_fc_bw(h_dst_bw).view(
              h_dst_bw.shape[0], -1, self._out_feats
          )
          agg_emb_fw = agg_emb_fw + resval_fw
          agg_emb_bw = agg_emb_bw + resval_bw
  
      # activation
      if self.activation:
          agg_emb_fw = self.activation(agg_emb_fw)
          agg_emb_bw = self.activation(agg_emb_bw)
  
      return [agg_emb_fw, agg_emb_bw]
  
imports: import dgl.function
  import torch
  import torch.nn
  from dgl.base import DGLError
  from dgl.nn.pytorch import GATConv
  from dgl.nn.pytorch.softmax import edge_softmax
  from dgl.utils import expand_as_pair
  from ..utils.generic_utils import Identity
  from .base import GNNBase, GNNLayerBase
label_desc: Use image node_id 3 for calling the BiSepGATLayerConv obj's underlying member method code with example usage: obj.forward(graph, feat) and returns: list