class_docstr: Compute node embeddings for incoming and outgoing directions
  separately, and then concatenate the two output node embeddings
  after the final layer.
  .. math::
     h_{i}^{0} = [ x_i \| \mathbf{0} ]
  
     a_{i, \vdash}^{t} = \sum_{j\in\mathcal{N}_{\vdash }(i)} W_{\vdash} h_{j, \vdash}^{t}
  
     a_{i, \dashv}^{t} = \sum_{j\in\mathcal{N}_{\dashv }(i)} W_{\dashv} h_{j, \dashv}^{t}
  
     h_{i, \vdash}^{t+1} = \mathrm{GRU}_{\vdash}(a_{i, \vdash}^{t}, h_{i, \vdash}^{t})
  
     h_{i, \dashv}^{t+1} = \mathrm{GRU}_{\dashv}(a_{i, \dashv}^{t}, h_{i, \dashv}^{t})
  Attributes
  ----------
  input_size: int
      Input feature size.
  output_size: int
      Output feature size.
  n_etypes: int
      Number of edge types. Default: 1.
  bias: bool
      If True, adds a learnable bias to the output. Default: True.
  Attributes
  ----------
  input_size: int
      Input feature size.
  output_size: int
      Output feature size.
  n_etypes: int
      Number of edge types. Default: 1.
  bias: bool
      If True, adds a learnable bias to the output. Default: True.
code: def forward(self, graph, node_feats, etypes=None, edge_weight=None):
      feat_in, feat_out = node_feats
  
      graph_in = graph
      graph_in = graph_in.local_var()
      graph_in.ndata["h"] = feat_in
      for i in range(self._n_etypes):
          eids = (etypes == i).nonzero(as_tuple=False).view(-1)
          if len(eids) > 0:
              if edge_weight is None:
                  graph_in.apply_edges(
                      lambda edges: {
                          "W_e*h": self.linears_in[i](edges.src["h"])
                      },
                      eids,
                  )
              else:
                  assert isinstance(edge_weight, tuple)
                  graph_in.apply_edges(
                      lambda edges: {
                          "W_e*h": self.linears_in[i](edges.src["h"])
                          * edge_weight[0].view(-1).unsqueeze(1)
                      },
                      eids,
                  )
      graph_in.update_all(fn.copy_e("W_e*h", "m"), fn.sum("m", "a"))
      a_in = graph_in.ndata.pop("a")  # (N, D)
      emb_in = self.gru_in(a_in, feat_in)
  
      graph_out = graph.reverse()
      graph_out = graph_out.local_var()
      graph_out.ndata["h"] = feat_out
      for i in range(self._n_etypes):
          eids = (etypes == i).nonzero(as_tuple=False).view(-1)
          if len(eids) > 0:
              if edge_weight is None:
                  graph_out.apply_edges(
                      lambda edges: {
                          "W_e*h": self.linears_out[i](edges.src["h"])
                      },
                      eids,
                  )
              else:
                  assert isinstance(edge_weight, tuple)
                  graph_out.apply_edges(
                      lambda edges: {
                          "W_e*h": self.linears_out[i](edges.src["h"])
                          * edge_weight[1].view(-1).unsqueeze(1)
                      },
                      eids,
                  )
      graph_out.update_all(fn.copy_e("W_e*h", "m"), fn.sum("m", "a"))
      a_out = graph_out.ndata.pop("a")  # (N, D)
      emb_out = self.gru_out(a_out, feat_out)
  
      return [emb_in, emb_out]
  
imports: import dgl
  import dgl.function
  import torch
  import torch.nn
  from torch.nn import init
  from ...data.data import GraphData
  from .base import GNNBase, GNNLayerBase
label_desc: Use image node_id 3 for calling the BiSepGGNNLayerConv obj's underlying member method code with example usage: obj.forward(graph, node_feats, etypes, edge_weight) and returns: list