def create_block(
    data_dict,
    num_src_nodes=None,
    num_dst_nodes=None,
    idtype=None,
    device=None,
    node_count_check=True,
):
    """Create a message flow graph (MFG) as a :class:`DGLBlock` object.

    Parameters
    ----------
    data_dict : graph data
        The dictionary data for constructing a MFG. The keys are in the form of
        string triplets (src_type, edge_type, dst_type), specifying the source node type,
        edge type, and destination node type. The values are graph data in the form of
        :math:`(U, V)`, where :math:`(U[i], V[i])` forms the edge with ID :math:`i`.
        The allowed graph data formats are:

        - ``(Tensor, Tensor)``: Each tensor must be a 1D tensor containing node IDs. DGL calls
          this format "tuple of node-tensors". The tensors should have the same data type,
          which must be either int32 or int64. They should also have the same device context
          (see below the descriptions of :attr:`idtype` and :attr:`device`).
        - ``('coo', (Tensor, Tensor))``: Same as ``(Tensor, Tensor)``.
        - ``('csr', (Tensor, Tensor, Tensor))``: The three tensors form the CSR representation
          of the graph's adjacency matrix.  The first one is the row index pointer.  The
          second one is the column indices.  The third one is the edge IDs, which can be empty
          to represent consecutive integer IDs starting from 0.
        - ``('csc', (Tensor, Tensor, Tensor))``: The three tensors form the CSC representation
          of the graph's adjacency matrix.  The first one is the column index pointer.  The
          second one is the row indices.  The third one is the edge IDs, which can be empty
          to represent consecutive integer IDs starting from 0.

        The tensors can be replaced with any iterable of integers (e.g. list, tuple,
        numpy.ndarray).

        If you would like to create a MFG with a single source node type, a single destination
        node type, and a single edge type, then you can pass in the graph data directly
        without wrapping it as a dictionary.
    num_src_nodes : dict[str, int] or int, optional
        The number of nodes for each source node type, which is a dictionary mapping a node type
        :math:`T` to the number of :math:`T`-typed source nodes.

        If not given for a node type :math:`T`, DGL finds the largest ID appearing in *every*
        graph data whose source node type is :math:`T`, and sets the number of nodes to
        be that ID plus one. If given and the value is no greater than the largest ID for some
        source node type, DGL will raise an error. By default, DGL infers the number of nodes for
        all source node types.

        If you would like to create a MFG with a single source node type, a single destination
        node type, and a single edge type, then you can pass in an integer to directly
        represent the number of source nodes.
    num_dst_nodes : dict[str, int] or int, optional
        The number of nodes for each destination node type, which is a dictionary mapping a node
        type :math:`T` to the number of :math:`T`-typed destination nodes.

        If not given for a node type :math:`T`, DGL finds the largest ID appearing in *every*
        graph data whose destination node type is :math:`T`, and sets the number of nodes to
        be that ID plus one. If given and the value is no greater than the largest ID for some
        destination node type, DGL will raise an error. By default, DGL infers the number of nodes
        for all destination node types.

        If you would like to create a MFG with a single destination node type, a single
        destination node type, and a single edge type, then you can pass in an integer to directly
        represent the number of destination nodes.
    idtype : int32 or int64, optional
        The data type for storing the structure-related graph information such as node and
        edge IDs. It should be a framework-specific data type object (e.g., ``torch.int32``).
        If ``None`` (default), DGL infers the ID type from the :attr:`data_dict` argument.
    device : device context, optional
        The device of the returned graph, which should be a framework-specific device object
        (e.g., ``torch.device``). If ``None`` (default), DGL uses the device of the tensors of
        the :attr:`data` argument. If :attr:`data` is not a tuple of node-tensors, the
        returned graph is on CPU.  If the specified :attr:`device` differs from that of the
        provided tensors, it casts the given tensors to the specified device first.
    node_count_check : bool, optional
        When num_src_nodes and num_dst_nodes are passed, whether we should perform
        sanity checks to ensure they are valid.

    Returns
    -------
    DGLBlock
        The created MFG.

    Notes
    -----
    1. If the :attr:`idtype` argument is not given then:

       - in the case of the tuple of node-tensor format, DGL uses
         the data type of the given ID tensors.
       - in the case of the tuple of sequence format, DGL uses int64.

       Once the graph has been created, you can change the data type by using
       :func:`dgl.DGLGraph.long` or :func:`dgl.DGLGraph.int`.

       If the specified :attr:`idtype` argument differs from the data type of the provided
       tensors, it casts the given tensors to the specified data type first.
    2. The most efficient construction approach is to provide a tuple of node tensors without
       specifying :attr:`idtype` and :attr:`device`. This is because the returned graph shares
       the storage with the input node-tensors in this case.
    3. DGL internally maintains multiple copies of the graph structure in different sparse
       formats and chooses the most efficient one depending on the computation invoked.
       If memory usage becomes an issue in the case of large graphs, use
       :func:`dgl.DGLGraph.formats` to restrict the allowed formats.
    4. DGL internally decides a deterministic order for the same set of node types and canonical
       edge types, which does not necessarily follow the order in :attr:`data_dict`.

    Examples
    --------

    The following example uses PyTorch backend.

    >>> import dgl
    >>> block = dgl.create_block(([0, 1, 2], [1, 2, 3]), num_src_nodes=3, num_dst_nodes=4)
    >>> block
    Block(num_src_nodes=3, num_dst_nodes=4, num_edges=3)

    >>> block = dgl.create_block({
    ...     ('A', 'AB', 'B'): ([1, 2, 3], [2, 1, 0]),
    ...     ('B', 'BA', 'A'): ([2, 1], [2, 3])},
    ...     num_src_nodes={'A': 6, 'B': 5},
    ...     num_dst_nodes={'A': 4, 'B': 3})
    >>> block
    Block(num_src_nodes={'A': 6, 'B': 5},
          num_dst_nodes={'A': 4, 'B': 3},
          num_edges={('A', 'AB', 'B'): 3, ('B', 'BA', 'A'): 2},
          metagraph=[('A', 'B', 'AB'), ('B', 'A', 'BA')])

    See also
    --------
    to_block
    """
    need_infer = num_src_nodes is None and num_dst_nodes is None
    if not isinstance(data_dict, Mapping):
        data_dict = {("_N", "_E", "_N"): data_dict}

        if not need_infer:
            assert isinstance(
                num_src_nodes, int
            ), "num_src_nodes must be a pair of integers if data_dict is not a dict"
            assert isinstance(
                num_dst_nodes, int
            ), "num_dst_nodes must be a pair of integers if data_dict is not a dict"
            num_src_nodes = {"_N": num_src_nodes}
            num_dst_nodes = {"_N": num_dst_nodes}
    else:
        if not need_infer:
            assert isinstance(
                num_src_nodes, Mapping
            ), "num_src_nodes must be a dict if data_dict is a dict"
            assert isinstance(
                num_dst_nodes, Mapping
            ), "num_dst_nodes must be a dict if data_dict is a dict"

    if need_infer:
        num_src_nodes = defaultdict(int)
        num_dst_nodes = defaultdict(int)

    # Convert all data to node tensors first
    node_tensor_dict = {}
    for (sty, ety, dty), data in data_dict.items():
        (sparse_fmt, arrays), urange, vrange = utils.graphdata2tensors(
            data,
            idtype,
            bipartite=True,
            infer_node_count=need_infer or node_count_check,
        )
        node_tensor_dict[(sty, ety, dty)] = (sparse_fmt, arrays)
        if need_infer:
            num_src_nodes[sty] = max(num_src_nodes[sty], urange)
            num_dst_nodes[dty] = max(num_dst_nodes[dty], vrange)
        elif node_count_check:  # sanity check
            if num_src_nodes[sty] < urange:
                raise DGLError(
                    "The given number of nodes of source node type {} must be larger"
                    " than the max ID in the data, but got {} and {}.".format(
                        sty, num_src_nodes[sty], urange - 1
                    )
                )
            if num_dst_nodes[dty] < vrange:
                raise DGLError(
                    "The given number of nodes of destination node type {} must be"
                    " larger than the max ID in the data, but got {} and {}.".format(
                        dty, num_dst_nodes[dty], vrange - 1
                    )
                )
    # Create the graph

    # Sort the ntypes and relation tuples to have a deterministic order for the same set
    # of type names.
    srctypes = list(sorted(num_src_nodes.keys()))
    dsttypes = list(sorted(num_dst_nodes.keys()))
    relations = list(sorted(node_tensor_dict.keys()))

    num_nodes_per_type = utils.toindex(
        [num_src_nodes[ntype] for ntype in srctypes]
        + [num_dst_nodes[ntype] for ntype in dsttypes],
        "int64",
    )
    srctype_dict = {ntype: i for i, ntype in enumerate(srctypes)}
    dsttype_dict = {
        ntype: i + len(srctypes) for i, ntype in enumerate(dsttypes)
    }

    meta_edges_src = []
    meta_edges_dst = []
    etypes = []
    rel_graphs = []
    for srctype, etype, dsttype in relations:
        meta_edges_src.append(srctype_dict[srctype])
        meta_edges_dst.append(dsttype_dict[dsttype])
        etypes.append(etype)
        sparse_fmt, arrays = node_tensor_dict[(srctype, etype, dsttype)]
        g = create_from_edges(
            sparse_fmt,
            arrays,
            "SRC/" + srctype,
            etype,
            "DST/" + dsttype,
            num_src_nodes[srctype],
            num_dst_nodes[dsttype],
        )
        rel_graphs.append(g)

    # metagraph is DGLGraph, currently still using int64 as index dtype
    metagraph = graph_index.from_coo(
        len(srctypes) + len(dsttypes), meta_edges_src, meta_edges_dst, True
    )
    # create graph index
    hgidx = heterograph_index.create_heterograph_from_relations(
        metagraph, [rgrh._graph for rgrh in rel_graphs], num_nodes_per_type
    )
    retg = DGLBlock(hgidx, (srctypes, dsttypes), etypes)

    return retg.to(device)
Create a message flow graph (MFG) as a :class:`DGLBlock` object.

    Parameters
    ----------
    data_dict : graph data
        The dictionary data for constructing a MFG. The keys are in the form of
        string triplets (src_type, edge_type, dst_type), specifying the source node type,
        edge type, and destination node type. The values are graph data in the form of
        :math:`(U, V)`, where :math:`(U[i], V[i])` forms the edge with ID :math:`i`.
        The allowed graph data formats are:

        - ``(Tensor, Tensor)``: Each tensor must be a 1D tensor containing node IDs. DGL calls
          this format "tuple of node-tensors". The tensors should have the same data type,
          which must be either int32 or int64. They should also have the same device context
          (see below the descriptions of :attr:`idtype` and :attr:`device`).
        - ``('coo', (Tensor, Tensor))``: Same as ``(Tensor, Tensor)``.
        - ``('csr', (Tensor, Tensor, Tensor))``: The three tensors form the CSR representation
          of the graph's adjacency matrix.  The first one is the row index pointer.  The
          second one is the column indices.  The third one is the edge IDs, which can be empty
          to represent consecutive integer IDs starting from 0.
        - ``('csc', (Tensor, Tensor, Tensor))``: The three tensors form the CSC representation
          of the graph's adjacency matrix.  The first one is the column index pointer.  The
          second one is the row indices.  The third one is the edge IDs, which can be empty
          to represent consecutive integer IDs starting from 0.

        The tensors can be replaced with any iterable of integers (e.g. list, tuple,
        numpy.ndarray).

        If you would like to create a MFG with a single source node type, a single destination
        node type, and a single edge type, then you can pass in the graph data directly
        without wrapping it as a dictionary.
    num_src_nodes : dict[str, int] or int, optional
        The number of nodes for each source node type, which is a dictionary mapping a node type
        :math:`T` to the number of :math:`T`-typed source nodes.

        If not given for a node type :math:`T`, DGL finds the largest ID appearing in *every*
        graph data whose source node type is :math:`T`, and sets the number of nodes to
        be that ID plus one. If given and the value is no greater than the largest ID for some
        source node type, DGL will raise an error. By default, DGL infers the number of nodes for
        all source node types.

        If you would like to create a MFG with a single source node type, a single destination
        node type, and a single edge type, then you can pass in an integer to directly
        represent the number of source nodes.
    num_dst_nodes : dict[str, int] or int, optional
        The number of nodes for each destination node type, which is a dictionary mapping a node
        type :math:`T` to the number of :math:`T`-typed destination nodes.

        If not given for a node type :math:`T`, DGL finds the largest ID appearing in *every*
        graph data whose destination node type is :math:`T`, and sets the number of nodes to
        be that ID plus one. If given and the value is no greater than the largest ID for some
        destination node type, DGL will raise an error. By default, DGL infers the number of nodes
        for all destination node types.

        If you would like to create a MFG with a single destination node type, a single
        destination node type, and a single edge type, then you can pass in an integer to directly
        represent the number of destination nodes.
    idtype : int32 or int64, optional
        The data type for storing the structure-related graph information such as node and
        edge IDs. It should be a framework-specific data type object (e.g., ``torch.int32``).
        If ``None`` (default), DGL infers the ID type from the :attr:`data_dict` argument.
    device : device context, optional
        The device of the returned graph, which should be a framework-specific device object
        (e.g., ``torch.device``). If ``None`` (default), DGL uses the device of the tensors of
        the :attr:`data` argument. If :attr:`data` is not a tuple of node-tensors, the
        returned graph is on CPU.  If the specified :attr:`device` differs from that of the
        provided tensors, it casts the given tensors to the specified device first.
    node_count_check : bool, optional
        When num_src_nodes and num_dst_nodes are passed, whether we should perform
        sanity checks to ensure they are valid.

    Returns
    -------
    DGLBlock
        The created MFG.

    Notes
    -----
    1. If the :attr:`idtype` argument is not given then:

       - in the case of the tuple of node-tensor format, DGL uses
         the data type of the given ID tensors.
       - in the case of the tuple of sequence format, DGL uses int64.

       Once the graph has been created, you can change the data type by using
       :func:`dgl.DGLGraph.long` or :func:`dgl.DGLGraph.int`.

       If the specified :attr:`idtype` argument differs from the data type of the provided
       tensors, it casts the given tensors to the specified data type first.
    2. The most efficient construction approach is to provide a tuple of node tensors without
       specifying :attr:`idtype` and :attr:`device`. This is because the returned graph shares
       the storage with the input node-tensors in this case.
    3. DGL internally maintains multiple copies of the graph structure in different sparse
       formats and chooses the most efficient one depending on the computation invoked.
       If memory usage becomes an issue in the case of large graphs, use
       :func:`dgl.DGLGraph.formats` to restrict the allowed formats.
    4. DGL internally decides a deterministic order for the same set of node types and canonical
       edge types, which does not necessarily follow the order in :attr:`data_dict`.

    Examples
    --------

    The following example uses PyTorch backend.

    >>> import dgl
    >>> block = dgl.create_block(([0, 1, 2], [1, 2, 3]), num_src_nodes=3, num_dst_nodes=4)
    >>> block
    Block(num_src_nodes=3, num_dst_nodes=4, num_edges=3)

    >>> block = dgl.create_block({
    ...     ('A', 'AB', 'B'): ([1, 2, 3], [2, 1, 0]),
    ...     ('B', 'BA', 'A'): ([2, 1], [2, 3])},
    ...     num_src_nodes={'A': 6, 'B': 5},
    ...     num_dst_nodes={'A': 4, 'B': 3})
    >>> block
    Block(num_src_nodes={'A': 6, 'B': 5},
          num_dst_nodes={'A': 4, 'B': 3},
          num_edges={('A', 'AB', 'B'): 3, ('B', 'BA', 'A'): 2},
          metagraph=[('A', 'B', 'AB'), ('B', 'A', 'BA')])

    See also
    --------
    to_block
    
python/dgl/convert.py