class_docstr: Description
  -----------
  The HGP-SL pooling layer from
  `Hierarchical Graph Pooling with Structure Learning <https://arxiv.org/pdf/1911.05954.pdf>`
  
  Parameters
  ----------
  in_feat : int
      The number of input node feature's channels
  ratio : float, optional
      Pooling ratio. Default: 0.8
  sample : bool, optional
      Whether use k-hop union graph to increase efficiency.
      Currently we only support full graph. Default: :obj:`False`
  sym_score_norm : bool, optional
      Use symmetric norm for adjacency or not. Default: :obj:`True`
  sparse : bool, optional
      Use edge sparsemax instead of edge softmax. Default: :obj:`True`
  sl : bool, optional
      Use structure learining module or not. Default: :obj:`True`
  lamb : float, optional
      The lambda parameter as weight of raw adjacency as described in the
      HGP-SL paper. Default: 1.0
  negative_slop : float, optional
      Negative slop for leaky_relu. Default: 0.2
  
  Returns
  -------
  DGLGraph
      The pooled graph.
  torch.Tensor
      Node features
  torch.Tensor
      Edge features
  torch.Tensor
      Permutation index
code: def forward(self, graph: DGLGraph, feat: Tensor, e_feat=None):
      # top-k pool first
      if e_feat is None:
          e_feat = torch.ones(
              (graph.num_edges(),), dtype=feat.dtype, device=feat.device
          )
      batch_num_nodes = graph.batch_num_nodes()
      x_score = self.calc_info_score(graph, feat, e_feat)
      perm, next_batch_num_nodes = topk(
          x_score,
          self.ratio,
          get_batch_id(batch_num_nodes),
          batch_num_nodes,
      )
      feat = feat[perm]
      pool_graph = None
      if not self.sample or not self.sl:
          # pool graph
          graph.edata["e"] = e_feat
          pool_graph = dgl.node_subgraph(graph, perm)
          e_feat = pool_graph.edata.pop("e")
          pool_graph.set_batch_num_nodes(next_batch_num_nodes)
  
      # no structure learning layer, directly return.
      if not self.sl:
          return pool_graph, feat, e_feat, perm
  
      # Structure Learning
      if self.sample:
          # A fast mode for large graphs.
          # In large graphs, learning the possible edge weights between each
          # pair of nodes is time consuming. To accelerate this process,
          # we sample it's K-Hop neighbors for each node and then learn the
          # edge weights between them.
  
          # first build multi-hop graph
          row, col = graph.all_edges()
          num_nodes = graph.num_nodes()
  
          scipy_adj = scipy.sparse.coo_matrix(
              (
                  e_feat.detach().cpu(),
                  (row.detach().cpu(), col.detach().cpu()),
              ),
              shape=(num_nodes, num_nodes),
          )
          for _ in range(self.k_hop):
              two_hop = scipy_adj**2
              two_hop = two_hop * (1e-5 / two_hop.max())
              scipy_adj = two_hop + scipy_adj
          row, col = scipy_adj.nonzero()
          row = torch.tensor(row, dtype=torch.long, device=graph.device)
          col = torch.tensor(col, dtype=torch.long, device=graph.device)
          e_feat = torch.tensor(
              scipy_adj.data, dtype=torch.float, device=feat.device
          )
  
          # perform pooling on multi-hop graph
          mask = perm.new_full((num_nodes,), -1)
          i = torch.arange(
              perm.size(0), dtype=torch.long, device=perm.device
          )
          mask[perm] = i
          row, col = mask[row], mask[col]
          mask = (row >= 0) & (col >= 0)
          row, col = row[mask], col[mask]
          e_feat = e_feat[mask]
  
          # add remaining self loops
          mask = row != col
          num_nodes = perm.size(0)  # num nodes after pool
          loop_index = torch.arange(
              0, num_nodes, dtype=row.dtype, device=row.device
          )
          inv_mask = ~mask
          loop_weight = torch.full(
              (num_nodes,), 0, dtype=e_feat.dtype, device=e_feat.device
          )
          remaining_e_feat = e_feat[inv_mask]
          if remaining_e_feat.numel() > 0:
              loop_weight[row[inv_mask]] = remaining_e_feat
          e_feat = torch.cat([e_feat[mask], loop_weight], dim=0)
          row, col = row[mask], col[mask]
          row = torch.cat([row, loop_index], dim=0)
          col = torch.cat([col, loop_index], dim=0)
  
          # attention scores
          weights = (
              torch.cat([feat[row], feat[col]], dim=1) * self.att
          ).sum(dim=-1)
          weights = (
              F.leaky_relu(weights, self.negative_slop)
              + e_feat * self.lamb
          )
  
          # sl and normalization
          sl_graph = dgl.graph((row, col))
          if self.sparse:
              weights = edge_sparsemax(sl_graph, weights)
          else:
              weights = edge_softmax(sl_graph, weights)
  
          # get final graph
          mask = torch.abs(weights) > 0
          row, col, weights = row[mask], col[mask], weights[mask]
          pool_graph = dgl.graph((row, col))
          pool_graph.set_batch_num_nodes(next_batch_num_nodes)
          e_feat = weights
  
      else:
          # Learning the possible edge weights between each pair of
          # nodes in the pooled subgraph, relative slower.
  
          # construct complete graphs for all graph in the batch
          # use dense to build, then transform to sparse.
          # maybe there's more efficient way?
          batch_num_nodes = next_batch_num_nodes
          block_begin_idx = torch.cat(
              [
                  batch_num_nodes.new_zeros(1),
                  batch_num_nodes.cumsum(dim=0)[:-1],
              ],
              dim=0,
          )
          block_end_idx = batch_num_nodes.cumsum(dim=0)
          dense_adj = torch.zeros(
              (pool_graph.num_nodes(), pool_graph.num_nodes()),
              dtype=torch.float,
              device=feat.device,
          )
          for idx_b, idx_e in zip(block_begin_idx, block_end_idx):
              dense_adj[idx_b:idx_e, idx_b:idx_e] = 1.0
          row, col = torch.nonzero(dense_adj).t().contiguous()
  
          # compute weights for node-pairs
          weights = (
              torch.cat([feat[row], feat[col]], dim=1) * self.att
          ).sum(dim=-1)
          weights = F.leaky_relu(weights, self.negative_slop)
          dense_adj[row, col] = weights
  
          # add pooled graph structure to weight matrix
          pool_row, pool_col = pool_graph.all_edges()
          dense_adj[pool_row, pool_col] += self.lamb * e_feat
          weights = dense_adj[row, col]
          del dense_adj
          torch.cuda.empty_cache()
  
          # edge softmax/sparsemax
          complete_graph = dgl.graph((row, col))
          if self.sparse:
              weights = edge_sparsemax(complete_graph, weights)
          else:
              weights = edge_softmax(complete_graph, weights)
  
          # get new e_feat and graph structure, clean up.
          mask = torch.abs(weights) > 1e-9
          row, col, weights = row[mask], col[mask], weights[mask]
          e_feat = weights
          pool_graph = dgl.graph((row, col))
          pool_graph.set_batch_num_nodes(next_batch_num_nodes)
  
      return pool_graph, feat, e_feat, perm
  
imports: import dgl
  import dgl.function
  import scipy.sparse
  import torch
  import torch.nn
  import torch.nn.functional
  from dgl import DGLGraph
  from dgl.nn import AvgPooling, GraphConv, MaxPooling
  from dgl.ops import edge_softmax
  from functions import edge_sparsemax
  from torch import Tensor
  from torch.nn import Parameter
  from utils import get_batch_id, topk
label_desc: Use image node_id 3 for calling the HGPSLPool obj's underlying member method code with example usage: obj.forward(graph, feat, e_feat) and returns: pool_graph,  feat,  e_feat,  perm, pool_graph,  feat,  e_feat,  perm