class_docstr: A block containing multiple subsequent graph convolution stacks.
  
  One convolution stack consists of one graph convolution in addition to an activation layer. The block expects the
  input tensors to have the form:
  - Feature matrix: first in_key: (batch-dim, num-of-nodes, feature-dim)
  - Adjacency matrix: second in_key: (batch-dim, num-of-nodes, num-of-nodes) (also symmetric)
  And returns a tensor of the form (batch-dim, num-of-nodes, feature-out-dim).
  
  :param in_keys: Two keys identifying the feature matrix and adjacency matrix respectively.
  :param out_keys: One key identifying the output tensors.
  :param in_shapes: List of input shapes.
  :param hidden_features: List containing the number of hidden features for hidden layers.
  :param bias: Specify if a bias should be used at each layer (can be list or single).
  :param non_lins: The non-linearity/ies to apply after each layer (the same in all layers, or a list corresponding
      to each layer).
  :param node_self_importance: Specify how important a given node is to itself (default should be 1).
  :param trainable_node_self_importance: Specify if the node_self_importance should be a constant or a trainable
      parameter with init value :param node_self_importance.
  :param preprocess_adj: Specify whether to preprocess the adjacency, that is compute:
      adj^ := D^bar^(-1/2) A^bar D^bar^(-1/2) in every forward pass for the whole bach. If this is set to false, the
      already normalized adj^ is expected as an input. Here A^bar := A + I_n * :param self_importance_scalar, and
      D^bar_ii := sum_j A^bar_ij.
code: def __init__(
      self,
      in_keys: Union[str, List[str]],
      out_keys: Union[str, List[str]],
      in_shapes: Union[Sequence[int], List[Sequence[int]]],
      hidden_features: List[int],
      bias: Union[bool, List[bool]],
      non_lins: Union[
          str, type(nn.Module), List[str], List[type(nn.Module)]
      ],
      node_self_importance: float,
      trainable_node_self_importance: bool,
      preprocess_adj: bool,
  ):
      super().__init__(
          in_keys=in_keys,
          out_keys=out_keys,
          in_shapes=in_shapes,
          in_num_dims=[3, 3],
          out_num_dims=3,
      )
  
      # Assertions
      assert (
          len(self.in_keys) == 2
      ), "There should be two input keys, feature matrix + adjacency matrix"
      assert self.in_shapes[0][-2] == self.in_shapes[1][-1], (
          "The node dimension of the feature matrix should be "
          "the same as the adjacency matrix's rows and "
          f"columns {self.in_shapes}"
      )
      assert (
          self.in_shapes[1][-1] == self.in_shapes[1][-2]
      ), "The adjacency matrix has to be a square matrix"
  
      # Specify dummy dict creation function for adjacency matrix:
      self.dummy_dict_creators[
          1
      ] = self._dummy_symmetric_adj_tensor_factory(self.in_shapes[1])
  
      # Init class objects
      self.input_features = self.in_shapes[0][-1]
      self.hidden_features = hidden_features
      self.output_features = self.hidden_features[-1]
  
      self.preprocess_adj = preprocess_adj
  
      # Create list of non-linearity's for each layer
      non_lins = (
          non_lins
          if isinstance(non_lins, list)
          else [non_lins] * len(self.hidden_features)
      )
      self.non_lins: List[type(nn.Module)] = [
          Factory(base_type=nn.Module).type_from_name(non_lin)
          for non_lin in non_lins
      ]
      # Create list of biases for each layer
      self.bias: List[bool] = (
          bias
          if isinstance(bias, list)
          else [bias] * len(self.hidden_features)
      )
  
      # Initialize node-self-importance scalar
      self.node_self_importance = torch.tensor(
          node_self_importance, dtype=torch.float32, requires_grad=False
      )
  
      # Initialize node-self-importance scalar as trainable parameter if applicable
      if trainable_node_self_importance:
          # Save the default value for weight init
          self.node_self_importance_default = self.node_self_importance
          self.node_self_importance = Parameter(
              self.node_self_importance
          )
          assert self.node_self_importance.requires_grad is True
          assert (
              self.node_self_importance_default.requires_grad is False
          )
  
      # compile layer dictionary
      layer_dict = self.build_layer_dict()
  
      # compile network
      self.net = nn.Sequential(layer_dict)
  
imports: from collections import OrderedDict
  from typing import Union, List, Sequence, Dict, Optional, Callable
  import numpy
  import torch
  from torch import nn
  from torch.nn.parameter import Parameter
  from maze.core.annotations import override
  from maze.core.utils.factory import Factory
  from maze.perception.blocks.shape_normalization import ShapeNormalizationBlock
label_desc: Use image node_id 1 to create a new GraphConvBlock object from inherited base classes: ShapeNormalizationBlock, GraphAdjacencyMethods with example: obj = GraphConvBlock(in_keys, out_keys, in_shapes, hidden_features, bias, non_lins, node__importance, trainable_node__importance, preprocess_adj)