Files
dmlc--dgl/python/dgl/heterograph_index.py
2026-07-13 13:35:51 +08:00

1608 lines
47 KiB
Python

"""Module for heterogeneous graph index class definition."""
from __future__ import absolute_import
import itertools
import sys
import numpy as np
import scipy
from . import backend as F, utils
from ._ffi.function import _init_api
from ._ffi.object import ObjectBase, register_object
from ._ffi.streams import to_dgl_stream_handle
from .base import dgl_warning, DGLError
from .graph_index import from_coo
@register_object("graph.HeteroGraph")
class HeteroGraphIndex(ObjectBase):
"""HeteroGraph index object.
Note
----
Do not create GraphIndex directly.
"""
def __new__(cls):
obj = ObjectBase.__new__(cls)
obj._cache = {}
return obj
def __getstate__(self):
"""Issue: https://github.com/pytorch/pytorch/issues/32351
Need to set the tensor created in the __getstate__ function
as object attribute to avoid potential bugs
"""
self._pk_state = _CAPI_DGLHeteroPickle(self)
return self._pk_state
def __setstate__(self, state):
self._cache = {}
# Pickle compatibility check
# TODO: we should store a storage version number in later releases.
if isinstance(state, HeteroPickleStates):
# post-0.4.3
self.__init_handle_by_constructor__(_CAPI_DGLHeteroUnpickle, state)
elif isinstance(state, tuple) and len(state) == 3:
# pre-0.4.2
metagraph, num_nodes, edges = state
self._cache = {}
# loop over etypes and recover unit graphs
rel_graphs = []
for i, edges_per_type in enumerate(edges):
src_ntype, dst_ntype = metagraph.find_edge(i)
num_src = num_nodes[src_ntype]
num_dst = num_nodes[dst_ntype]
src_id, dst_id, _ = edges_per_type
rel_graphs.append(
create_unitgraph_from_coo(
1 if src_ntype == dst_ntype else 2,
num_src,
num_dst,
src_id,
dst_id,
["coo", "csr", " csc"],
)
)
self.__init_handle_by_constructor__(
_CAPI_DGLHeteroCreateHeteroGraph, metagraph, rel_graphs
)
@property
def metagraph(self):
"""Meta graph
Returns
-------
GraphIndex
The meta graph.
"""
return _CAPI_DGLHeteroGetMetaGraph(self)
def is_metagraph_unibipartite(self):
"""Return whether or not the graph is unibiparite."""
return _CAPI_DGLHeteroIsMetaGraphUniBipartite(self)
def number_of_ntypes(self):
"""Return number of node types."""
return self.metagraph.num_nodes()
def number_of_etypes(self):
"""Return number of edge types."""
return self.metagraph.num_edges()
def get_relation_graph(self, etype):
"""Get the unitgraph graph of the given edge/relation type.
Parameters
----------
etype : int
The edge/relation type.
Returns
-------
HeteroGraphIndex
The unitgraph graph.
"""
return _CAPI_DGLHeteroGetRelationGraph(self, int(etype))
def flatten_relations(self, etypes):
"""Convert the list of requested unitgraph graphs into a single unitgraph
graph.
Parameters
----------
etypes : list[int]
The edge/relation types.
Returns
-------
FlattenedHeteroGraph
A flattened heterograph object
"""
return _CAPI_DGLHeteroGetFlattenedGraph(self, etypes)
def add_nodes(self, ntype, num):
"""Add nodes.
Parameters
----------
ntype : int
Node type
num : int
Number of nodes to be added.
"""
_CAPI_DGLHeteroAddVertices(self, int(ntype), int(num))
self.clear_cache()
def add_edge(self, etype, u, v):
"""Add one edge.
Parameters
----------
etype : int
Edge type
u : int
The src node.
v : int
The dst node.
"""
_CAPI_DGLHeteroAddEdge(self, int(etype), int(u), int(v))
self.clear_cache()
def add_edges(self, etype, u, v):
"""Add many edges.
Parameters
----------
etype : int
Edge type
u : utils.Index
The src nodes.
v : utils.Index
The dst nodes.
"""
_CAPI_DGLHeteroAddEdges(
self, int(etype), u.todgltensor(), v.todgltensor()
)
self.clear_cache()
def clear(self):
"""Clear the graph."""
_CAPI_DGLHeteroClear(self)
self._cache.clear()
@property
def dtype(self):
"""Return the data type of this graph index.
Returns
-------
DGLDataType
The data type of the graph.
"""
return _CAPI_DGLHeteroDataType(self)
@property
def ctx(self):
"""Return the context of this graph index.
Returns
-------
DGLContext
The context of the graph.
"""
return _CAPI_DGLHeteroContext(self)
def bits_needed(self, etype):
"""Return the number of integer bits needed to represent the unitgraph graph.
Parameters
----------
etype : int
The edge type.
Returns
-------
int
The number of bits needed.
"""
stype, dtype = self.metagraph.find_edge(etype)
if (
self.num_edges(etype) >= 0x80000000
or self.num_nodes(stype) >= 0x80000000
or self.num_nodes(dtype) >= 0x80000000
):
return 64
else:
return 32
def asbits(self, bits):
"""Transform the graph to a new one with the given number of bits storage.
NOTE: this method only works for immutable graph index
Parameters
----------
bits : int
The number of integer bits (32 or 64)
Returns
-------
HeteroGraphIndex
The graph index stored using the given number of bits.
"""
return _CAPI_DGLHeteroAsNumBits(self, int(bits))
def copy_to(self, ctx):
"""Copy this immutable graph index to the given device context.
NOTE: this method only works for immutable graph index
Parameters
----------
ctx : DGLContext
The target device context.
Returns
-------
HeteroGraphIndex
The graph index on the given device context.
"""
return _CAPI_DGLHeteroCopyTo(self, ctx.device_type, ctx.device_id)
def pin_memory(self):
"""Copies the graph structure to pinned memory, if it's not already
pinned.
NOTE: This function is similar to PyTorch's Tensor.pin_memory(), but
tailored for graphs. It utilizes the same pin_memory allocator as
PyTorch, so the lifecycle of the graph is also managed by PyTorch.
If a batch includes a DGL graph object (HeteroGraphIndex),
PyTorch's DataLoader memory pinning logic will detect it and
automatically activate this function when pin_memory=True.
Returns
-------
HeteroGraphIndex
The pinned graph index.
"""
return _CAPI_DGLHeteroPinMemory(self)
def pin_memory_(self):
"""Pin this graph to the page-locked memory.
NOTE: This is an inplace method to pin the current graph index, i.e.,
it does not require new memory allocation but simply flags the
existing graph structure to be page-locked. The graph structure
must be on CPU to be pinned. If the graph struture is already
pinned, the function directly returns it.
Returns
-------
HeteroGraphIndex
The pinned graph index.
"""
return _CAPI_DGLHeteroPinMemory_(self)
def unpin_memory_(self):
"""Unpin this graph from the page-locked memory.
NOTE: this is an inplace method.
If the graph struture is not pinned, e.g., on CPU or GPU,
the function directly returns it.
Returns
-------
HeteroGraphIndex
The unpinned graph index.
"""
return _CAPI_DGLHeteroUnpinMemory_(self)
def is_pinned(self):
"""Check if this graph is pinned to the page-locked memory.
Returns
-------
bool
True if the graph is pinned.
"""
return bool(_CAPI_DGLHeteroIsPinned(self))
def record_stream(self, stream):
"""Record the stream that is using this graph.
Parameters
----------
stream : torch.cuda.Stream
The stream that is using this graph.
Returns
-------
HeteroGraphIndex
self.
"""
return _CAPI_DGLHeteroRecordStream(self, to_dgl_stream_handle(stream))
def shared_memory(
self, name, ntypes=None, etypes=None, formats=("coo", "csr", "csc")
):
"""Return a copy of this graph in shared memory
Parameters
----------
name : str
The name of the shared memory.
ntypes : list of str
Name of node types
etypes : list of str
Name of edge types
format : list of str
Desired formats to be materialized.
Returns
-------
HeteroGraphIndex
The graph index in shared memory
"""
assert len(name) > 0, "The name of shared memory cannot be empty"
assert len(formats) > 0
for fmt in formats:
assert fmt in ("coo", "csr", "csc")
ntypes = [] if ntypes is None else ntypes
etypes = [] if etypes is None else etypes
return _CAPI_DGLHeteroCopyToSharedMem(
self, name, ntypes, etypes, formats
)
def is_multigraph(self):
"""Return whether the graph is a multigraph
The time cost will be O(E)
Returns
-------
bool
True if it is a multigraph, False otherwise.
"""
return bool(_CAPI_DGLHeteroIsMultigraph(self))
def is_readonly(self):
"""Return whether the graph index is read-only.
Returns
-------
bool
True if it is a read-only graph, False otherwise.
"""
return bool(_CAPI_DGLHeteroIsReadonly(self))
def num_nodes(self, ntype):
"""Return the number of nodes.
Parameters
----------
ntype : int
Node type.
Returns
-------
int
The number of nodes.
"""
return _CAPI_DGLHeteroNumVertices(self, int(ntype))
def num_edges(self, etype):
"""Return the number of edges.
Parameters
----------
etype : int
Edge type.
Returns
-------
int
The number of edges.
"""
return _CAPI_DGLHeteroNumEdges(self, int(etype))
# TODO(#5485): remove this method.
def number_of_nodes(self, ntype):
"""Return the number of nodes.
Parameters
----------
ntype : int
Node type
Returns
-------
int
The number of nodes
"""
return _CAPI_DGLHeteroNumVertices(self, int(ntype))
# TODO(#5485): remove this method.
def number_of_edges(self, etype):
"""Return the number of edges.
Parameters
----------
etype : int
Edge type
Returns
-------
int
The number of edges
"""
return _CAPI_DGLHeteroNumEdges(self, int(etype))
def has_nodes(self, ntype, vids):
"""Return true if the nodes exist.
Parameters
----------
ntype : int
Node type
vid : Tensor
Node IDs
Returns
-------
Tensor
0-1 array indicating existence
"""
return F.from_dgl_nd(
_CAPI_DGLHeteroHasVertices(self, int(ntype), F.to_dgl_nd(vids))
)
def has_edges_between(self, etype, u, v):
"""Return true if the edge exists.
Parameters
----------
etype : int
Edge type
u : Tensor
Src node Ids.
v : Tensor
Dst node Ids.
Returns
-------
Tensor
0-1 array indicating existence
"""
return F.from_dgl_nd(
_CAPI_DGLHeteroHasEdgesBetween(
self, int(etype), F.to_dgl_nd(u), F.to_dgl_nd(v)
)
)
def predecessors(self, etype, v):
"""Return the predecessors of the node.
Assume that node_type(v) == dst_type(etype). Thus, the ntype argument is omitted.
Parameters
----------
etype : int
Edge type
v : int
The node.
Returns
-------
Tensor
Array of predecessors
"""
return F.from_dgl_nd(
_CAPI_DGLHeteroPredecessors(self, int(etype), int(v))
)
def successors(self, etype, v):
"""Return the successors of the node.
Assume that node_type(v) == src_type(etype). Thus, the ntype argument is omitted.
Parameters
----------
etype : int
Edge type
v : int
The node.
Returns
-------
Tensor
Array of successors
"""
return F.from_dgl_nd(
_CAPI_DGLHeteroSuccessors(self, int(etype), int(v))
)
def edge_ids_all(self, etype, u, v):
"""Return a triplet of arrays that contains the edge IDs.
Parameters
----------
etype : int
Edge type
u : Tensor
The src nodes.
v : Tensor
The dst nodes.
Returns
-------
Tensor
The src nodes.
Tensor
The dst nodes.
Tensor
The edge ids.
"""
edge_array = _CAPI_DGLHeteroEdgeIdsAll(
self, int(etype), F.to_dgl_nd(u), F.to_dgl_nd(v)
)
src = F.from_dgl_nd(edge_array(0))
dst = F.from_dgl_nd(edge_array(1))
eid = F.from_dgl_nd(edge_array(2))
return src, dst, eid
def edge_ids_one(self, etype, u, v):
"""Return an arrays of edge IDs.
Parameters
----------
etype : int
Edge type
u : Tensor
The src nodes.
v : Tensor
The dst nodes.
Returns
-------
Tensor
The edge ids.
"""
eid = F.from_dgl_nd(
_CAPI_DGLHeteroEdgeIdsOne(
self, int(etype), F.to_dgl_nd(u), F.to_dgl_nd(v)
)
)
return eid
def find_edges(self, etype, eid):
"""Return a triplet of arrays that contains the edge IDs.
Parameters
----------
etype : int
Edge type
eid : Tensor
Edge ids.
Returns
-------
Tensor
The src nodes.
Tensor
The dst nodes.
Tensor
The edge ids.
"""
edge_array = _CAPI_DGLHeteroFindEdges(
self, int(etype), F.to_dgl_nd(eid)
)
src = F.from_dgl_nd(edge_array(0))
dst = F.from_dgl_nd(edge_array(1))
eid = F.from_dgl_nd(edge_array(2))
return src, dst, eid
def in_edges(self, etype, v):
"""Return the in edges of the node(s).
Assume that node_type(v) == dst_type(etype). Thus, the ntype argument is omitted.
Parameters
----------
etype : int
Edge type
v : Tensor
Node IDs.
Returns
-------
Tensor
The src nodes.
Tensor
The dst nodes.
Tensor
The edge ids.
"""
edge_array = _CAPI_DGLHeteroInEdges_2(self, int(etype), F.to_dgl_nd(v))
src = F.from_dgl_nd(edge_array(0))
dst = F.from_dgl_nd(edge_array(1))
eid = F.from_dgl_nd(edge_array(2))
return src, dst, eid
def out_edges(self, etype, v):
"""Return the out edges of the node(s).
Assume that node_type(v) == src_type(etype). Thus, the ntype argument is omitted.
Parameters
----------
etype : int
Edge type
v : Tensor
Node IDs.
Returns
-------
Tensor
The src nodes.
Tensor
The dst nodes.
Tensor
The edge ids.
"""
edge_array = _CAPI_DGLHeteroOutEdges_2(self, int(etype), F.to_dgl_nd(v))
src = F.from_dgl_nd(edge_array(0))
dst = F.from_dgl_nd(edge_array(1))
eid = F.from_dgl_nd(edge_array(2))
return src, dst, eid
def edges(self, etype, order=None):
"""Return all the edges
Parameters
----------
etype : int
Edge type
order : string
The order of the returned edges. Currently support:
- 'srcdst' : sorted by their src and dst ids.
- 'eid' : sorted by edge Ids.
- None : the arbitrary order.
Returns
-------
Tensor
The src nodes.
Tensor
The dst nodes.
Tensor
The edge ids.
"""
if order is None:
order = ""
elif order not in ["srcdst", "eid"]:
raise DGLError(
"Expect order to be one of None, 'srcdst', 'eid', "
"got {}".format(order)
)
edge_array = _CAPI_DGLHeteroEdges(self, int(etype), order)
src = F.from_dgl_nd(edge_array(0))
dst = F.from_dgl_nd(edge_array(1))
eid = F.from_dgl_nd(edge_array(2))
return src, dst, eid
def in_degrees(self, etype, v):
"""Return the in degrees of the nodes.
Assume that node_type(v) == dst_type(etype). Thus, the ntype argument is omitted.
Parameters
----------
etype : int
Edge type
v : Tensor
The nodes.
Returns
-------
Tensor
The in degree array.
"""
return F.from_dgl_nd(
_CAPI_DGLHeteroInDegrees(self, int(etype), F.to_dgl_nd(v))
)
def out_degrees(self, etype, v):
"""Return the out degrees of the nodes.
Assume that node_type(v) == src_type(etype). Thus, the ntype argument is omitted.
Parameters
----------
etype : int
Edge type
v : Tensor
The nodes.
Returns
-------
Tensor
The out degree array.
"""
return F.from_dgl_nd(
_CAPI_DGLHeteroOutDegrees(self, int(etype), F.to_dgl_nd(v))
)
def adjacency_matrix(self, etype, transpose, ctx):
"""Return the adjacency matrix representation of this graph.
By default, a row of returned adjacency matrix represents the source
of an edge and the column represents the destination.
When transpose is True, a row represents the destination and a column represents
the source.
Parameters
----------
etype : int
Edge type
transpose : bool
A flag to transpose the returned adjacency matrix.
ctx : context
The context of the returned matrix.
Returns
-------
SparseTensor
The adjacency matrix.
Tensor
A index for data shuffling due to sparse format change. Return None
if shuffle is not required.
"""
if not isinstance(transpose, bool):
raise DGLError(
'Expect bool value for "transpose" arg,'
" but got %s." % (type(transpose))
)
fmt = F.get_preferred_sparse_format()
rst = _CAPI_DGLHeteroGetAdj(self, int(etype), transpose, fmt)
# convert to framework-specific sparse matrix
srctype, dsttype = self.metagraph.find_edge(etype)
nrows = (
self.num_nodes(dsttype) if transpose else self.num_nodes(srctype)
)
ncols = (
self.num_nodes(srctype) if transpose else self.num_nodes(dsttype)
)
nnz = self.num_edges(etype)
if fmt == "csr":
indptr = F.copy_to(F.from_dgl_nd(rst(0)), ctx)
indices = F.copy_to(F.from_dgl_nd(rst(1)), ctx)
shuffle = F.copy_to(F.from_dgl_nd(rst(2)), ctx)
dat = F.ones(
nnz, dtype=F.float32, ctx=ctx
) # FIXME(minjie): data type
spmat = F.sparse_matrix(
dat, ("csr", indices, indptr), (nrows, ncols)
)[0]
return spmat, shuffle
elif fmt == "coo":
idx = F.copy_to(F.from_dgl_nd(rst(0)), ctx)
idx = F.reshape(idx, (2, nnz))
dat = F.ones((nnz,), dtype=F.float32, ctx=ctx)
adj, shuffle_idx = F.sparse_matrix(
dat, ("coo", idx), (nrows, ncols)
)
return adj, shuffle_idx
else:
raise Exception("unknown format")
def adjacency_matrix_tensors(self, etype, transpose, fmt):
"""Return the adjacency matrix as a triplet of tensors.
By default, a row of returned adjacency matrix represents the source
of an edge and the column represents the destination.
When transpose is True, a row represents the destination and a column represents
the source.
Parameters
----------
etype : int
Edge type
transpose : bool
A flag to transpose the returned adjacency matrix.
fmt : str
Indicates the format of returned adjacency matrix.
Returns
-------
tuple[int, int, Tensor, Tensor] or tuple[int, int, Tensor, Tensor, Tensor]
The number of rows and columns, followed by the adjacency matrix tensors
whose data type and device are the same as those of the graph.
If :attr:`fmt` is ``'coo'``, then the triplet will be
the row array and column array of the COO representation.
If :attr:`fmt` is ``'csr'``, then the triplet will be
the index pointer array (``indptr``), indices array, and data array
of the CSR representation. The data array will contain the edge ID for
each entry of the adjacency matrix. If the data array is empty, then it is
equivalent to a consecutive array from zero to the number of edges minus one.
"""
if not isinstance(transpose, bool):
raise DGLError(
'Expect bool value for "transpose" arg,'
" but got %s." % (type(transpose))
)
rst = _CAPI_DGLHeteroGetAdj(self, int(etype), transpose, fmt)
srctype, dsttype = self.metagraph.find_edge(etype)
nrows = (
self.num_nodes(dsttype) if transpose else self.num_nodes(srctype)
)
ncols = (
self.num_nodes(srctype) if transpose else self.num_nodes(dsttype)
)
nnz = self.num_edges(etype)
if fmt == "csr":
indptr = F.from_dgl_nd(rst(0))
indices = F.from_dgl_nd(rst(1))
data = F.from_dgl_nd(rst(2))
return nrows, ncols, indptr, indices, data
elif fmt == "coo":
idx = F.from_dgl_nd(rst(0))
row, col = F.reshape(idx, (2, nnz))
return nrows, ncols, row, col
else:
raise ValueError("unknown format")
def adjacency_matrix_scipy(
self, etype, transpose, fmt, return_edge_ids=None
):
"""Return the scipy adjacency matrix representation of this graph.
By default, a row of returned adjacency matrix represents the destination
of an edge and the column represents the source.
When transpose is True, a row represents the source and a column represents
a destination.
Parameters
----------
etype : int
Edge type
transpose : bool
A flag to transpose the returned adjacency matrix.
fmt : str
Indicates the format of returned adjacency matrix.
return_edge_ids : bool
Indicates whether to return edge IDs or 1 as elements.
Returns
-------
scipy.sparse.spmatrix
The scipy representation of adjacency matrix.
"""
if return_edge_ids is None:
dgl_warning(
"Adjacency matrix by default currently returns edge IDs."
" As a result there is one 0 entry which is not eliminated."
" In the next release it will return 1s by default,"
" and 0 will be eliminated otherwise.",
FutureWarning,
)
return_edge_ids = True
if fmt == "csr":
nrows, ncols, indptr, indices, data = self.adjacency_matrix_tensors(
etype, transpose, fmt
)
indptr = F.asnumpy(indptr)
indices = F.asnumpy(indices)
data = F.asnumpy(data)
# Check if edge ID is omitted
if return_edge_ids and data.shape[0] == 0:
data = np.arange(self.num_edges(etype))
else:
data = np.ones_like(indices)
return scipy.sparse.csr_matrix(
(data, indices, indptr), shape=(nrows, ncols)
)
elif fmt == "coo":
nrows, ncols, row, col = self.adjacency_matrix_tensors(
etype, transpose, fmt
)
row = F.asnumpy(row)
col = F.asnumpy(col)
data = (
np.arange(self.num_edges(etype))
if return_edge_ids
else np.ones_like(row)
)
return scipy.sparse.coo_matrix(
(data, (row, col)), shape=(nrows, ncols)
)
else:
raise ValueError("unknown format")
def incidence_matrix(self, etype, typestr, ctx):
"""Return the incidence matrix representation of this graph.
An incidence matrix is an n x m sparse matrix, where n is
the number of nodes and m is the number of edges. Each nnz
value indicating whether the edge is incident to the node
or not.
There are three types of an incidence matrix `I`:
* "in":
- I[v, e] = 1 if e is the in-edge of v (or v is the dst node of e);
- I[v, e] = 0 otherwise.
* "out":
- I[v, e] = 1 if e is the out-edge of v (or v is the src node of e);
- I[v, e] = 0 otherwise.
* "both":
- I[v, e] = 1 if e is the in-edge of v;
- I[v, e] = -1 if e is the out-edge of v;
- I[v, e] = 0 otherwise (including self-loop).
Parameters
----------
etype : int
Edge type
typestr : str
Can be either "in", "out" or "both"
ctx : context
The context of returned incidence matrix.
Returns
-------
SparseTensor
The incidence matrix.
utils.Index
A index for data shuffling due to sparse format change. Return None
if shuffle is not required.
"""
src, dst, eid = self.edges(etype)
srctype, dsttype = self.metagraph.find_edge(etype)
m = self.num_edges(etype)
if typestr == "in":
n = self.num_nodes(dsttype)
row = F.unsqueeze(dst, 0)
col = F.unsqueeze(eid, 0)
idx = F.copy_to(F.cat([row, col], dim=0), ctx)
# FIXME(minjie): data type
dat = F.ones((m,), dtype=F.float32, ctx=ctx)
inc, shuffle_idx = F.sparse_matrix(dat, ("coo", idx), (n, m))
elif typestr == "out":
n = self.num_nodes(srctype)
row = F.unsqueeze(src, 0)
col = F.unsqueeze(eid, 0)
idx = F.copy_to(F.cat([row, col], dim=0), ctx)
# FIXME(minjie): data type
dat = F.ones((m,), dtype=F.float32, ctx=ctx)
inc, shuffle_idx = F.sparse_matrix(dat, ("coo", idx), (n, m))
elif typestr == "both":
assert (
srctype == dsttype
), "'both' is supported only if source and destination type are the same"
n = self.num_nodes(srctype)
# first remove entries for self loops
mask = F.logical_not(F.equal(src, dst))
src = F.boolean_mask(src, mask)
dst = F.boolean_mask(dst, mask)
eid = F.boolean_mask(eid, mask)
n_entries = F.shape(src)[0]
# create index
row = F.unsqueeze(F.cat([src, dst], dim=0), 0)
col = F.unsqueeze(F.cat([eid, eid], dim=0), 0)
idx = F.copy_to(F.cat([row, col], dim=0), ctx)
# FIXME(minjie): data type
x = -F.ones((n_entries,), dtype=F.float32, ctx=ctx)
y = F.ones((n_entries,), dtype=F.float32, ctx=ctx)
dat = F.cat([x, y], dim=0)
inc, shuffle_idx = F.sparse_matrix(dat, ("coo", idx), (n, m))
else:
raise DGLError("Invalid incidence matrix type: %s" % str(typestr))
return inc, shuffle_idx
def node_subgraph(self, induced_nodes):
"""Return the induced node subgraph.
Parameters
----------
induced_nodes : list of utils.Index
Induced nodes. The length should be equal to the number of
node types in this heterograph.
Returns
-------
SubgraphIndex
The subgraph index.
"""
vids = [F.to_dgl_nd(nodes) for nodes in induced_nodes]
return _CAPI_DGLHeteroVertexSubgraph(self, vids)
def edge_subgraph(self, induced_edges, preserve_nodes):
"""Return the induced edge subgraph.
Parameters
----------
induced_edges : list of utils.Index
Induced edges. The length should be equal to the number of
edge types in this heterograph.
preserve_nodes : bool
Indicates whether to preserve all nodes or not.
If true, keep the nodes which have no edge connected in the subgraph;
If false, all nodes without edge connected to it would be removed.
Returns
-------
SubgraphIndex
The subgraph index.
"""
eids = [F.to_dgl_nd(edges) for edges in induced_edges]
return _CAPI_DGLHeteroEdgeSubgraph(self, eids, preserve_nodes)
def get_unitgraph(self, etype, ctx):
"""Create a unitgraph graph from given edge type and copy to the given device
context.
Note: this internal function is for DGL scheduler use only
Parameters
----------
etype : int
If the graph index is a Bipartite graph index, this argument must be None.
Otherwise, it represents the edge type.
ctx : DGLContext
The context of the returned graph.
Returns
-------
HeteroGraphIndex
"""
g = self.get_relation_graph(etype)
return g.copy_to(ctx).asbits(self.bits_needed(etype or 0))
def get_csr_shuffle_order(self, etype):
"""Return the edge shuffling order when a coo graph is converted to csr format
Parameters
----------
etype : int
The edge type
Returns
-------
tuple of two utils.Index
The first element of the tuple is the shuffle order for outward graph
The second element of the tuple is the shuffle order for inward graph
"""
csr = _CAPI_DGLHeteroGetAdj(self, int(etype), False, "csr")
order = csr(2)
rev_csr = _CAPI_DGLHeteroGetAdj(self, int(etype), True, "csr")
rev_order = rev_csr(2)
return utils.toindex(order, self.dtype), utils.toindex(
rev_order, self.dtype
)
def formats(self, formats=None):
"""Get a graph index with the specified allowed sparse format(s) or
query for the usage status of sparse formats.
If the graph has multiple edge types, they will have the same
sparse format.
When ``formats`` is not None, if the intersection between `formats` and
the current graph's created sparse format(s) is not empty, the returned
cloned graph only retains all sparse format(s) in the intersection. If
the intersection is empty, a sparse format will be selected to be
created following the order of ``'coo' -> 'csr' -> 'csc'``.
Parameters
----------
formats : str or list of str or None
* If formats is None, return the usage status of sparse formats
* Otherwise, it can be ``'coo'``/``'csr'``/``'csc'`` or a sublist of
them, specifying the sparse formats to use.
Returns
-------
dict or GraphIndex
* If formats is None, the result will be a dict recording the usage
status of sparse formats.
* Otherwise, a GraphIndex will be returned, which is a clone of the
original graph with the specified allowed sparse format(s)
``formats``.
"""
formats_allowed = _CAPI_DGLHeteroGetAllowedFormats(self)
formats_created = _CAPI_DGLHeteroGetCreatedFormats(self)
created = []
not_created = []
if formats is None:
for fmt in ["coo", "csr", "csc"]:
if fmt in formats_allowed:
if fmt in formats_created:
created.append(fmt)
else:
not_created.append(fmt)
return {"created": created, "not created": not_created}
else:
if isinstance(formats, str):
formats = [formats]
return _CAPI_DGLHeteroGetFormatGraph(self, formats)
def create_formats_(self):
"""Create all sparse matrices allowed for the graph."""
return _CAPI_DGLHeteroCreateFormat(self)
def reverse(self):
"""Reverse the heterogeneous graph adjacency
The node types and edge types are not changed.
Returns
-------
A new graph index.
"""
return _CAPI_DGLHeteroReverse(self)
@register_object("graph.HeteroSubgraph")
class HeteroSubgraphIndex(ObjectBase):
"""Hetero-subgraph data structure"""
@property
def graph(self):
"""The subgraph structure
Returns
-------
HeteroGraphIndex
The subgraph
"""
return _CAPI_DGLHeteroSubgraphGetGraph(self)
@property
def induced_nodes(self):
"""Induced nodes for each node type. The return list
length should be equal to the number of node types.
Returns
-------
list of utils.Index
Induced nodes
"""
ret = _CAPI_DGLHeteroSubgraphGetInducedVertices(self)
return [F.from_dgl_nd(v) for v in ret]
@property
def induced_edges(self):
"""Induced edges for each edge type. The return list
length should be equal to the number of edge types.
Returns
-------
list of utils.Index
Induced edges
"""
ret = _CAPI_DGLHeteroSubgraphGetInducedEdges(self)
return [F.from_dgl_nd(v) for v in ret]
#################################################################
# Creators
#################################################################
def create_metagraph_index(ntypes, canonical_etypes):
"""Return a GraphIndex instance for a metagraph given the node types and canonical
edge types.
This function will reorder the node types and canonical edge types.
Parameters
----------
ntypes : Iterable[str]
The node types.
canonical_etypes : Iterable[tuple[str, str, str]]
The canonical edge types.
Returns
-------
GraphIndex
The index object for metagraph.
list[str]
The reordered node types for each node in the metagraph.
list[str]
The reordered edge types for each edge in the metagraph.
list[tuple[str, str, str]]
The reordered canonical edge types for each edge in the metagraph.
"""
# Sort the ntypes and relation tuples to have a deterministic order for the same set
# of type names.
ntypes = list(sorted(ntypes))
relations = list(sorted(canonical_etypes))
ntype_dict = {ntype: i for i, ntype in enumerate(ntypes)}
meta_edges_src = []
meta_edges_dst = []
etypes = []
for srctype, etype, dsttype in relations:
meta_edges_src.append(ntype_dict[srctype])
meta_edges_dst.append(ntype_dict[dsttype])
etypes.append(etype)
# metagraph is DGLGraph, currently still using int64 as index dtype
metagraph = from_coo(len(ntypes), meta_edges_src, meta_edges_dst, True)
return metagraph, ntypes, etypes, relations
def create_unitgraph_from_coo(
num_ntypes,
num_src,
num_dst,
row,
col,
formats,
row_sorted=False,
col_sorted=False,
):
"""Create a unitgraph graph index from COO format
Parameters
----------
num_ntypes : int
Number of node types (must be 1 or 2).
num_src : int
Number of nodes in the src type.
num_dst : int
Number of nodes in the dst type.
row : utils.Index
Row index.
col : utils.Index
Col index.
formats : list of str.
Restrict the storage formats allowed for the unit graph.
row_sorted : bool, optional
Whether or not the rows of the COO are in ascending order.
col_sorted : bool, optional
Whether or not the columns of the COO are in ascending order within
each row. This only has an effect when ``row_sorted`` is True.
Returns
-------
HeteroGraphIndex
"""
if isinstance(formats, str):
formats = [formats]
return _CAPI_DGLHeteroCreateUnitGraphFromCOO(
int(num_ntypes),
int(num_src),
int(num_dst),
F.to_dgl_nd(row),
F.to_dgl_nd(col),
formats,
row_sorted,
col_sorted,
)
def create_unitgraph_from_csr(
num_ntypes,
num_src,
num_dst,
indptr,
indices,
edge_ids,
formats,
transpose=False,
):
"""Create a unitgraph graph index from CSR format
Parameters
----------
num_ntypes : int
Number of node types (must be 1 or 2).
num_src : int
Number of nodes in the src type.
num_dst : int
Number of nodes in the dst type.
indptr : utils.Index
CSR indptr.
indices : utils.Index
CSR indices.
edge_ids : utils.Index
Edge shuffle id.
formats : str
Restrict the storage formats allowed for the unit graph.
transpose : bool, optional
If True, treats the input matrix as CSC.
Returns
-------
HeteroGraphIndex
"""
if isinstance(formats, str):
formats = [formats]
return _CAPI_DGLHeteroCreateUnitGraphFromCSR(
int(num_ntypes),
int(num_src),
int(num_dst),
F.to_dgl_nd(indptr),
F.to_dgl_nd(indices),
F.to_dgl_nd(edge_ids),
formats,
transpose,
)
def create_heterograph_from_relations(
metagraph, rel_graphs, num_nodes_per_type
):
"""Create a heterograph from metagraph and graphs of every relation.
Parameters
----------
metagraph : GraphIndex
Meta-graph.
rel_graphs : list of HeteroGraphIndex
Bipartite graph of each relation.
num_nodes_per_type : utils.Index, optional
Number of nodes per node type
Returns
-------
HeteroGraphIndex
"""
if num_nodes_per_type is None:
return _CAPI_DGLHeteroCreateHeteroGraph(metagraph, rel_graphs)
else:
return _CAPI_DGLHeteroCreateHeteroGraphWithNumNodes(
metagraph, rel_graphs, num_nodes_per_type.todgltensor()
)
def create_heterograph_from_shared_memory(name):
"""Create a heterograph from shared memory with the given name.
Paramaters
----------
name : str
The name of the share memory
Returns
-------
HeteroGraphIndex (in shared memory)
ntypes : list of str
Names of node types
etypes : list of str
Names of edge types
"""
g, ntypes, etypes = _CAPI_DGLHeteroCreateFromSharedMem(name)
return g, list(ntypes), list(etypes)
def joint_union(metagraph, gidx_list):
"""Return a joint union of the input heterographs.
Parameters
----------
metagraph : GraphIndex
Meta-graph.
gidx_list : list of HeteroGraphIndex
Heterographs to be joint_unioned.
Returns
-------
HeteroGraphIndex
joint_unioned Heterograph.
"""
return _CAPI_DGLHeteroJointUnion(metagraph, gidx_list)
def disjoint_union(metagraph, graphs):
"""Return a disjoint union of the input heterographs.
Parameters
----------
metagraph : GraphIndex
Meta-graph.
graphs : list of HeteroGraphIndex
Heterographs to be batched.
Returns
-------
HeteroGraphIndex
Batched Heterograph.
"""
return _CAPI_DGLHeteroDisjointUnion_v2(metagraph, graphs)
def disjoint_partition(graph, bnn_all_types, bne_all_types):
"""Partition the graph disjointly.
Parameters
----------
graph : HeteroGraphIndex
The graph to be partitioned.
bnn_all_types : list of list of int
bnn_all_types[t] gives the number of nodes with t-th type in the batch.
bne_all_types : list of list of int
bne_all_types[t] gives the number of edges with t-th type in the batch.
Returns
--------
list of HeteroGraphIndex
Heterographs unbatched.
"""
bnn_all_types = utils.toindex(
list(itertools.chain.from_iterable(bnn_all_types))
)
bne_all_types = utils.toindex(
list(itertools.chain.from_iterable(bne_all_types))
)
return _CAPI_DGLHeteroDisjointPartitionBySizes_v2(
graph, bnn_all_types.todgltensor(), bne_all_types.todgltensor()
)
def slice_gidx(graph, num_nodes, start_nid, num_edges, start_eid):
"""Slice a chunk of the graph.
Parameters
----------
graph : HeteroGraphIndex
The batched graph to slice.
num_nodes : utils.Index
Number of nodes per node type in the result graph.
start_nid : utils.Index
Start node ID per node type in the result graph.
num_edges : utils.Index
Number of edges per edge type in the result graph.
start_eid : utils.Index
Start edge ID per edge type in the result graph.
Returns
-------
HeteroGraphIndex
The sliced graph.
"""
return _CAPI_DGLHeteroSlice(
graph,
num_nodes.todgltensor(),
start_nid.todgltensor(),
num_edges.todgltensor(),
start_eid.todgltensor(),
)
#################################################################
# Data structure used by C APIs
#################################################################
@register_object("graph.FlattenedHeteroGraph")
class FlattenedHeteroGraph(ObjectBase):
"""FlattenedHeteroGraph object class in C++ backend."""
@register_object("graph.HeteroPickleStates")
class HeteroPickleStates(ObjectBase):
"""Pickle states object class in C++ backend."""
@property
def version(self):
"""Version number
Returns
-------
int
version number
"""
return _CAPI_DGLHeteroPickleStatesGetVersion(self)
@property
def meta(self):
"""Meta info
Returns
-------
bytearray
Serialized meta info
"""
return bytearray(_CAPI_DGLHeteroPickleStatesGetMeta(self))
@property
def arrays(self):
"""Arrays representing the graph structure (COO or CSR)
Returns
-------
list of dgl.ndarray.NDArray
Arrays
"""
num_arr = _CAPI_DGLHeteroPickleStatesGetArraysNum(self)
arr_func = _CAPI_DGLHeteroPickleStatesGetArrays(self)
return [arr_func(i) for i in range(num_arr)]
def __getstate__(self):
"""Issue: https://github.com/pytorch/pytorch/issues/32351
Need to set the tensor created in the __getstate__ function
as object attribute to avoid potential bugs
"""
self._pk_arrays = [
F.zerocopy_from_dgl_ndarray(arr) for arr in self.arrays
]
return self.version, self.meta, self._pk_arrays
def __setstate__(self, state):
if isinstance(state[0], int):
version, meta, arrays = state
arrays = [F.zerocopy_to_dgl_ndarray(arr) for arr in arrays]
self.__init_handle_by_constructor__(
_CAPI_DGLCreateHeteroPickleStates, version, meta, arrays
)
else:
metagraph, num_nodes_per_type, adjs = state
num_nodes_per_type = F.zerocopy_to_dgl_ndarray(num_nodes_per_type)
self.__init_handle_by_constructor__(
_CAPI_DGLCreateHeteroPickleStatesOld,
metagraph,
num_nodes_per_type,
adjs,
)
def _forking_rebuild(pk_state):
version, meta, arrays = pk_state
arrays = [F.to_dgl_nd(arr) for arr in arrays]
states = _CAPI_DGLCreateHeteroPickleStates(version, meta, arrays)
graph_index = _CAPI_DGLHeteroForkingUnpickle(states)
graph_index._forking_pk_state = pk_state
return graph_index
def _forking_reduce(graph_index):
# Because F.from_dgl_nd(F.to_dgl_nd(x)) loses the information of shared memory
# file descriptor (because DLPack does not keep it), without caching the tensors
# PyTorch will allocate one shared memory region for every single worker.
# The downside is that if a graph_index is shared by forking and new formats are created
# afterwards, then sharing it again will not bring together the new formats. This case
# should be rare though because (1) DataLoader will create all the formats if num_workers > 0
# anyway, and (2) we require the users to explicitly create all formats before calling
# mp.spawn().
if hasattr(graph_index, "_forking_pk_state"):
return _forking_rebuild, (graph_index._forking_pk_state,)
states = _CAPI_DGLHeteroForkingPickle(graph_index)
arrays = [F.from_dgl_nd(arr) for arr in states.arrays]
# Similar to what being mentioned in HeteroGraphIndex.__getstate__, we need to save
# the tensors as an attribute of the original graph index object. Otherwise
# PyTorch will throw weird errors like bad value(s) in fds_to_keep or unable to
# resize file.
graph_index._forking_pk_state = (states.version, states.meta, arrays)
return _forking_rebuild, (graph_index._forking_pk_state,)
if not (F.get_preferred_backend() == "mxnet" and sys.version_info.minor <= 6):
# Python 3.6 MXNet crashes with the following statement; remove until we no longer support
# 3.6 (which is EOL anyway).
from multiprocessing.reduction import ForkingPickler
ForkingPickler.register(HeteroGraphIndex, _forking_reduce)
_init_api("dgl.heterograph_index")