"""Views of DGLGraph.""" from __future__ import absolute_import from collections import defaultdict, namedtuple from collections.abc import MutableMapping from . import backend as F from .base import ALL, DGLError from .frame import LazyFeature NodeSpace = namedtuple("NodeSpace", ["data"]) EdgeSpace = namedtuple("EdgeSpace", ["data"]) class HeteroNodeView(object): """A NodeView class to act as G.nodes for a DGLGraph.""" __slots__ = ["_graph", "_typeid_getter"] def __init__(self, graph, typeid_getter): self._graph = graph self._typeid_getter = typeid_getter def __getitem__(self, key): if isinstance(key, slice): # slice if not ( key.start is None and key.stop is None and key.step is None ): raise DGLError('Currently only full slice ":" is supported') nodes = ALL ntype = None elif isinstance(key, tuple): nodes, ntype = key elif key is None or isinstance(key, str): nodes = ALL ntype = key else: nodes = key ntype = None ntid = self._typeid_getter(ntype) return NodeSpace( data=HeteroNodeDataView(self._graph, ntype, ntid, nodes) ) def __call__(self, ntype=None): """Return the nodes.""" ntid = self._typeid_getter(ntype) ret = F.arange( 0, self._graph._graph.num_nodes(ntid), dtype=self._graph.idtype, ctx=self._graph.device, ) return ret class HeteroNodeDataView(MutableMapping): """The data view class when G.ndata[ntype] is called.""" __slots__ = ["_graph", "_ntype", "_ntid", "_nodes"] def __init__(self, graph, ntype, ntid, nodes): self._graph = graph self._ntype = ntype self._ntid = ntid self._nodes = nodes def __getitem__(self, key): if isinstance(self._ntype, list): ret = {} for (i, ntype) in enumerate(self._ntype): value = self._graph._get_n_repr(self._ntid[i], self._nodes).get( key, None ) if value is not None: ret[ntype] = value return ret else: return self._graph._get_n_repr(self._ntid, self._nodes)[key] def __setitem__(self, key, val): if isinstance(val, LazyFeature): self._graph._node_frames[self._ntid][key] = val elif isinstance(self._ntype, list): assert isinstance(val, dict), ( "Current HeteroNodeDataView has multiple node types, " "please passing the node type and the corresponding data through a dict." ) for (ntype, data) in val.items(): ntid = self._graph.get_ntype_id(ntype) self._graph._set_n_repr(ntid, self._nodes, {key: data}) else: assert isinstance(val, dict) is False, ( "The HeteroNodeDataView has only one node type. " "please pass a tensor directly" ) self._graph._set_n_repr(self._ntid, self._nodes, {key: val}) def __delitem__(self, key): if isinstance(self._ntype, list): for ntid in self._ntid: if self._graph._get_n_repr(ntid, ALL).get(key, None) is None: continue self._graph._pop_n_repr(ntid, key) else: self._graph._pop_n_repr(self._ntid, key) def _transpose(self, as_dict=False): if isinstance(self._ntype, list): ret = defaultdict(dict) for (i, ntype) in enumerate(self._ntype): data = self._graph._get_n_repr(self._ntid[i], self._nodes) for key in self._graph._node_frames[self._ntid[i]]: ret[key][ntype] = data[key] else: ret = self._graph._get_n_repr(self._ntid, self._nodes) if as_dict: ret = { key: ret[key] for key in self._graph._node_frames[self._ntid] } return ret def __len__(self): return len(self._transpose()) def __iter__(self): return iter(self._transpose()) def keys(self): return self._transpose().keys() def values(self): return self._transpose().values() def __repr__(self): return repr(self._transpose(as_dict=True)) class HeteroEdgeView(object): """A EdgeView class to act as G.edges for a DGLGraph.""" __slots__ = ["_graph"] def __init__(self, graph): self._graph = graph def __getitem__(self, key): if isinstance(key, slice): # slice if not ( key.start is None and key.stop is None and key.step is None ): raise DGLError('Currently only full slice ":" is supported') edges = ALL etype = None elif key is None: edges = ALL etype = None elif isinstance(key, tuple): if len(key) == 3: edges = ALL etype = key else: edges = key etype = None elif isinstance(key, str): edges = ALL etype = key else: edges = key etype = None return EdgeSpace(data=HeteroEdgeDataView(self._graph, etype, edges)) def __call__(self, *args, **kwargs): """Return all the edges.""" return self._graph.all_edges(*args, **kwargs) class HeteroEdgeDataView(MutableMapping): """The data view class when G.edata[etype] is called.""" __slots__ = ["_graph", "_etype", "_etid", "_edges"] def __init__(self, graph, etype, edges): self._graph = graph self._etype = etype self._etid = ( [self._graph.get_etype_id(t) for t in etype] if isinstance(etype, list) else self._graph.get_etype_id(etype) ) self._edges = edges def __getitem__(self, key): if isinstance(self._etype, list): ret = {} for (i, etype) in enumerate(self._etype): value = self._graph._get_e_repr(self._etid[i], self._edges).get( key, None ) if value is not None: ret[etype] = value return ret else: return self._graph._get_e_repr(self._etid, self._edges)[key] def __setitem__(self, key, val): if isinstance(val, LazyFeature): self._graph._edge_frames[self._etid][key] = val elif isinstance(self._etype, list): assert isinstance(val, dict), ( "Current HeteroEdgeDataView has multiple edge types, " "please pass the edge type and the corresponding data through a dict." ) for (etype, data) in val.items(): etid = self._graph.get_etype_id(etype) self._graph._set_e_repr(etid, self._edges, {key: data}) else: assert isinstance(val, dict) is False, ( "The HeteroEdgeDataView has only one edge type. " "please pass a tensor directly" ) self._graph._set_e_repr(self._etid, self._edges, {key: val}) def __delitem__(self, key): if isinstance(self._etype, list): for etid in self._etid: if self._graph._get_e_repr(etid, ALL).get(key, None) is None: continue self._graph._pop_e_repr(etid, key) else: self._graph._pop_e_repr(self._etid, key) def _transpose(self, as_dict=False): if isinstance(self._etype, list): ret = defaultdict(dict) for (i, etype) in enumerate(self._etype): data = self._graph._get_e_repr(self._etid[i], self._edges) for key in self._graph._edge_frames[self._etid[i]]: ret[key][etype] = data[key] else: ret = self._graph._get_e_repr(self._etid, self._edges) if as_dict: ret = { key: ret[key] for key in self._graph._edge_frames[self._etid] } return ret def __len__(self): return len(self._transpose()) def __iter__(self): return iter(self._transpose()) def keys(self): return self._transpose().keys() def values(self): return self._transpose().values() def __repr__(self): return repr(self._transpose(as_dict=True))