class_docstr: A data generator for link prediction with directed Homogeneous GraphSAGE models
  
  At minimum, supply the StellarDiGraph, the batch size, and the number of
  node samples (separately for in-nodes and out-nodes) for each layer of the GraphSAGE model.
  
  The supplied graph should be a StellarDiGraph object with node features.
  
  Use the :meth:`flow` method supplying the nodes and (optionally) targets,
  or an UnsupervisedSampler instance that generates node samples on demand,
  to get an object that can be used as a Keras data generator.
  
  Example::
  
      G_generator = DirectedGraphSageLinkGenerator(G, 50, [10,10], [10,10])
      train_data_gen = G_generator.flow(edge_ids)
  
  .. seealso::
  
     Model using this generator: :class:`.GraphSAGE`.
  
     Related functionality:
  
     - :class:`.UnsupervisedSampler` for unsupervised training using random walks
     - :class:`.DirectedGraphSAGENodeGenerator` for node classification and related tasks
     - :class:`.GraphSAGELinkGenerator` for undirected graphs
     - :class:`.HinSAGELinkGenerator` for heterogeneous graphs
  
  Args:
      G (StellarGraph): A machine-learning ready graph.
      batch_size (int): Size of batch of links to return.
      in_samples (list): The number of in-node samples per layer (hop) to take.
      out_samples (list): The number of out-node samples per layer (hop) to take.
      seed (int or str), optional: Random seed for the sampling methods.
      name, optional: Name of generator.
      weighted (bool, optional): If True, sample neighbours using the edge weights in the graph.
class_docstr_tok: ["A","data","generator","for","link","prediction","with","directed","Homogeneous","GraphSAGE","models","At","minimum",",","supply","the","StellarDiGraph",",","the","batch","size",",","and","the","number","of","node","samples","(","separately","for","in-nodes","and","out-nodes",")","for","each","layer","of","the","GraphSAGE","model",".","The","supplied","graph","should","be","a","StellarDiGraph","object","with","node","features",".","Use","the",":","meth",":","`","flow","`","method","supplying","the","nodes","and","(","optionally",")","targets",",","or","an","UnsupervisedSampler","instance","that","generates","node","samples","on","demand",",","to","get","an","object","that","can","be","used","as","a","Keras","data","generator",".","Example",":",":","G_generator","=","DirectedGraphSageLinkGenerator","(","G",",","50",",","[","10,10","]",",","[","10,10","]",")","train_data_gen","=","G_generator.flow","(","edge_ids",")","..","seealso",":",":","Model","using","this","generator",":",":","class",":","`",".GraphSAGE","`",".","Related","functionality",":","-",":","class",":","`",".UnsupervisedSampler","`","for","unsupervised","training","using","random","walks","-",":","class",":","`",".DirectedGraphSAGENodeGenerator","`","for","node","classification","and","related","tasks","-",":","class",":","`",".GraphSAGELinkGenerator","`","for","undirected","graphs","-",":","class",":","`",".HinSAGELinkGenerator","`","for","heterogeneous","graphs","Args",":","G","(","StellarGraph",")",":","A","machine-learning","ready","graph",".","batch_size","(","int",")",":","Size","of","batch","of","links","to","return",".","in_samples","(","list",")",":","The","number","of","in-node","samples","per","layer","(","hop",")","to","take",".","out_samples","(","list",")",":","The","number","of","out-node","samples","per","layer","(","hop",")","to","take",".","seed","(","int","or","str",")",",","optional",":","Random","seed","for","the","sampling","methods",".","name",",","optional",":","Name","of","generator",".","weighted","(","bool",",","optional",")",":","If","True",",","sample","neighbours","using","the","edge","weights","in","the","graph","."]
docstr: Sample neighbours recursively from the head links, collect the features of the
  sampled nodes, and return these as a list of feature arrays for the GraphSAGE
  algorithm.
  
  Args:
      head_links: An iterable of head links to perform sampling on.
  
  Returns:
      A list of feature tensors from the sampled nodes at each layer, each of shape:
      ``(len(head_nodes), num_sampled_at_layer, feature_size)``
      where ``num_sampled_at_layer`` is the total number (cumulative product)
      of nodes sampled at the given number of hops from each head node,
      given the sequence of in/out directions.
code: def sample_features(self, head_links, batch_num):
      """
      Sample neighbours recursively from the head links, collect the features of the
      sampled nodes, and return these as a list of feature arrays for the GraphSAGE
      algorithm.
  
      Args:
          head_links: An iterable of head links to perform sampling on.
  
      Returns:
          A list of feature tensors from the sampled nodes at each layer, each of shape:
          ``(len(head_nodes), num_sampled_at_layer, feature_size)``
          where ``num_sampled_at_layer`` is the total number (cumulative product)
          of nodes sampled at the given number of hops from each head node,
          given the sequence of in/out directions.
      """
  
      batch_feats = []
      for hns in zip(*head_links):
          node_samples = self._samplers[batch_num].run(
              nodes=hns,
              n=1,
              in_size=self.in_samples,
              out_size=self.out_samples,
              weighted=self.weighted,
          )
  
          # Reshape node samples to sensible format
          # Each 'slot' represents the list of nodes sampled from some neighbourhood, and will have a corresponding
          # NN input layer. Every hop potentially generates both in-nodes and out-nodes, held separately,
          # and thus the slot (or directed hop sequence) structure forms a binary tree.
  
          node_type = self.head_node_types[0]
  
          max_hops = len(self.in_samples)
          max_slots = 2 ** (max_hops + 1) - 1
          features = [None] * max_slots  # flattened binary tree
  
          for slot in range(max_slots):
              nodes_in_slot = [
                  element
                  for sample in node_samples
                  for element in sample[slot]
              ]
              features_for_slot = self.graph.node_features(
                  nodes_in_slot,
                  node_type,
                  use_ilocs=True,
              )
  
              features[slot] = np.reshape(
                  features_for_slot,
                  (len(hns), -1, features_for_slot.shape[1]),
              )
  
          # Get features for the sampled nodes
          batch_feats.append(features)
  
      # Resize features to (batch_size, n_neighbours, feature_size)
      # and re-pack features into a list where source, target feats alternate
      # This matches the GraphSAGE link model with (node_src, node_dst) input sockets:
      batch_feats = [feats for ab in zip(*batch_feats) for feats in ab]
      return batch_feats
  
imports: import random
  import operator
  import numpy
  import itertools
  import operator
  import collections
  import abc
  import warnings
  from functools import reduce
  from tensorflow import keras
  from ..core.graph import StellarGraph, GraphSchema
  from ..data import SampledBreadthFirstWalk, SampledHeterogeneousBreadthFirstWalk, UniformRandomWalk, UnsupervisedSampler, DirectedBreadthFirstNeighbours
  from ..core.utils import is_real_iterable
  from .None import LinkSequence, OnDemandLinkSequence
  from ..random import SeededPerBatch
  from .base import Generator
label_desc: Use image node_id 2 for calling the DirectedGraphSAGELinkGenerator obj's underlying member method code with example usage: obj.sample_features(head_links, batch_num) and returns: batch_feats