code: def add_self_loops(  # noqa: F811
      edge_index: Tensor,
      edge_attr: Optional[Tensor] = None,
      fill_value: Optional[Union[float, Tensor, str]] = None,
      num_nodes: Optional[Union[int, Tuple[int, int]]] = None,
  ) -> Tuple[Tensor, Optional[Tensor]]:
      r"""Adds a self-loop :math:`(i,i) \in \mathcal{E}` to every node
      :math:`i \in \mathcal{V}` in the graph given by :attr:`edge_index`.
      In case the graph is weighted or has multi-dimensional edge features
      (:obj:`edge_attr != None`), edge features of self-loops will be added
      according to :obj:`fill_value`.
  
      Args:
          edge_index (LongTensor): The edge indices.
          edge_attr (Tensor, optional): Edge weights or multi-dimensional edge
              features. (default: :obj:`None`)
          fill_value (float or Tensor or str, optional): The way to generate
              edge features of self-loops (in case :obj:`edge_attr != None`).
              If given as :obj:`float` or :class:`torch.Tensor`, edge features of
              self-loops will be directly given by :obj:`fill_value`.
              If given as :obj:`str`, edge features of self-loops are computed by
              aggregating all features of edges that point to the specific node,
              according to a reduce operation. (:obj:`"add"`, :obj:`"mean"`,
              :obj:`"min"`, :obj:`"max"`, :obj:`"mul"`). (default: :obj:`1.`)
          num_nodes (int or Tuple[int, int], optional): The number of nodes,
              *i.e.* :obj:`max_val + 1` of :attr:`edge_index`.
              If given as a tuple, then :obj:`edge_index` is interpreted as a
              bipartite graph with shape :obj:`(num_src_nodes, num_dst_nodes)`.
              (default: :obj:`None`)
  
      :rtype: (:class:`LongTensor`, :class:`Tensor`)
  
      Examples:
          >>> edge_index = torch.tensor([[0, 1, 0],
          ...                            [1, 0, 0]])
          >>> edge_weight = torch.tensor([0.5, 0.5, 0.5])
          >>> add_self_loops(edge_index)
          (tensor([[0, 1, 0, 0, 1],
                  [1, 0, 0, 0, 1]]),
          None)
  
          >>> add_self_loops(edge_index, edge_weight)
          (tensor([[0, 1, 0, 0, 1],
                  [1, 0, 0, 0, 1]]),
          tensor([0.5000, 0.5000, 0.5000, 1.0000, 1.0000]))
  
          >>> # edge features of self-loops are filled by constant `2.0`
          >>> add_self_loops(edge_index, edge_weight,
          ...                fill_value=2.)
          (tensor([[0, 1, 0, 0, 1],
                  [1, 0, 0, 0, 1]]),
          tensor([0.5000, 0.5000, 0.5000, 2.0000, 2.0000]))
  
          >>> # Use 'add' operation to merge edge features for self-loops
          >>> add_self_loops(edge_index, edge_weight,
          ...                fill_value='add')
          (tensor([[0, 1, 0, 0, 1],
                  [1, 0, 0, 0, 1]]),
          tensor([0.5000, 0.5000, 0.5000, 1.0000, 0.5000]))
      """
      if not typing.TYPE_CHECKING and torch.jit.is_scripting():
          layout: Optional[int] = None
      else:
          layout: Optional[torch.layout] = None
      is_sparse = is_torch_sparse_tensor(edge_index)
  
      value: Optional[Tensor] = None
      if is_sparse:
          assert edge_attr is None
          layout = edge_index.layout
          size = (edge_index.size(0), edge_index.size(1))
          N = min(size)
          edge_index, value = to_edge_index(edge_index)
      elif isinstance(num_nodes, (tuple, list)):
          size = (num_nodes[0], num_nodes[1])
          N = min(size)
      else:
          N = maybe_num_nodes(edge_index, num_nodes)
          size = (N, N)
  
      device = edge_index.device
      if torch.jit.is_scripting():
          loop_index = (
              torch.arange(0, N, device=device).view(1, -1).repeat(2, 1)
          )
      else:
          loop_index = EdgeIndex(
              torch.arange(0, N, device=device)
              .view(1, -1)
              .repeat(2, 1),
              sparse_size=(N, N),
              is_undirected=True,
          )
      full_edge_index = torch.cat([edge_index, loop_index], dim=1)
  
      if is_sparse:
          assert edge_attr is None
          assert value is not None
          loop_attr = compute_loop_attr(  #
              edge_index, value, N, is_sparse, fill_value
          )
          value = torch.cat([value, loop_attr], dim=0)
  
          if (
              str(layout) == "torch.sparse_coo"
          ):  # str(...) for TorchScript :(
              return (
                  to_torch_coo_tensor(full_edge_index, value, size),
                  None,
              )
          elif str(layout) == "torch.sparse_csr":
              return (
                  to_torch_csr_tensor(full_edge_index, value, size),
                  None,
              )
          raise ValueError(
              f"Unexpected sparse tensor layout (got '{layout}')"
          )
  
      if edge_attr is not None:
          loop_attr = compute_loop_attr(  #
              edge_index, edge_attr, N, is_sparse, fill_value
          )
          edge_attr = torch.cat([edge_attr, loop_attr], dim=0)
  
      return full_edge_index, edge_attr
  
imports: import typing
  from typing import Optional, Tuple, Union
  import torch
  from torch import Tensor
  from torch_geometric import EdgeIndex
  from torch_geometric.utils import scatter
  from torch_geometric.utils.num_nodes import maybe_num_nodes
  from torch_geometric.utils.sparse import is_torch_sparse_tensor, to_edge_index, to_torch_coo_tensor, to_torch_csr_tensor
label_desc: Use image node_id 28 for calling a global function with example usage: add_self_loops(edge_index, edge_attr, fill_value, num_nodes) and returns: full_edge_index,  edge_attr, to_torch_coo_tensor,  None, to_torch_csr_tensor,  None