def sample_layer_neighbors(
        self,
        seeds: Union[torch.Tensor, Dict[str, torch.Tensor]],
        fanouts: torch.Tensor,
        replace: bool = False,
        probs_name: Optional[str] = None,
        returning_indices_and_original_edge_ids_are_optional: bool = False,
        random_seed: torch.Tensor = None,
        seed2_contribution: float = 0.0,
        async_op: bool = False,
    ) -> SampledSubgraphImpl:
        """Sample neighboring edges of the given nodes and return the induced
        subgraph via layer-neighbor sampling from the NeurIPS 2023 paper
        `Layer-Neighbor Sampling -- Defusing Neighborhood Explosion in GNNs
        <https://proceedings.neurips.cc/paper_files/paper/2023/file/51f9036d5e7ae822da8f6d4adda1fb39-Paper-Conference.pdf>`__

        Parameters
        ----------
        seeds: torch.Tensor or Dict[str, torch.Tensor]
            IDs of the given seed nodes.
              - If `nodes` is a tensor: It means the graph is homogeneous
                graph, and ids inside are homogeneous ids.
              - If `nodes` is a dictionary: The keys should be node type and
                ids inside are heterogeneous ids.
        fanouts: torch.Tensor
            The number of edges to be sampled for each node with or without
            considering edge types.
              - When the length is 1, it indicates that the fanout applies to
                all neighbors of the node as a collective, regardless of the
                edge type.
              - Otherwise, the length should equal to the number of edge
                types, and each fanout value corresponds to a specific edge
                type of the nodes.
            The value of each fanout should be >= 0 or = -1.
              - When the value is -1, all neighbors (with non-zero probability,
                if weighted) will be sampled once regardless of replacement. It
                is equivalent to selecting all neighbors with non-zero
                probability when the fanout is >= the number of neighbors (and
                replace is set to false).
              - When the value is a non-negative integer, it serves as a
                minimum threshold for selecting neighbors.
        replace: bool
            Boolean indicating whether the sample is preformed with or
            without replacement. If True, a value can be selected multiple
            times. Otherwise, each value can be selected only once.
        probs_name: str, optional
            An optional string specifying the name of an edge attribute. This
            attribute tensor should contain (unnormalized) probabilities
            corresponding to each neighboring edge of a node. It must be a 1D
            floating-point or boolean tensor, with the number of elements
            equalling the total number of edges.
        returning_indices_and_original_edge_ids_are_optional: bool
            Boolean indicating whether it is okay for the call to this function
            to leave the indices and the original edge ids tensors
            uninitialized. In this case, it is the user's responsibility to
            gather them using _edge_ids_in_fused_csc_sampling_graph if either is
            missing.
        random_seed: torch.Tensor, optional
            An int64 tensor with one or two elements.

            The passed random_seed makes it so that for any seed node ``s`` and
            its neighbor ``t``, the rolled random variate ``r_t`` is the same
            for any call to this function with the same random seed. When
            sampling as part of the same batch, one would want identical seeds
            so that LABOR can globally sample. One example is that for
            heterogenous graphs, there is a single random seed passed for each
            edge type. This will sample much fewer nodes compared to having
            unique random seeds for each edge type. If one called this function
            individually for each edge type for a heterogenous graph with
            different random seeds, then it would run LABOR locally for each
            edge type, resulting into a larger number of nodes being sampled.

            If this function is called without a ``random_seed``, we get the
            random seed by getting a random number from GraphBolt. Use this
            argument with identical random_seed if multiple calls to this
            function are used to sample as part of a single batch.

            If given two numbers, then the ``seed2_contribution`` argument
            determines the interpolation between the two random seeds.
        seed2_contribution: float, optional
            A float value between [0, 1) that determines the contribution of the
            second random seed, ``random_seed[-1]``, to generate the random
            variates.
        async_op: bool
            Boolean indicating whether the call is asynchronous. If so, the
            result can be obtained by calling wait on the returned future.

        Returns
        -------
        SampledSubgraphImpl
            The sampled subgraph.

        Examples
        --------
        >>> import dgl.graphbolt as gb
        >>> import torch
        >>> ntypes = {"n1": 0, "n2": 1}
        >>> etypes = {"n1:e1:n2": 0, "n2:e2:n1": 1}
        >>> indptr = torch.LongTensor([0, 2, 4, 6, 7, 9])
        >>> indices = torch.LongTensor([2, 4, 2, 3, 0, 1, 1, 0, 1])
        >>> node_type_offset = torch.LongTensor([0, 2, 5])
        >>> type_per_edge = torch.LongTensor([1, 1, 1, 1, 0, 0, 0, 0, 0])
        >>> graph = gb.fused_csc_sampling_graph(indptr, indices,
        ...     node_type_offset=node_type_offset,
        ...     type_per_edge=type_per_edge,
        ...     node_type_to_id=ntypes,
        ...     edge_type_to_id=etypes)
        >>> nodes = {'n1': torch.LongTensor([0]), 'n2': torch.LongTensor([0])}
        >>> fanouts = torch.tensor([1, 1])
        >>> subgraph = graph.sample_layer_neighbors(nodes, fanouts)
        >>> print(subgraph.sampled_csc)
        {'n1:e1:n2': CSCFormatBase(indptr=tensor([0, 1]),
                    indices=tensor([0]),
        ), 'n2:e2:n1': CSCFormatBase(indptr=tensor([0, 1]),
                    indices=tensor([2]),
        )}
        """
        if random_seed is not None:
            assert (
                1 <= len(random_seed) <= 2
            ), "There should be a 1 or 2 random seeds."
            if len(random_seed) == 2:
                assert (
                    0 <= seed2_contribution <= 1
                ), "seed2_contribution should be in [0, 1]."

        seed_offsets = None
        if isinstance(seeds, dict):
            seeds, seed_offsets = self._convert_to_homogeneous_nodes(seeds)
        elif seeds is None:
            seed_offsets = self._indptr_node_type_offset_list
        probs_or_mask = self.edge_attributes[probs_name] if probs_name else None
        self._check_sampler_arguments(seeds, fanouts, probs_or_mask)
        sampling_fn = (
            self._c_csc_graph.sample_neighbors_async
            if async_op
            else self._c_csc_graph.sample_neighbors
        )
        C_sampled_subgraph = sampling_fn(
            seeds,
            seed_offsets,
            fanouts.tolist(),
            replace,
            True,  # is_labor
            returning_indices_and_original_edge_ids_are_optional,
            probs_or_mask,
            random_seed,
            seed2_contribution,
        )
        if async_op:
            return _SampleNeighborsWaiter(
                self._convert_to_sampled_subgraph,
                C_sampled_subgraph,
                seed_offsets,
                returning_indices_and_original_edge_ids_are_optional,
            )
        else:
            return self._convert_to_sampled_subgraph(
                C_sampled_subgraph,
                seed_offsets,
                returning_indices_and_original_edge_ids_are_optional,
            )
Sample neighboring edges of the given nodes and return the induced
        subgraph via layer-neighbor sampling from the NeurIPS 2023 paper
        `Layer-Neighbor Sampling -- Defusing Neighborhood Explosion in GNNs
        <https://proceedings.neurips.cc/paper_files/paper/2023/file/51f9036d5e7ae822da8f6d4adda1fb39-Paper-Conference.pdf>`__

        Parameters
        ----------
        seeds: torch.Tensor or Dict[str, torch.Tensor]
            IDs of the given seed nodes.
              - If `nodes` is a tensor: It means the graph is homogeneous
                graph, and ids inside are homogeneous ids.
              - If `nodes` is a dictionary: The keys should be node type and
                ids inside are heterogeneous ids.
        fanouts: torch.Tensor
            The number of edges to be sampled for each node with or without
            considering edge types.
              - When the length is 1, it indicates that the fanout applies to
                all neighbors of the node as a collective, regardless of the
                edge type.
              - Otherwise, the length should equal to the number of edge
                types, and each fanout value corresponds to a specific edge
                type of the nodes.
            The value of each fanout should be >= 0 or = -1.
              - When the value is -1, all neighbors (with non-zero probability,
                if weighted) will be sampled once regardless of replacement. It
                is equivalent to selecting all neighbors with non-zero
                probability when the fanout is >= the number of neighbors (and
                replace is set to false).
              - When the value is a non-negative integer, it serves as a
                minimum threshold for selecting neighbors.
        replace: bool
            Boolean indicating whether the sample is preformed with or
            without replacement. If True, a value can be selected multiple
            times. Otherwise, each value can be selected only once.
        probs_name: str, optional
            An optional string specifying the name of an edge attribute. This
            attribute tensor should contain (unnormalized) probabilities
            corresponding to each neighboring edge of a node. It must be a 1D
            floating-point or boolean tensor, with the number of elements
            equalling the total number of edges.
        returning_indices_and_original_edge_ids_are_optional: bool
            Boolean indicating whether it is okay for the call to this function
            to leave the indices and the original edge ids tensors
            uninitialized. In this case, it is the user's responsibility to
            gather them using _edge_ids_in_fused_csc_sampling_graph if either is
            missing.
        random_seed: torch.Tensor, optional
            An int64 tensor with one or two elements.

            The passed random_seed makes it so that for any seed node ``s`` and
            its neighbor ``t``, the rolled random variate ``r_t`` is the same
            for any call to this function with the same random seed. When
            sampling as part of the same batch, one would want identical seeds
            so that LABOR can globally sample. One example is that for
            heterogenous graphs, there is a single random seed passed for each
            edge type. This will sample much fewer nodes compared to having
            unique random seeds for each edge type. If one called this function
            individually for each edge type for a heterogenous graph with
            different random seeds, then it would run LABOR locally for each
            edge type, resulting into a larger number of nodes being sampled.

            If this function is called without a ``random_seed``, we get the
            random seed by getting a random number from GraphBolt. Use this
            argument with identical random_seed if multiple calls to this
            function are used to sample as part of a single batch.

            If given two numbers, then the ``seed2_contribution`` argument
            determines the interpolation between the two random seeds.
        seed2_contribution: float, optional
            A float value between [0, 1) that determines the contribution of the
            second random seed, ``random_seed[-1]``, to generate the random
            variates.
        async_op: bool
            Boolean indicating whether the call is asynchronous. If so, the
            result can be obtained by calling wait on the returned future.

        Returns
        -------
        SampledSubgraphImpl
            The sampled subgraph.

        Examples
        --------
        >>> import dgl.graphbolt as gb
        >>> import torch
        >>> ntypes = {"n1": 0, "n2": 1}
        >>> etypes = {"n1:e1:n2": 0, "n2:e2:n1": 1}
        >>> indptr = torch.LongTensor([0, 2, 4, 6, 7, 9])
        >>> indices = torch.LongTensor([2, 4, 2, 3, 0, 1, 1, 0, 1])
        >>> node_type_offset = torch.LongTensor([0, 2, 5])
        >>> type_per_edge = torch.LongTensor([1, 1, 1, 1, 0, 0, 0, 0, 0])
        >>> graph = gb.fused_csc_sampling_graph(indptr, indices,
        ...     node_type_offset=node_type_offset,
        ...     type_per_edge=type_per_edge,
        ...     node_type_to_id=ntypes,
        ...     edge_type_to_id=etypes)
        >>> nodes = {'n1': torch.LongTensor([0]), 'n2': torch.LongTensor([0])}
        >>> fanouts = torch.tensor([1, 1])
        >>> subgraph = graph.sample_layer_neighbors(nodes, fanouts)
        >>> print(subgraph.sampled_csc)
        {'n1:e1:n2': CSCFormatBase(indptr=tensor([0, 1]),
                    indices=tensor([0]),
        ), 'n2:e2:n1': CSCFormatBase(indptr=tensor([0, 1]),
                    indices=tensor([2]),
        )}
        
python/dgl/graphbolt/impl/fused_csc_sampling_graph.py