human
Solve the following python programming problem given the following code:
# Adapted from https://github.com/deepmind/jraph/blob/master/jraph/_src/models.py#L506
def GraphConvolution(update_node_fn: Callable,
                     aggregate_nodes_fn: Callable = jax.ops.segment_sum,
                     add_self_edges: bool = False,
                     symmetric_normalization: bool = True) -> Callable:
  """Returns a method that applies a Graph Convolution layer.

  Graph Convolutional layer as in https://arxiv.org/abs/1609.02907,
  NOTE: This implementation does not add an activation after aggregation.
  If you are stacking layers, you may want to add an activation between
  each layer.
  Args:
    update_node_fn: function used to update the nodes. In the paper a single
      layer MLP is used.
    aggregate_nodes_fn: function used to aggregates the sender nodes.
    add_self_edges: whether to add self edges to nodes in the graph as in the
      paper definition of GCN. Defaults to False.
    symmetric_normalization: whether to use symmetric normalization. Defaults to
      True.

  Returns:
    A method that applies a Graph Convolution layer.
  """

  def _ApplyGCN(graph: jraph.GraphsTuple) -> jraph.GraphsTuple:
    """Applies a Graph Convolution layer."""
    nodes, _, receivers, senders, _, _, _ = graph

    # First pass nodes through the node updater.
    nodes = update_node_fn(nodes)
    # Equivalent to jnp.sum(n_node), but jittable
    total_num_nodes = tree.tree_leaves(nodes)[0].shape[0]
    if add_self_edges:
      # We add self edges to the senders and receivers so that each node
      # includes itself in aggregation.
      # In principle, a `GraphsTuple` should partition by n_edge, but in
      # this case it is not required since a GCN is agnostic to whether
      # the `GraphsTuple` is a batch of graphs or a single large graph.
      conv_receivers, conv_senders = add_self_edges_fn(receivers, senders,
                                                       total_num_nodes)
    else:
      conv_senders = senders
      conv_receivers = receivers

    # pylint:...
Given the above code, generate some markdown instructions for it.
assistant
#Here is the Python code you requested.

Add Symmetric Normalization
Note that the nodes may have different numbers of neighbors / degrees.
This could lead to instabilities during neural network training, e.g. exploding or vanishing gradients. To address that, normalization is a commonly used method. In this case, we will normalize by node degrees.
As a first attempt, we could count the number of incoming edges (including self-edge) and divide by that value.
More formally, let $A$ be the adjacency matrix defining the edges of the graph.
Then we define the degree matrix $D$ as a diagonal matrix with $D_{ii} = \sum_jA_{ij}$ (the degree of node $i$)
Now we can normalize $AH$ by dividing it by the node degrees:
$${D}^{-1}AH$$
To take both the in and out degrees into account, we can use symmetric normalization, which is also what Kipf and Welling proposed in their paper:
$$D^{-\frac{1}{2}}AD^{-\frac{1}{2}}H$$
General GCN Layer
Now we can write a more general and configurable version of the Graph Convolution layer, allowing the caller to specify:

update_node_fn: Function to use to update node features (e.g. the MLP block version we just implemented)
aggregate_nodes_fn: Aggregation function to use to aggregate messages from neighbourhood.
add_self_edges: Whether to add self edges for aggregation step.
symmetric_normalization: Whether to add symmetric normalization.