text: import re
  import copy
  import logging
  import collections
  
  import torch
  import torch_geometric
  from torch_sparse import coalesce, SparseTensor
  from torch_geometric.utils import (contains_isolated_nodes,
                                     contains_self_loops, is_undirected)
  
  from ..utils.num_nodes import maybe_num_nodes
  
  __num_nodes_warn_msg__ = (
      'The number of nodes in your data object can only be inferred by its {} '
      'indices, and hence may result in unexpected batch-wise behavior, e.g., '
      'in case there exists isolated nodes. Please consider explicitly setting '
      'the number of nodes for this data object by assigning it to '
      'data.num_nodes.')
  
  
  def size_repr(key, item, indent=0):
      indent_str = ' ' * indent
      if torch.is_tensor(item) and item.dim() == 0:
          out = item.item()
      elif torch.is_tensor(item):
          out = str(list(item.size()))
      elif isinstance(item, SparseTensor):
          out = str(item.sizes())[:-1] + f', nnz={item.nnz()}]'
      elif isinstance(item, list) or isinstance(item, tuple):
          out = str([len(item)])
      elif isinstance(item, dict):
          lines = [indent_str + size_repr(k, v, 2) for k, v in item.items()]
          out = '{\n' + ',\n'.join(lines) + '\n' + indent_str + '}'
      elif isinstance(item, str):
          out = f'"{item}"'
      else:
          out = str(item)
  
      return f'{indent_str}{key}={out}'
  
  
  class Data(object):
      r"""A plain old python object modeling a single graph with various
      (optional) attributes:
  
      Args:
          x (Tensor, optional): Node feature matrix with shape :obj:`[num_nodes,
              num_node_features]`. (default: :obj:`None`)
          edge_index (LongTensor, optional): Graph connectivity in COO format
              with shape :obj:`[2, num_edges]`. (default: :obj:`None`)
          edge_attr (Tensor, optional): Edge feature matrix with shape
              :obj:`[num_edges, num_edge_features]`. (default: :obj:`None`)
          y (Tensor, optional): Graph or node targets with arbitrary shape.
              (default: :obj:`None`)
          pos (Tensor, optional): Node position matrix with shape
              :obj:`[num_nodes, num_dimensions]`. (default: :obj:`None`)
          normal (Tensor, optional): Normal vector matrix with shape
              :obj:`[num_nodes, num_dimensions]`. (default: :obj:`None`)
          face (LongTensor, optional): Face adjacency matrix with shape
              :obj:`[3, num_faces]`. (default: :obj:`None`)
  
      The data object is not restricted to these attributes and can be extented
      by any other additional data.
  
      Example::
  
          data = Data(x=x, edge_index=edge_index)
          data.train_idx = torch.tensor([...], dtype=torch.long)
          data.test_mask = torch.tensor([...], dtype=torch.bool)
      """
      def __init__(self, x=None, edge_index=None, edge_attr=None, y=None,
                   pos=None, normal=None, face=None, **kwargs):
          self.x = x
          self.edge_index = edge_index
          self.edge_attr = edge_attr
          self.y = y
          self.pos = pos
          self.normal = normal
          self.face = face
          for key, item in kwargs.items():
              if key == 'num_nodes':
                  self.__num_nodes__ = item
              else:
                  self[key] = item
  
          if edge_index is not None and edge_index.dtype != torch.long:
              raise ValueError(
                  (f'Argument `edge_index` needs to be of type `torch.long` but '
                   f'found type `{edge_index.dtype}`.'))
  
          if face is not None and face.dtype != torch.long:
              raise ValueError(
                  (f'Argument `face` needs to be of type `torch.long` but found '
                   f'type `{face.dtype}`.'))
  
          if torch_geometric.is_debug_enabled():
              self.debug()
  
      @classmethod
      def from_dict(cls, dictionary):
          r"""Creates a data object from a python dictionary."""
          data = cls()
  
          for key, item in dictionary.items():
              data[key] = item
  
          if torch_geometric.is_debug_enabled():
              data.debug()
  
          return data
  
      def to_dict(self):
          return {key: item for key, item in self}
  
      def to_namedtuple(self):
          keys = self.keys
          DataTuple = collections.namedtuple('DataTuple', keys)
          return DataTuple(*[self[key] for key in keys])
  
      def __getitem__(self, key):
          r"""Gets the data of the attribute :obj:`key`."""
          return getattr(self, key, None)
  
      def __setitem__(self, key, value):
          """Sets the attribute :obj:`key` to :obj:`value`."""
          setattr(self, key, value)
  
      @property
      def keys(self):
          r"""Returns all names of graph attributes."""
          keys = [key for key in self.__dict__.keys() if self[key] is not None]
          keys = [key for key in keys if key[:2] != '__' and key[-2:] != '__']
          return keys
  
      def __len__(self):
          r"""Returns the number of all present attributes."""
          return len(self.keys)
  
      def __contains__(self, key):
          r"""Returns :obj:`True`, if the attribute :obj:`key` is present in the
          data."""
          return key in self.keys
  
      def __iter__(self):
          r"""Iterates over all present attributes in the data, yielding their
          attribute names and content."""
          for key in sorted(self.keys):
              yield key, self[key]
  
      def __call__(self, *keys):
          r"""Iterates over all attributes :obj:`*keys` in the data, yielding
          their attribute names and content.
          If :obj:`*keys` is not given this method will iterative over all
          present attributes."""
          for key in sorted(self.keys) if not keys else keys:
              if key in self:
                  yield key, self[key]
  
      def __cat_dim__(self, key, value):
          r"""Returns the dimension for which :obj:`value` of attribute
          :obj:`key` will get concatenated when creating batches.
  
          .. note::
  
              This method is for internal use only, and should only be overridden
              if the batch concatenation process is corrupted for a specific data
              attribute.
          """
          # By default, concatenate sparse matrices diagonally.
          if isinstance(value, SparseTensor):
              return (0, 1)
          # Concatenate `*index*` and `*face*` attributes in the last dimension.
          elif bool(re.search('(index|face)', key)):
              return -1
          return 0
  
      def __inc__(self, key, value):
          r"""Returns the incremental count to cumulatively increase the value
          of the next attribute of :obj:`key` when creating batches.
  
          .. note::
  
              This method is for internal use only, and should only be overridden
              if the batch concatenation process is corrupted for a specific data
              attribute.
          """
          # Only `*index*` and `*face*` attributes should be cumulatively summed
          # up when creating batches.
          return self.num_nodes if bool(re.search('(index|face)', key)) else 0
  
      @property
      def num_nodes(self):
          r"""Returns or sets the number of nodes in the graph.
  
          .. note::
              The number of nodes in your data object is typically automatically
              inferred, *e.g.*, when node features :obj:`x` are present.
              In some cases however, a graph may only be given by its edge
              indices :obj:`edge_index`.
              PyTorch Geometric then *guesses* the number of nodes
              according to :obj:`edge_index.max().item() + 1`, but in case there
              exists isolated nodes, this number has not to be correct and can
              therefore result in unexpected batch-wise behavior.
              Thus, we recommend to set the number of nodes in your data object
              explicitly via :obj:`data.num_nodes = ...`.
              You will be given a warning that requests you to do so.
          """
          if hasattr(self, '__num_nodes__'):
              return self.__num_nodes__
          for key, item in self('x', 'pos', 'normal', 'batch'):
              if isinstance(item, SparseTensor):
                  return item.size(0)
              else:
                  return item.size(self.__cat_dim__(key, item))
          if hasattr(self, 'adj'):
              return self.adj.size(0)
          if hasattr(self, 'adj_t'):
              return self.adj_t.size(1)
          if self.face is not None:
              logging.warning(__num_nodes_warn_msg__.format('face'))
              return maybe_num_nodes(self.face)
          if self.edge_index is not None:
              logging.warning(__num_nodes_warn_msg__.format('edge'))
              return maybe_num_nodes(self.edge_index)
          return None
  
      @num_nodes.setter
      def num_nodes(self, num_nodes):
          self.__num_nodes__ = num_nodes
  
      @property
      def num_edges(self):
          """
          Returns the number of edges in the graph.
          For undirected graphs, this will return the number of bi-directional
          edges, which is double the amount of unique edges.
          """
          for key, item in self('edge_index', 'edge_attr'):
              return item.size(self.__cat_dim__(key, item))
          for key, item in self('adj', 'adj_t'):
              return item.nnz()
          return None
  
      @property
      def num_faces(self):
          r"""Returns the number of faces in the mesh."""
          if self.face is not None:
              return self.face.size(self.__cat_dim__('face', self.face))
          return None
  
      @property
      def num_node_features(self):
          r"""Returns the number of features per node in the graph."""
          if self.x is None:
              return 0
          return 1 if self.x.dim() == 1 else self.x.size(1)
  
      @property
      def num_features(self):
          r"""Alias for :py:attr:`~num_node_features`."""
          return self.num_node_features
  
      @property
      def num_edge_features(self):
          r"""Returns the number of features per edge in the graph."""
          if self.edge_attr is None:
              return 0
          return 1 if self.edge_attr.dim() == 1 else self.edge_attr.size(1)
  
      def is_coalesced(self):
          r"""Returns :obj:`True`, if edge indices are ordered and do not contain
          duplicate entries."""
          edge_index, _ = coalesce(self.edge_index, None, self.num_nodes,
                                   self.num_nodes)
          return self.edge_index.numel() == edge_index.numel() and (
              self.edge_index != edge_index).sum().item() == 0
  
      def coalesce(self):
          r""""Orders and removes duplicated entries from edge indices."""
          self.edge_index, self.edge_attr = coalesce(self.edge_index,
                                                     self.edge_attr,
                                                     self.num_nodes,
                                                     self.num_nodes)
          return self
  
      def contains_isolated_nodes(self):
          r"""Returns :obj:`True`, if the graph contains isolated nodes."""
          return contains_isolated_nodes(self.edge_index, self.num_nodes)
  
      def contains_self_loops(self):
          """Returns :obj:`True`, if the graph contains self-loops."""
          return contains_self_loops(self.edge_index)
  
      def is_undirected(self):
          r"""Returns :obj:`True`, if graph edges are undirected."""
          return is_undirected(self.edge_index, self.edge_attr, self.num_nodes)
  
      def is_directed(self):
          r"""Returns :obj:`True`, if graph edges are directed."""
          return not self.is_undirected()
  
      def __apply__(self, item, func):
          if torch.is_tensor(item):
              return func(item)
          elif isinstance(item, SparseTensor):
              # Not all apply methods are supported for `SparseTensor`, e.g.,
              # `contiguous()`. We can get around it by capturing the exception.
              try:
                  return func(item)
              except AttributeError:
                  return item
          elif isinstance(item, (tuple, list)):
              return [self.__apply__(v, func) for v in item]
          elif isinstance(item, dict):
              return {k: self.__apply__(v, func) for k, v in item.items()}
          else:
              return item
  
      def apply(self, func, *keys):
          r"""Applies the function :obj:`func` to all tensor attributes
          :obj:`*keys`. If :obj:`*keys` is not given, :obj:`func` is applied to
          all present attributes.
          """
          for key, item in self(*keys):
              self[key] = self.__apply__(item, func)
          return self
  
      def contiguous(self, *keys):
          r"""Ensures a contiguous memory layout for all attributes :obj:`*keys`.
          If :obj:`*keys` is not given, all present attributes are ensured to
          have a contiguous memory layout."""
          return self.apply(lambda x: x.contiguous(), *keys)
  
      def to(self, device, *keys, **kwargs):
          r"""Performs tensor dtype and/or device conversion to all attributes
          :obj:`*keys`.
          If :obj:`*keys` is not given, the conversion is applied to all present
          attributes."""
          return self.apply(lambda x: x.to(device, **kwargs), *keys)
  
      def cpu(self, *keys):
          r"""Copies all attributes :obj:`*keys` to CPU memory.
          If :obj:`*keys` is not given, the conversion is applied to all present
          attributes."""
          return self.apply(lambda x: x.cpu(), *keys)
  
      def cuda(self, device=None, non_blocking=False, *keys):
          r"""Copies all attributes :obj:`*keys` to CUDA memory.
          If :obj:`*keys` is not given, the conversion is applied to all present
          attributes."""
          return self.apply(
              lambda x: x.cuda(device=device, non_blocking=non_blocking), *keys)
  
      def clone(self):
          r"""Performs a deep-copy of the data object."""
          return self.__class__.from_dict({
              k: v.clone() if torch.is_tensor(v) else copy.deepcopy(v)
              for k, v in self.__dict__.items()
          })
  
      def pin_memory(self, *keys):
          r"""Copies all attributes :obj:`*keys` to pinned memory.
          If :obj:`*keys` is not given, the conversion is applied to all present
          attributes."""
          return self.apply(lambda x: x.pin_memory(), *keys)
  
      def debug(self):
          if self.edge_index is not None:
              if self.edge_index.dtype != torch.long:
                  raise RuntimeError(
                      ('Expected edge indices of dtype {}, but found dtype '
                       ' {}').format(torch.long, self.edge_index.dtype))
  
          if self.face is not None:
              if self.face.dtype != torch.long:
                  raise RuntimeError(
                      ('Expected face indices of dtype {}, but found dtype '
                       ' {}').format(torch.long, self.face.dtype))
  
          if self.edge_index is not None:
              if self.edge_index.dim() != 2 or self.edge_index.size(0) != 2:
                  raise RuntimeError(
                      ('Edge indices should have shape [2, num_edges] but found'
                       ' shape {}').format(self.edge_index.size()))
  
          if self.edge_index is not None and self.num_nodes is not None:
              if self.edge_index.numel() > 0:
                  min_index = self.edge_index.min()
                  max_index = self.edge_index.max()
              else:
                  min_index = max_index = 0
              if min_index < 0 or max_index > self.num_nodes - 1:
                  raise RuntimeError(
                      ('Edge indices must lay in the interval [0, {}]'
                       ' but found them in the interval [{}, {}]').format(
                           self.num_nodes - 1, min_index, max_index))
  
          if self.face is not None:
              if self.face.dim() != 2 or self.face.size(0) != 3:
                  raise RuntimeError(
                      ('Face indices should have shape [3, num_faces] but found'
                       ' shape {}').format(self.face.size()))
  
          if self.face is not None and self.num_nodes is not None:
              if self.face.numel() > 0:
                  min_index = self.face.min()
                  max_index = self.face.max()
              else:
                  min_index = max_index = 0
              if min_index < 0 or max_index > self.num_nodes - 1:
                  raise RuntimeError(
                      ('Face indices must lay in the interval [0, {}]'
                       ' but found them in the interval [{}, {}]').format(
                           self.num_nodes - 1, min_index, max_index))
  
          if self.edge_index is not None and self.edge_attr is not None:
              if self.edge_index.size(1) != self.edge_attr.size(0):
                  raise RuntimeError(
                      ('Edge indices and edge attributes hold a differing '
                       'number of edges, found {} and {}').format(
                           self.edge_index.size(), self.edge_attr.size()))
  
          if self.x is not None and self.num_nodes is not None:
              if self.x.size(0) != self.num_nodes:
                  raise RuntimeError(
                      ('Node features should hold {} elements in the first '
                       'dimension but found {}').format(self.num_nodes,
                                                        self.x.size(0)))
  
          if self.pos is not None and self.num_nodes is not None:
              if self.pos.size(0) != self.num_nodes:
                  raise RuntimeError(
                      ('Node positions should hold {} elements in the first '
                       'dimension but found {}').format(self.num_nodes,
                                                        self.pos.size(0)))
  
          if self.normal is not None and self.num_nodes is not None:
              if self.normal.size(0) != self.num_nodes:
                  raise RuntimeError(
                      ('Node normals should hold {} elements in the first '
                       'dimension but found {}').format(self.num_nodes,
                                                        self.normal.size(0)))
  
      def __repr__(self):
          cls = str(self.__class__.__name__)
          has_dict = any([isinstance(item, dict) for _, item in self])
  
          if not has_dict:
              info = [size_repr(key, item) for key, item in self]
              return '{}({})'.format(cls, ', '.join(info))
          else:
              info = [size_repr(key, item, indent=2) for key, item in self]
              return '{}(\n{}\n)'.format(cls, ',\n'.join(info))
  