diff --git a/.github/workflows/analyze.yml b/.github/workflows/analyze.yml index 25ddf32..c4c5db7 100644 --- a/.github/workflows/analyze.yml +++ b/.github/workflows/analyze.yml @@ -37,7 +37,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@v4 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 64c7ee5..bc3f9ff 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,9 +1,8 @@ name: build on: workflow_dispatch: - push: - branches: [ master ] pull_request: + push: branches: [ master ] env: PACKAGE_DIR: adbdgl_adapter @@ -13,34 +12,47 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python: ["3.7", "3.8", "3.9"] + python: ["3.8", "3.9", "3.10", "3.11"] name: Python ${{ matrix.python }} steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + - name: Setup Python ${{ matrix.python }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python }} + cache: 'pip' + cache-dependency-path: setup.py + - name: Set up ArangoDB Instance via Docker - run: docker create --name adb -p 8529:8529 -e ARANGO_ROOT_PASSWORD= arangodb/arangodb:3.9.1 + run: docker create --name adb -p 8529:8529 -e ARANGO_ROOT_PASSWORD= arangodb/arangodb + - name: Start ArangoDB Instance run: docker start adb + - name: Setup pip run: python -m pip install --upgrade pip setuptools wheel + - name: Install packages run: pip install .[dev] + - name: Run black run: black --check --verbose --diff --color ${{env.PACKAGE_DIR}} ${{env.TESTS_DIR}} + - name: Run flake8 run: flake8 ${{env.PACKAGE_DIR}} ${{env.TESTS_DIR}} + - name: Run isort run: isort --check --profile=black ${{env.PACKAGE_DIR}} ${{env.TESTS_DIR}} + - name: Run mypy run: mypy ${{env.PACKAGE_DIR}} ${{env.TESTS_DIR}} + - name: Run pytest run: pytest --cov=${{env.PACKAGE_DIR}} --cov-report xml --cov-report term-missing -v --color=yes --no-cov-on-fail --code-highlight=yes + - name: Publish to coveralls.io - if: matrix.python == '3.8' + if: matrix.python == '3.10' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: coveralls --service=github \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index eb8b8a2..ac040fc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,76 +3,34 @@ on: workflow_dispatch: release: types: [published] -env: - PACKAGE_DIR: adbdgl_adapter - TESTS_DIR: tests jobs: - build: - runs-on: ubuntu-latest - strategy: - matrix: - python: ["3.7", "3.8", "3.9"] - name: Python ${{ matrix.python }} - steps: - - uses: actions/checkout@v2 - - name: Setup Python ${{ matrix.python }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python }} - - name: Set up ArangoDB Instance via Docker - run: docker create --name adb -p 8529:8529 -e ARANGO_ROOT_PASSWORD= arangodb/arangodb:3.9.1 - - name: Start ArangoDB Instance - run: docker start adb - - name: Setup pip - run: python -m pip install --upgrade pip setuptools wheel - - name: Install packages - run: pip install .[dev] - - name: Run black - run: black --check --verbose --diff --color ${{env.PACKAGE_DIR}} ${{env.TESTS_DIR}} - - name: Run flake8 - run: flake8 ${{env.PACKAGE_DIR}} ${{env.TESTS_DIR}} - - name: Run isort - run: isort --check --profile=black ${{env.PACKAGE_DIR}} ${{env.TESTS_DIR}} - - name: Run mypy - run: mypy ${{env.PACKAGE_DIR}} ${{env.TESTS_DIR}} - - name: Run pytest - run: pytest --cov=${{env.PACKAGE_DIR}} --cov-report xml --cov-report term-missing -v --color=yes --no-cov-on-fail --code-highlight=yes - - name: Publish to coveralls.io - if: matrix.python == '3.8' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: coveralls --service=github - release: - needs: build runs-on: ubuntu-latest name: Release package steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Fetch complete history for all tags and branches run: git fetch --prune --unshallow - - name: Setup python - uses: actions/setup-python@v2 + - name: Setup Python + uses: actions/setup-python@v4 with: - python-version: "3.8" + python-version: "3.10" - name: Install release packages run: pip install setuptools wheel twine setuptools-scm[toml] - - name: Install dependencies - run: pip install .[dev] - - name: Build distribution run: python setup.py sdist bdist_wheel - - name: Publish to PyPI Test + - name: Publish to Test PyPi env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD_TEST }} run: twine upload --repository testpypi dist/* #--skip-existing - - name: Publish to PyPI + + - name: Publish to PyPi env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} @@ -83,7 +41,7 @@ jobs: runs-on: ubuntu-latest name: Update Changelog steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 with: fetch-depth: 0 @@ -95,10 +53,10 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Setup python - uses: actions/setup-python@v2 + - name: Setup Python + uses: actions/setup-python@v4 with: - python-version: "3.8" + python-version: "3.10" - name: Install release packages run: pip install wheel gitchangelog pystache @@ -110,12 +68,12 @@ jobs: run: gitchangelog ${{env.VERSION}} > CHANGELOG.md - name: Make commit for auto-generated changelog - uses: EndBug/add-and-commit@v7 + uses: EndBug/add-and-commit@v9 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: add: "CHANGELOG.md" - branch: actions/changelog + new_branch: actions/changelog message: "!gitchangelog" - name: Create pull request for the auto generated changelog @@ -128,4 +86,4 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Alert developer of open PR - run: echo "Changelog $PR_URL is ready to be merged by developer." \ No newline at end of file + run: echo "Changelog $PR_URL is ready to be merged by developer." diff --git a/README.md b/README.md index 3c782ce..301b4c8 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ [![License](https://img.shields.io/github/license/arangoml/dgl-adapter?color=9E2165&style=for-the-badge)](https://github.com/arangoml/dgl-adapter/blob/master/LICENSE) [![Code style: black](https://img.shields.io/static/v1?style=for-the-badge&label=code%20style&message=black&color=black)](https://github.com/psf/black) -[![Downloads](https://img.shields.io/badge/dynamic/json?style=for-the-badge&color=282661&label=Downloads&query=total_downloads&url=https://api.pepy.tech/api/projects/adbdgl-adapter)](https://pepy.tech/project/adbdgl-adapter) +[![Downloads](https://img.shields.io/badge/dynamic/json?style=for-the-badge&color=282661&label=Downloads&query=total_downloads&url=https://api.pepy.tech/api/v2/projects/adbdgl-adapter)](https://pepy.tech/project/adbdgl-adapter) ![](https://raw.githubusercontent.com/arangoml/dgl-adapter/master/examples/assets/adb_logo.png) @@ -18,6 +18,7 @@ The ArangoDB-DGL Adapter exports Graphs from ArangoDB, the multi-model database for graph & beyond, into Deep Graph Library (DGL), a python package for graph neural networks, and vice-versa. +Note: The ArangoDB-DGL Adapter currently only supports the use of PyTorch as the [DGL backend](https://docs.dgl.ai/en/0.8.x/install/#backends). Support for MXNet and Tensorflow will be added in the future. ## About DGL @@ -45,44 +46,217 @@ pip install git+https://github.com/arangoml/dgl-adapter.git Also available as an ArangoDB Lunch & Learn session: [Graph & Beyond Course #2.8](https://www.arangodb.com/resources/lunch-sessions/graph-beyond-lunch-break-2-8-dgl-adapter/) ```py -from arango import ArangoClient # Python-Arango driver -from dgl.data import KarateClubDataset # Sample graph from DGL +import dgl +import torch +import pandas -from adbdgl_adapter import ADBDGL_Adapter +from arango import ArangoClient +from adbdgl_adapter import ADBDGL_Adapter, ADBDGL_Controller +from adbdgl_adapter.encoders import IdentityEncoder, CategoricalEncoder -# Let's assume that the ArangoDB "fraud detection" dataset is imported to this endpoint -db = ArangoClient(hosts="http://localhost:8529").db("_system", username="root", password="") +# Connect to ArangoDB +db = ArangoClient().db() +# Instantiate the adapter adbdgl_adapter = ADBDGL_Adapter(db) -# Use Case 1.1: ArangoDB to DGL via Graph name -dgl_fraud_graph = adbdgl_adapter.arangodb_graph_to_dgl("fraud-detection") +# Create a DGL Heterogeneous Graph +fake_hetero = dgl.heterograph({ + ("user", "follows", "user"): (torch.tensor([0, 1]), torch.tensor([1, 2])), + ("user", "follows", "topic"): (torch.tensor([1, 1]), torch.tensor([1, 2])), + ("user", "plays", "game"): (torch.tensor([0, 3]), torch.tensor([3, 4])), +}) +fake_hetero.nodes["user"].data["features"] = torch.tensor([21, 44, 16, 25]) +fake_hetero.nodes["user"].data["label"] = torch.tensor([1, 2, 0, 1]) +fake_hetero.nodes["game"].data["features"] = torch.tensor([[0, 0], [0, 1], [1, 0], [1, 1], [1, 1]]) +fake_hetero.edges[("user", "plays", "game")].data["features"] = torch.tensor([[6, 1], [1000, 0]]) +``` + +### DGL to ArangoDB +```py +############################ +# 1.1: without a Metagraph # +############################ + +adb_g = adbdgl_adapter.dgl_to_arangodb("FakeHetero", fake_hetero) -# Use Case 1.2: ArangoDB to DGL via Collection names -dgl_fraud_graph_2 = adbdgl_adapter.arangodb_collections_to_dgl( - "fraud-detection", - {"account", "Class", "customer"}, # Vertex collections - {"accountHolder", "Relationship", "transaction"}, # Edge collections -) +######################### +# 1.2: with a Metagraph # +######################### -# Use Case 1.3: ArangoDB to DGL via Metagraph +# Specifying a Metagraph provides customized adapter behaviour metagraph = { + "nodeTypes": { + "user": { + "features": "user_age", # 1) you can specify a string value for attribute renaming + "label": label_tensor_to_2_column_dataframe, # 2) you can specify a function for user-defined handling, as long as the function returns a Pandas DataFrame + }, + # 3) You can specify set of strings if you want to preserve the same DGL attribute names for the node/edge type + "game": {"features"} # this is equivalent to {"features": "features"} + }, + "edgeTypes": { + ("user", "plays", "game"): { + # 4) you can specify a list of strings for tensor dissasembly (if you know the number of node/edge features in advance) + "features": ["hours_played", "is_satisfied_with_game"] + }, + }, +} + +def label_tensor_to_2_column_dataframe(dgl_tensor: torch.Tensor, adb_df: pandas.DataFrame) -> pandas.DataFrame: + """A user-defined function to create two + ArangoDB attributes out of the 'user' label tensor + + :param dgl_tensor: The DGL Tensor containing the data + :type dgl_tensor: torch.Tensor + :param adb_df: The ArangoDB DataFrame to populate, whose + size is preset to the length of **dgl_tensor**. + :type adb_df: pandas.DataFrame + :return: The populated ArangoDB DataFrame + :rtype: pandas.DataFrame + """ + label_map = {0: "Class A", 1: "Class B", 2: "Class C"} + + adb_df["label_num"] = dgl_tensor.tolist() + adb_df["label_str"] = adb_df["label_num"].map(label_map) + + return adb_df + + +adb_g = adbdgl_adapter.dgl_to_arangodb("FakeHetero", fake_hetero, metagraph, explicit_metagraph=False) + +####################################################### +# 1.3: with a Metagraph and `explicit_metagraph=True` # +####################################################### + +# With `explicit_metagraph=True`, the node & edge types omitted from the metagraph will NOT be converted to ArangoDB. +adb_g = adbdgl_adapter.dgl_to_arangodb("FakeHetero", fake_hetero, metagraph, explicit_metagraph=True) + +######################################## +# 1.4: with a custom ADBDGL Controller # +######################################## + +class Custom_ADBDGL_Controller(ADBDGL_Controller): + def _prepare_dgl_node(self, dgl_node: dict, node_type: str) -> dict: + """Optionally modify a DGL node object before it gets inserted into its designated ArangoDB collection. + + :param dgl_node: The DGL node object to (optionally) modify. + :param node_type: The DGL Node Type of the node. + :return: The DGL Node object + """ + dgl_node["foo"] = "bar" + return dgl_node + + def _prepare_dgl_edge(self, dgl_edge: dict, edge_type: tuple) -> dict: + """Optionally modify a DGL edge object before it gets inserted into its designated ArangoDB collection. + + :param dgl_edge: The DGL edge object to (optionally) modify. + :param edge_type: The Edge Type of the DGL edge. Formatted + as (from_collection, edge_collection, to_collection) + :return: The DGL Edge object + """ + dgl_edge["bar"] = "foo" + return dgl_edge + + +adb_g = ADBDGL_Adapter(db, Custom_ADBDGL_Controller()).dgl_to_arangodb("FakeHetero", fake_hetero) +``` + +### ArangoDB to DGL +```py +# Start from scratch! +db.delete_graph("FakeHetero", drop_collections=True, ignore_missing=True) +adbdgl_adapter.dgl_to_arangodb("FakeHetero", fake_hetero) + +####################### +# 2.1: via Graph name # +####################### + +# Due to risk of ambiguity, this method does not transfer attributes +dgl_g = adbdgl_adapter.arangodb_graph_to_dgl("FakeHetero") + +############################# +# 2.2: via Collection names # +############################# + +# Due to risk of ambiguity, this method does not transfer attributes +dgl_g = adbdgl_adapter.arangodb_collections_to_dgl("FakeHetero", v_cols={"user", "game"}, e_cols={"plays"}) + +###################### +# 2.3: via Metagraph # +###################### + +# Transfers attributes "as is", meaning they are already formatted to DGL data standards. +# Learn more about the DGL Data Standards here: https://docs.dgl.ai/guide/graph.html#guide-graph +metagraph_v1 = { + "vertexCollections": { + # Move the "features" & "label" ArangoDB attributes to DGL as "features" & "label" Tensors + "user": {"features", "label"}, # equivalent to {"features": "features", "label": "label"} + "game": {"dgl_game_features": "features"}, + "topic": {}, + }, + "edgeCollections": { + "plays": {"dgl_plays_features": "features"}, + "follows": {} + }, +} + +dgl_g = adbdgl_adapter.arangodb_to_dgl("FakeHetero", metagraph_v1) + +################################################# +# 2.4: via Metagraph with user-defined encoders # +################################################# + +# Transforms attributes via user-defined encoders +metagraph_v2 = { + "vertexCollections": { + "Movies": { + "features": { # Build a feature matrix from the "Action" & "Drama" document attributes + "Action": IdentityEncoder(dtype=torch.long), + "Drama": IdentityEncoder(dtype=torch.long), + }, + "label": "Comedy", + }, + "Users": { + "features": { + "Gender": CategoricalEncoder(), # CategoricalEncoder(mapping={"M": 0, "F": 1}), + "Age": IdentityEncoder(dtype=torch.long), + } + }, + }, + "edgeCollections": {"Ratings": {"weight": "Rating"}}, +} + +dgl_g = adbdgl_adapter.arangodb_to_dgl("imdb", metagraph_v2) + +################################################## +# 2.5: via Metagraph with user-defined functions # +################################################## + +# Transforms attributes via user-defined functions +metagraph_v3 = { "vertexCollections": { - "account": {"Balance", "rank"}, - "customer": {"rank"}, - "Class": {}, + "user": { + "features": udf_user_features, # supports named functions + "label": lambda df: torch.tensor(df["label"].to_list()), # also supports lambda functions + }, + "game": {"features": udf_game_features}, }, "edgeCollections": { - "transaction": {"transaction_amt", "sender_bank_id", "receiver_bank_id"}, - "accountHolder": {}, - "Relationship": {}, + "plays": {"features": (lambda df: torch.tensor(df["features"].to_list()))}, }, } -dgl_fraud_graph_3 = adbdgl_adapter.arangodb_to_dgl("fraud-detection", metagraph) -# Use Case 2: DGL to ArangoDB -dgl_karate_graph = KarateClubDataset()[0] -adb_karate_graph = adbdgl_adapter.dgl_to_arangodb("Karate", dgl_karate_graph) +def udf_user_features(user_df: pandas.DataFrame) -> torch.Tensor: + # user_df["features"] = ... + return torch.tensor(user_df["features"].to_list()) + + +def udf_game_features(game_df: pandas.DataFrame) -> torch.Tensor: + # game_df["features"] = ... + return torch.tensor(game_df["features"].to_list()) + + +dgl_g = adbdgl_adapter.arangodb_to_dgl("FakeHetero", metagraph_v3) ``` ## Development & Testing diff --git a/adbdgl_adapter/abc.py b/adbdgl_adapter/abc.py index 51c8117..12d1746 100644 --- a/adbdgl_adapter/abc.py +++ b/adbdgl_adapter/abc.py @@ -2,14 +2,12 @@ # -*- coding: utf-8 -*- from abc import ABC -from typing import Any, List, Set, Union +from typing import Any, Set, Union from arango.graph import Graph as ArangoDBGraph -from dgl import DGLGraph -from dgl.heterograph import DGLHeteroGraph -from torch import Tensor +from dgl import DGLGraph, DGLHeteroGraph -from .typings import ArangoMetagraph, DGLCanonicalEType, Json +from .typings import ADBMetagraph, DGLCanonicalEType, DGLMetagraph, Json class Abstract_ADBDGL_Adapter(ABC): @@ -17,55 +15,35 @@ def __init__(self) -> None: raise NotImplementedError # pragma: no cover def arangodb_to_dgl( - self, name: str, metagraph: ArangoMetagraph, **query_options: Any + self, name: str, metagraph: ADBMetagraph, **adb_export_kwargs: Any ) -> DGLHeteroGraph: raise NotImplementedError # pragma: no cover def arangodb_collections_to_dgl( - self, name: str, v_cols: Set[str], e_cols: Set[str], **query_options: Any + self, name: str, v_cols: Set[str], e_cols: Set[str], **adb_export_kwargs: Any ) -> DGLHeteroGraph: raise NotImplementedError # pragma: no cover - def arangodb_graph_to_dgl(self, name: str, **query_options: Any) -> DGLHeteroGraph: + def arangodb_graph_to_dgl( + self, name: str, **adb_export_kwargs: Any + ) -> DGLHeteroGraph: raise NotImplementedError # pragma: no cover def dgl_to_arangodb( self, name: str, dgl_g: Union[DGLGraph, DGLHeteroGraph], + metagraph: DGLMetagraph = {}, + explicit_metagraph: bool = True, overwrite_graph: bool = False, - **import_options: Any, + **adb_import_kwargs: Any, ) -> ArangoDBGraph: raise NotImplementedError # pragma: no cover - def etypes_to_edefinitions( - self, canonical_etypes: List[DGLCanonicalEType] - ) -> List[Json]: - raise NotImplementedError # pragma: no cover - - def __prepare_dgl_features(self) -> None: - raise NotImplementedError # pragma: no cover - - def __insert_dgl_features(self) -> None: - raise NotImplementedError # pragma: no cover - - def __prepare_adb_attributes(self) -> None: - raise NotImplementedError # pragma: no cover - - def __fetch_adb_docs(self) -> None: - raise NotImplementedError # pragma: no cover - - def __insert_adb_docs(self) -> None: - raise NotImplementedError # pragma: no cover - - @property - def DEFAULT_CANONICAL_ETYPE(self) -> List[DGLCanonicalEType]: - return [("_N", "_E", "_N")] - class Abstract_ADBDGL_Controller(ABC): - def _adb_attribute_to_dgl_feature(self, key: str, col: str, val: Any) -> Any: + def _prepare_dgl_node(self, dgl_node: Json, node_type: str) -> Json: raise NotImplementedError # pragma: no cover - def _dgl_feature_to_adb_attribute(self, key: str, col: str, val: Tensor) -> Any: + def _prepare_dgl_edge(self, dgl_edge: Json, edge_type: DGLCanonicalEType) -> Json: raise NotImplementedError # pragma: no cover diff --git a/adbdgl_adapter/adapter.py b/adbdgl_adapter/adapter.py index 2e3dfb8..71a3092 100644 --- a/adbdgl_adapter/adapter.py +++ b/adbdgl_adapter/adapter.py @@ -2,20 +2,43 @@ # -*- coding: utf-8 -*- import logging from collections import defaultdict -from typing import Any, DefaultDict, Dict, List, Optional, Set, Union +from math import ceil +from typing import Any, Callable, DefaultDict, Dict, List, Optional, Set, Tuple, Union from arango.cursor import Cursor -from arango.database import Database +from arango.database import StandardDatabase from arango.graph import Graph as ADBGraph -from arango.result import Result -from dgl import DGLGraph, DGLHeteroGraph, heterograph -from dgl.view import HeteroEdgeDataView, HeteroNodeDataView -from torch import Tensor, tensor +from dgl import DGLGraph, DGLHeteroGraph, graph, heterograph +from dgl.view import EdgeSpace, HeteroEdgeDataView, HeteroNodeDataView, NodeSpace +from pandas import DataFrame, Series +from rich.console import Group +from rich.live import Live +from rich.progress import Progress +from torch import Tensor, cat, tensor from .abc import Abstract_ADBDGL_Adapter from .controller import ADBDGL_Controller -from .typings import ArangoMetagraph, DGLCanonicalEType, DGLDataDict, Json -from .utils import logger +from .exceptions import ADBMetagraphError, DGLMetagraphError +from .typings import ( + ADBMap, + ADBMetagraph, + ADBMetagraphValues, + DGLCanonicalEType, + DGLData, + DGLDataDict, + DGLDataTypes, + DGLMetagraph, + DGLMetagraphValues, + Json, +) +from .utils import ( + get_bar_progress, + get_export_spinner_progress, + get_import_spinner_progress, + logger, + validate_adb_metagraph, + validate_dgl_metagraph, +) class ADBDGL_Adapter(Abstract_ADBDGL_Adapter): @@ -36,27 +59,28 @@ class ADBDGL_Adapter(Abstract_ADBDGL_Adapter): def __init__( self, - db: Database, + db: StandardDatabase, controller: ADBDGL_Controller = ADBDGL_Controller(), logging_lvl: Union[str, int] = logging.INFO, ): self.set_logging(logging_lvl) - if issubclass(type(db), Database) is False: - msg = "**db** parameter must inherit from arango.database.Database" + if not isinstance(db, StandardDatabase): + msg = "**db** parameter must inherit from arango.database.StandardDatabase" raise TypeError(msg) - if issubclass(type(controller), ADBDGL_Controller) is False: + if not isinstance(controller, ADBDGL_Controller): msg = "**controller** parameter must inherit from ADBDGL_Controller" raise TypeError(msg) self.__db = db - self.__cntrl: ADBDGL_Controller = controller + self.__async_db = db.begin_async_execution(return_result=False) + self.__cntrl = controller logger.info(f"Instantiated ADBDGL_Adapter with database '{db.name}'") @property - def db(self) -> Database: + def db(self) -> StandardDatabase: return self.__db # pragma: no cover @property @@ -66,99 +90,239 @@ def cntrl(self) -> ADBDGL_Controller: def set_logging(self, level: Union[int, str]) -> None: logger.setLevel(level) + ########################### + # Public: ArangoDB -> DGL # + ########################### + def arangodb_to_dgl( - self, name: str, metagraph: ArangoMetagraph, **query_options: Any - ) -> DGLHeteroGraph: - """Create a DGLHeteroGraph from the user-defined metagraph. + self, name: str, metagraph: ADBMetagraph, **adb_export_kwargs: Any + ) -> Union[DGLGraph, DGLHeteroGraph]: + """Create a DGL graph from an ArangoDB Metagraph. Carries + over node/edge data via the **metagraph**. :param name: The DGL graph name. :type name: str :param metagraph: An object defining vertex & edge collections to import - to DGL, along with their associated attributes to keep. - :type metagraph: adbdgl_adapter.typings.ArangoMetagraph - :param query_options: Keyword arguments to specify AQL query options when + to DGL, along with collection-level specifications to indicate + which ArangoDB attributes will become DGL features/labels. + + The current supported **metagraph** values are: + 1) Set[str]: The set of DGL-ready ArangoDB attributes to store + in your DGL graph. + + 2) Dict[str, str]: The DGL property name mapped to the ArangoDB + attribute name that stores your DGL ready data. + + 3) Dict[str, Dict[str, None | Callable]]: + The DGL property name mapped to a dictionary, which maps your + ArangoDB attribute names to a callable Python Class + (i.e has a `__call__` function defined), or to None + (if the ArangoDB attribute is already a list of numerics). + NOTE: The `__call__` function must take as input a Pandas DataFrame, + and must return a PyTorch Tensor. + + 4) Dict[str, Callable[[pandas.DataFrame], torch.Tensor]]: + The DGL property name mapped to a user-defined function + for custom behaviour. NOTE: The function must take as input + a Pandas DataFrame, and must return a PyTorch Tensor. + + See below for examples of **metagraph**. + :type metagraph: adbdgl_adapter.typings.ADBMetagraph + :param adb_export_kwargs: Keyword arguments to specify AQL query options when fetching documents from the ArangoDB instance. Full parameter list: https://docs.python-arango.com/en/main/specs.html#arango.aql.AQL.execute - :type query_options: Any - :return: A DGL Heterograph - :rtype: dgl.heterograph.DGLHeteroGraph - :raise ValueError: If missing required keys in metagraph + :type adb_export_kwargs: Any + :return: A DGL Homogeneous or Heterogeneous graph object + :rtype: dgl.DGLGraph | dgl.DGLHeteroGraph + :raise adbdgl_adapter.exceptions.ADBMetagraphError: If invalid metagraph. - Here is an example entry for parameter **metagraph**: + **metagraph** examples + 1) .. code-block:: python { "vertexCollections": { - "account": {"Balance", "account_type", "customer_id", "rank"}, - "bank": {"Country", "Id", "bank_id", "bank_name"}, - "customer": {"Name", "Sex", "Ssn", "rank"}, + "v0": {'x', 'y'}, # equivalent to {'x': 'x', 'y': 'y'} + "v1": {'x'}, + "v2": {'x'}, }, "edgeCollections": { - "accountHolder": {}, - "transaction": { - "transaction_amt", "receiver_bank_id", "sender_bank_id" + "e0": {'edge_attr'}, + "e1": {'edge_weight'}, + }, + } + + The metagraph above specifies that each document + within the "v0" ArangoDB collection has a "pre-built" feature matrix + named "x", and also has a node label named "y". + We map these keys to the "x" and "y" properties of the DGL graph. + + 2) + .. code-block:: python + { + "vertexCollections": { + "v0": {'x': 'v0_features', 'y': 'label'}, + "v1": {'x': 'v1_features'}, + "v2": {'x': 'v2_features'}, + }, + "edgeCollections": { + "e0": {'edge_attr': 'e0_features'}, + "e1": {'edge_weight': 'edge_weight'}, + }, + } + + The metagraph above specifies that each document + within the "v0" ArangoDB collection has a "pre-built" feature matrix + named "v0_features", and also has a node label named "label". + We map these keys to the "x" and "y" properties of the DGL graph. + + 3) + .. code-block:: python + from adbdgl_adapter.encoders import IdentityEncoder, CategoricalEncoder + + { + "vertexCollections": { + "Movies": { + "x": { + "Action": IdentityEncoder(dtype=torch.long), + "Drama": IdentityEncoder(dtype=torch.long), + 'Misc': None + }, + "y": "Comedy", + }, + "Users": { + "x": { + "Gender": CategoricalEncoder(), + "Age": IdentityEncoder(dtype=torch.long), + } + }, + }, + "edgeCollections": { + "Ratings": { "edge_weight": "Rating" } + }, + } + + The metagraph above will build the "Movies" feature matrix 'x' + using the ArangoDB 'Action', 'Drama' & 'misc' attributes, by relying on + the user-specified Encoders (see adbdgl_adapter.encoders for examples). + NOTE: If the mapped value is `None`, then it assumes that the ArangoDB attribute + value is a list containing numerical values only. + + 4) + .. code-block:: python + def udf_v0_x(v0_df): + # process v0_df here to return v0 "x" feature matrix + # ... + return torch.tensor(v0_df["x"].to_list()) + + def udf_v1_x(v1_df): + # process v1_df here to return v1 "x" feature matrix + # ... + return torch.tensor(v1_df["x"].to_list()) + + { + "vertexCollections": { + "v0": { + "x": udf_v0_x, # named functions + "y": (lambda df: tensor(df["y"].to_list())), # lambda functions }, + "v1": {"x": udf_v1_x}, + "v2": {"x": (lambda df: tensor(df["x"].to_list()))}, + }, + "edgeCollections": { + "e0": {"edge_attr": (lambda df: tensor(df["edge_attr"].to_list()))}, }, } + + The metagraph above provides an interface for a user-defined function to + build a DGL-ready Tensor from a DataFrame equivalent to the + associated ArangoDB collection. """ logger.debug(f"--arangodb_to_dgl('{name}')--") - # Maps ArangoDB vertex IDs to DGL node IDs - adb_map: Dict[str, Json] = dict() + validate_adb_metagraph(metagraph) + + # Maps ArangoDB Vertex _keys to DGL Node ids + adb_map: ADBMap = defaultdict(dict) - # Dictionaries for constructing a heterogeneous graph. + # The data for constructing a graph, + # which takes the form of (U, V). + # (U[i], V[i]) forms the edge with ID i in the graph. data_dict: DGLDataDict = dict() - ndata: DefaultDict[str, DefaultDict[str, List[Any]]] - ndata = defaultdict(lambda: defaultdict(list)) + # The node data view for storing node features + ndata: DGLData = defaultdict(lambda: defaultdict(Tensor)) - edata: DefaultDict[str, DefaultDict[str, List[Any]]] - edata = defaultdict(lambda: defaultdict(list)) + # The edge data view for storing edge features + edata: DGLData = defaultdict(lambda: defaultdict(Tensor)) - adb_v: Json - for v_col, atribs in metagraph["vertexCollections"].items(): - logger.debug(f"Preparing '{v_col}' vertices") - for i, adb_v in enumerate(self.__fetch_adb_docs(v_col, query_options)): - adb_id = adb_v["_id"] - logger.debug(f"V{i}: {adb_id}") + v_cols: List[str] = list(metagraph["vertexCollections"].keys()) - adb_map[adb_id] = {"id": i, "col": v_col} - self.__prepare_dgl_features(ndata, atribs, adb_v, v_col) + ###################### + # Vertex Collections # + ###################### - adb_e: Json - edge_dict: DefaultDict[DGLCanonicalEType, DefaultDict[str, List[Any]]] - for e_col, atribs in metagraph["edgeCollections"].items(): - logger.debug(f"Preparing '{e_col}' edges") + for v_col, meta in metagraph["vertexCollections"].items(): + logger.debug(f"Preparing '{v_col}' vertices") - edge_dict = defaultdict(lambda: defaultdict(list)) + # 1. Fetch ArangoDB vertices + v_col_cursor, v_col_size = self.__fetch_adb_docs( + v_col, meta, **adb_export_kwargs + ) - for i, adb_e in enumerate(self.__fetch_adb_docs(e_col, query_options)): - logger.debug(f'E{i}: {adb_e["_id"]}') + # 2. Process ArangoDB vertices + self.__process_adb_cursor( + "#319BF5", + v_col_cursor, + v_col_size, + self.__process_adb_vertex_df, + v_col, + adb_map, + meta, + ndata=ndata, + ) - from_node = adb_map[adb_e["_from"]] - to_node = adb_map[adb_e["_to"]] - edge_type = (from_node["col"], e_col, to_node["col"]) + #################### + # Edge Collections # + #################### - edge_data = edge_dict[edge_type] - edge_data["from_nodes"].append(from_node["id"]) - edge_data["to_nodes"].append(to_node["id"]) + # The set of skipped edge types + edge_type_blacklist: Set[DGLCanonicalEType] = set() - self.__prepare_dgl_features(edata, atribs, adb_e, edge_type) + for e_col, meta in metagraph["edgeCollections"].items(): + logger.debug(f"Preparing '{e_col}' edges") - for edge_type, edges in edge_dict.items(): - logger.debug(f"Inserting {edge_type} edges") - data_dict[edge_type] = ( - tensor(edges["from_nodes"]), - tensor(edges["to_nodes"]), - ) + # 1. Fetch ArangoDB edges + e_col_cursor, e_col_size = self.__fetch_adb_docs( + e_col, meta, **adb_export_kwargs + ) - dgl_g: DGLHeteroGraph = heterograph(data_dict) - has_one_ntype = len(dgl_g.ntypes) == 1 - has_one_etype = len(dgl_g.etypes) == 1 - logger.debug(f"Is graph '{name}' homogenous? {has_one_ntype and has_one_etype}") + # 2. Process ArangoDB edges + self.__process_adb_cursor( + "#FCFDFC", + e_col_cursor, + e_col_size, + self.__process_adb_edge_df, + e_col, + adb_map, + meta, + edata=edata, + data_dict=data_dict, + v_cols=v_cols, + edge_type_blacklist=edge_type_blacklist, + ) + + if not data_dict: # pragma: no cover + msg = f""" + Can't create the DGL graph: no complete edge types found. + The following edge types were skipped due to missing + vertex collection specifications: {edge_type_blacklist} + """ + raise ValueError(msg) - self.__insert_dgl_features(ndata, dgl_g.ndata, has_one_ntype) - self.__insert_dgl_features(edata, dgl_g.edata, has_one_etype) + dgl_g = self.__create_dgl_graph(data_dict, adb_map, metagraph) + self.__link_dgl_data(dgl_g.ndata, ndata, len(dgl_g.ntypes) == 1) + self.__link_dgl_data(dgl_g.edata, edata, len(dgl_g.canonical_etypes) == 1) logger.info(f"Created DGL '{name}' Graph") return dgl_g @@ -168,55 +332,70 @@ def arangodb_collections_to_dgl( name: str, v_cols: Set[str], e_cols: Set[str], - **query_options: Any, - ) -> DGLHeteroGraph: - """Create a DGL graph from ArangoDB collections. + **adb_export_kwargs: Any, + ) -> Union[DGLGraph, DGLHeteroGraph]: + """Create a DGL graph from ArangoDB collections. Due to risk of + ambiguity, this method DOES NOT transfer ArangoDB attributes to DGL. :param name: The DGL graph name. :type name: str - :param v_cols: A set of ArangoDB vertex collections to - import to DGL. + :param v_cols: The set of ArangoDB vertex collections to import to DGL. :type v_cols: Set[str] - :param e_cols: A set of ArangoDB edge collections to import to DGL. + :param e_cols: The set of ArangoDB edge collections to import to DGL. :type e_cols: Set[str] - :param query_options: Keyword arguments to specify AQL query options when + :param adb_export_kwargs: Keyword arguments to specify AQL query options when fetching documents from the ArangoDB instance. Full parameter list: https://docs.python-arango.com/en/main/specs.html#arango.aql.AQL.execute - :type query_options: Any - :return: A DGL Heterograph - :rtype: dgl.heterograph.DGLHeteroGraph + :type adb_export_kwargs: Any + :return: A DGL Homogeneous or Heterogeneous graph object + :rtype: dgl.DGLGraph | dgl.DGLHeteroGraph + :raise adbdgl_adapter.exceptions.ADBMetagraphError: If invalid metagraph. """ - metagraph: ArangoMetagraph = { - "vertexCollections": {col: set() for col in v_cols}, - "edgeCollections": {col: set() for col in e_cols}, + metagraph: ADBMetagraph = { + "vertexCollections": {col: dict() for col in v_cols}, + "edgeCollections": {col: dict() for col in e_cols}, } - return self.arangodb_to_dgl(name, metagraph, **query_options) + return self.arangodb_to_dgl(name, metagraph, **adb_export_kwargs) - def arangodb_graph_to_dgl(self, name: str, **query_options: Any) -> DGLHeteroGraph: + def arangodb_graph_to_dgl( + self, name: str, **adb_export_kwargs: Any + ) -> Union[DGLGraph, DGLHeteroGraph]: """Create a DGL graph from an ArangoDB graph. :param name: The ArangoDB graph name. :type name: str - :param query_options: Keyword arguments to specify AQL query options when + :param adb_export_kwargs: Keyword arguments to specify AQL query options when fetching documents from the ArangoDB instance. Full parameter list: https://docs.python-arango.com/en/main/specs.html#arango.aql.AQL.execute - :type query_options: Any - :return: A DGL Heterograph - :rtype: dgl.heterograph.DGLHeteroGraph + :type adb_export_kwargs: Any + :return: A DGL Homogeneous or Heterogeneous graph object + :rtype: dgl.DGLGraph | dgl.DGLHeteroGraph + :raise adbdgl_adapter.exceptions.ADBMetagraphError: If invalid metagraph. """ graph = self.__db.graph(name) - v_cols = graph.vertex_collections() - e_cols = {col["edge_collection"] for col in graph.edge_definitions()} + v_cols: Set[str] = graph.vertex_collections() # type: ignore + edge_definitions: List[Json] = graph.edge_definitions() # type: ignore + e_cols: Set[str] = {c["edge_collection"] for c in edge_definitions} - return self.arangodb_collections_to_dgl(name, v_cols, e_cols, **query_options) + return self.arangodb_collections_to_dgl( + name, v_cols, e_cols, **adb_export_kwargs + ) + + ########################### + # Public: DGL -> ArangoDB # + ########################### def dgl_to_arangodb( self, name: str, dgl_g: Union[DGLGraph, DGLHeteroGraph], + metagraph: DGLMetagraph = {}, + explicit_metagraph: bool = True, overwrite_graph: bool = False, - **import_options: Any, + batch_size: Optional[int] = None, + use_async: bool = False, + **adb_import_kwargs: Any, ) -> ADBGraph: """Create an ArangoDB graph from a DGL graph. @@ -224,117 +403,644 @@ def dgl_to_arangodb( :type name: str :param dgl_g: The existing DGL graph. :type dgl_g: Union[dgl.DGLGraph, dgl.heterograph.DGLHeteroGraph] + :param metagraph: An optional object mapping the DGL keys of + the node & edge data to strings, list of strings, or user-defined + functions. NOTE: Unlike the metagraph for ArangoDB to DGL, this + one is optional. + + The current supported **metagraph** values are: + 1) Set[str]: The set of DGL data properties to store + in your ArangoDB database. + + 2) Dict[str, str]: The DGL property name mapped to the ArangoDB + attribute name that will be used to store your DGL data in ArangoDB. + + 3) List[str]: A list of ArangoDB attribute names that will break down + your tensor data, resulting in one ArangoDB attribute per feature. + Must know the number of node/edge features in advance to take + advantage of this metagraph value type. + + 4) Dict[str, Callable[[pandas.DataFrame], torch.Tensor]]: + The DGL property name mapped to a user-defined function + for custom behaviour. NOTE: The function must take as input + a PyTorch Tensor, and must return a Pandas DataFrame. + + See below for an example of **metagraph**. + :type metagraph: adbdgl_adapter.typings.DGLMetagraph + :param explicit_metagraph: Whether to take the metagraph at face value or not. + If False, node & edge types OMITTED from the metagraph will still be + brought over into ArangoDB. Also applies to node & edge attributes. + Defaults to True. + :type explicit_metagraph: bool :param overwrite_graph: Overwrites the graph if it already exists. - Does not drop associated collections. + Does not drop associated collections. Defaults to False. :type overwrite_graph: bool - :param import_options: Keyword arguments to specify additional + :param batch_size: Process the DGL Nodes & Edges in batches of size + **batch_size**. Defaults to `None`, which processes each + NodeStorage & EdgeStorage in one batch. + :type batch_size: int + :param use_async: Performs asynchronous ArangoDB ingestion if enabled. + Defaults to False. + :type use_async: bool + :param adb_import_kwargs: Keyword arguments to specify additional parameters for ArangoDB document insertion. Full parameter list: https://docs.python-arango.com/en/main/specs.html#arango.collection.Collection.import_bulk - :type import_options: Any + :type adb_import_kwargs: Any :return: The ArangoDB Graph API wrapper. :rtype: arango.graph.Graph + :raise adbdgl_adapter.exceptions.DGLMetagraphError: If invalid metagraph. + + **metagraph** example + + .. code-block:: python + def y_tensor_to_2_column_dataframe(dgl_tensor): + # A user-defined function to create two ArangoDB attributes + # out of the 'y' label tensor + label_map = {0: "Kiwi", 1: "Blueberry", 2: "Avocado"} + + df = pandas.DataFrame(columns=["label_num", "label_str"]) + df["label_num"] = dgl_tensor.tolist() + df["label_str"] = df["label_num"].map(label_map) + + return df + + metagraph = { + "nodeTypes": { + "v0": { + "x": "features", # 1) + "y": y_tensor_to_2_column_dataframe, # 2) + }, + "v1": {"x"} # 3) + }, + "edgeTypes": { + ("v0", "e0", "v0"): {"edge_attr": [ "a", "b"]}, # 4) + }, + } + + The metagraph above accomplishes the following: + 1) Renames the DGL 'v0' 'x' feature matrix to 'features' + when stored in ArangoDB. + 2) Builds a 2-column Pandas DataFrame from the 'v0' 'y' labels + through a user-defined function for custom behaviour handling. + 3) Transfers the DGL 'v1' 'x' feature matrix under the same name. + 4) Dissasembles the 2-feature Tensor into two ArangoDB attributes, + where each attribute holds one feature value. """ logger.debug(f"--dgl_to_arangodb('{name}')--") - is_default = dgl_g.canonical_etypes == self.DEFAULT_CANONICAL_ETYPE - logger.debug(f"Is graph '{name}' using default canonical_etypes? {is_default}") + validate_dgl_metagraph(metagraph) - edge_definitions = self.etypes_to_edefinitions( - [(name + "_N", name + "_E", name + "_N")] - if is_default - else dgl_g.canonical_etypes + is_custom_controller = type(self.__cntrl) is not ADBDGL_Controller + is_explicit_metagraph = metagraph != {} and explicit_metagraph + + has_one_ntype = len(dgl_g.ntypes) == 1 + has_one_etype = len(dgl_g.canonical_etypes) == 1 + + # Get the Node & Edge types + node_types, edge_types = self.__get_node_and_edge_types( + name, dgl_g, metagraph, is_explicit_metagraph ) - if overwrite_graph: - logger.debug("Overwrite graph flag is True. Deleting old graph.") - self.__db.delete_graph(name, ignore_missing=True) + # Create the ArangoDB Graph + adb_graph = self.__create_adb_graph( + name, overwrite_graph, node_types, edge_types + ) - if self.__db.has_graph(name): - adb_graph = self.__db.graph(name) - else: - adb_graph = self.__db.create_graph(name, edge_definitions) - - adb_v_cols = adb_graph.vertex_collections() - adb_e_cols = [e_d["edge_collection"] for e_d in adb_graph.edge_definitions()] - - has_one_vcol = len(adb_v_cols) == 1 - has_one_ecol = len(adb_e_cols) == 1 - logger.debug(f"Is graph '{name}' homogenous? {has_one_vcol and has_one_ecol}") - - node: Tensor - v_col_docs: List[Json] = [] # to-be-inserted ArangoDB vertices - for ntype in dgl_g.ntypes: - v_col = adb_v_cols[0] if is_default else ntype - logger.debug(f"Preparing {dgl_g.number_of_nodes(ntype)} '{v_col}' nodes") - - features = dgl_g.node_attr_schemes(ntype).keys() - - for i, node in enumerate(dgl_g.nodes(ntype)): - dgl_node_id = node.item() - logger.debug(f"N{i}: {dgl_node_id}") - - adb_vertex = {"_key": str(dgl_node_id)} - self.__prepare_adb_attributes( - dgl_g.ndata, - features, - dgl_node_id, - adb_vertex, - v_col, - has_one_vcol, + spinner_progress = get_import_spinner_progress(" ") + + ############# + # DGL Nodes # + ############# + + n_meta = metagraph.get("nodeTypes", {}) + for n_type in node_types: + meta = n_meta.get(n_type, {}) + + n_key = None if has_one_ntype else n_type + + ndata = dgl_g.nodes[n_key].data + ndata_size = dgl_g.num_nodes(n_key) + ndata_batch_size = batch_size or ndata_size + + start_index = 0 + end_index = min(ndata_batch_size, ndata_size) + batches = ceil(ndata_size / ndata_batch_size) + + bar_progress = get_bar_progress(f"(DGL → ADB): '{n_type}'", "#97C423") + bar_progress_task = bar_progress.add_task(n_type, total=ndata_size) + + with Live(Group(bar_progress, spinner_progress)): + for _ in range(batches): + # 1. Process the Node batch + df = self.__process_dgl_node_batch( + n_type, + ndata, + ndata_size, + meta, + is_explicit_metagraph, + is_custom_controller, + start_index, + end_index, + ) + + bar_progress.advance(bar_progress_task, advance=len(df)) + + # 2. Insert the ArangoDB Node Documents + self.__insert_adb_docs( + spinner_progress, df, n_type, use_async, **adb_import_kwargs + ) + + # 3. Update the batch indices + start_index = end_index + end_index = min(end_index + ndata_batch_size, ndata_size) + + ############# + # DGL Edges # + ############# + + e_meta = metagraph.get("edgeTypes", {}) + for e_type in edge_types: + meta = e_meta.get(e_type, {}) + + e_key = None if has_one_etype else e_type + + edata = dgl_g.edges[e_key].data + edata_size = dgl_g.num_edges(e_key) + edata_batch_size = batch_size or edata_size + + start_index = 0 + end_index = min(edata_batch_size, edata_size) + batches = ceil(edata_size / edata_batch_size) + + bar_progress = get_bar_progress(f"(DGL → ADB): {e_type}", "#994602") + bar_progress_task = bar_progress.add_task(str(e_type), total=edata_size) + + from_nodes, to_nodes = dgl_g.edges(etype=e_key) + + with Live(Group(bar_progress, spinner_progress)): + for _ in range(batches): + # 1. Process the Edge batch + df = self.__process_dgl_edge_batch( + e_type, + edata, + edata_size, + meta, + from_nodes, + to_nodes, + is_explicit_metagraph, + is_custom_controller, + start_index, + end_index, + ) + + bar_progress.advance(bar_progress_task, advance=len(df)) + + # 2. Insert the ArangoDB Edge Documents + self.__insert_adb_docs( + spinner_progress, df, e_type[1], use_async, **adb_import_kwargs + ) + + # 3. Update the batch indices + start_index = end_index + end_index = min(end_index + edata_batch_size, edata_size) + + logger.info(f"Created ArangoDB '{name}' Graph") + return adb_graph + + ############################ + # Private: ArangoDB -> DGL # + ############################ + + def __fetch_adb_docs( + self, + col: str, + meta: Union[Set[str], Dict[str, ADBMetagraphValues]], + **adb_export_kwargs: Any, + ) -> Tuple[Cursor, int]: + """ArangoDB -> DGL: Fetches ArangoDB documents within a collection. + Returns the documents in a DataFrame. + + :param col: The ArangoDB collection. + :type col: str + :param meta: The MetaGraph associated to **col** + :type meta: Set[str] | Dict[str, adbdgl_adapter.typings.ADBMetagraphValues] + :param adb_export_kwargs: Keyword arguments to specify AQL query options + when fetching documents from the ArangoDB instance. + :type adb_export_kwargs: Any + :return: A DataFrame representing the ArangoDB documents. + :rtype: pandas.DataFrame + """ + + def get_aql_return_value( + meta: Union[Set[str], Dict[str, ADBMetagraphValues]] + ) -> str: + """Helper method to formulate the AQL `RETURN` value based on + the document attributes specified in **meta** + """ + attributes = [] + + if type(meta) is set: + attributes = list(meta) + + elif type(meta) is dict: + for value in meta.values(): + if type(value) is str: + attributes.append(value) + elif type(value) is dict: + attributes.extend(list(value.keys())) + elif callable(value): + # Cannot determine which attributes to extract if UDFs are used + # Therefore we just return the entire document + return "doc" + + return f""" + MERGE( + {{ _key: doc._key, _from: doc._from, _to: doc._to }}, + KEEP(doc, {list(attributes)}) ) + """ - v_col_docs.append(adb_vertex) + col_size: int = self.__db.collection(col).count() # type: ignore + + with get_export_spinner_progress(f"ADB Export: '{col}' ({col_size})") as p: + p.add_task(col) + + cursor: Cursor = self.__db.aql.execute( # type: ignore + f"FOR doc IN @@col RETURN {get_aql_return_value(meta)}", + bind_vars={"@col": col}, + **{**adb_export_kwargs, **{"stream": True}}, + ) - self.__insert_adb_docs(v_col, v_col_docs, import_options) - v_col_docs.clear() + return cursor, col_size + + def __process_adb_cursor( + self, + progress_color: str, + cursor: Cursor, + col_size: int, + process_adb_df: Callable[..., int], + col: str, + adb_map: ADBMap, + meta: Union[Set[str], Dict[str, ADBMetagraphValues]], + **kwargs: Any, + ) -> None: + """ArangoDB -> DGL: Processes the ArangoDB Cursors for vertices and edges. + + :param progress_color: The progress bar color. + :type progress_color: str + :param cursor: The ArangoDB cursor for the current **col**. + :type cursor: arango.cursor.Cursor + :param col_size: The size of **col**. + :type col_size: int + :param process_adb_df: The function to process the cursor data + (in the form of a Dataframe). + :type process_adb_df: Callable + :param col: The ArangoDB collection for the current **cursor**. + :type col: str + :param adb_map: The ArangoDB -> DGL map. + :type adb_map: adbdgl_adapter.typings.ADBMap + :param meta: The metagraph for the current **col**. + :type meta: Set[str] | Dict[str, ADBMetagraphValues] + :param kwargs: Additional keyword arguments to pass to **process_adb_df**. + :type args: Any + """ - from_n: Tensor - to_n: Tensor - e_col_docs: List[Json] = [] # to-be-inserted ArangoDB edges - for c_etype in dgl_g.canonical_etypes: - logger.debug(f"Preparing {dgl_g.number_of_edges(c_etype)} {c_etype} edges") + progress = get_bar_progress(f"(ADB → DGL): '{col}'", progress_color) + progress_task_id = progress.add_task(col, total=col_size) - features = dgl_g.edge_attr_schemes(c_etype).keys() + with Live(Group(progress)): + i = 0 + while not cursor.empty(): + cursor_batch = len(cursor.batch()) # type: ignore + df = DataFrame([cursor.pop() for _ in range(cursor_batch)]) - if is_default: - e_col = adb_e_cols[0] - from_col = to_col = adb_v_cols[0] + i = process_adb_df(i, df, col, adb_map, meta, **kwargs) + progress.advance(progress_task_id, advance=len(df)) + + df.drop(df.index, inplace=True) + + if cursor.has_more(): + cursor.fetch() + + def __process_adb_vertex_df( + self, + i: int, + df: DataFrame, + v_col: str, + adb_map: ADBMap, + meta: Union[Set[str], Dict[str, ADBMetagraphValues]], + ndata: DGLData, + ) -> int: + """ArangoDB -> DGL: Process the ArangoDB Vertex DataFrame + into the DGL NData object. + + :param i: The last DGL Node id value. + :type i: int + :param df: The ArangoDB Vertex DataFrame. + :type df: pandas.DataFrame + :param v_col: The ArangoDB Vertex Collection. + :type v_col: str + :param adb_map: The ArangoDB -> DGL map. + :type adb_map: adbdgl_adapter.typings.ADBMap + :param meta: The metagraph for the current **v_col**. + :type meta: Set[str] | Dict[str, ADBMetagraphValues] + :param node_data: The node data view for storing node features + :type node_data: adbdgl_adapter.typings.DGLData + :return: The last DGL Node id value. + :rtype: int + """ + # 1. Map each ArangoDB _key to a DGL node id + for adb_id in df["_key"]: + adb_map[v_col][adb_id] = i + i += 1 + + # 2. Set the DGL Node Data + self.__set_dgl_data(v_col, meta, ndata, df) + + return i + + def __process_adb_edge_df( + self, + _: int, + df: DataFrame, + e_col: str, + adb_map: ADBMap, + meta: Union[Set[str], Dict[str, ADBMetagraphValues]], + edata: DGLData, + data_dict: DGLDataDict, + v_cols: List[str], + edge_type_blacklist: Set[DGLCanonicalEType], + ) -> int: + """ArangoDB -> DGL: Process the ArangoDB Edge DataFrame + into the DGL EdgeData object. + + :param _: Not used. + :type _: int + :param df: The ArangoDB Edge DataFrame. + :type df: pandas.DataFrame + :param e_col: The ArangoDB Edge Collection. + :type e_col: str + :param adb_map: The ArangoDB -> DGL map. + :type adb_map: adbdgl_adapter.typings.ADBMap + :param meta: The metagraph for the current **e_col**. + :type meta: Set[str] | Dict[str, ADBMetagraphValues] + :param edata: The edge data view for storing edge features + :type edata: adbdgl_adapter.typings.DGLData + :param data_dict: The data for constructing a graph, + which takes the form of (U, V). + (U[i], V[i]) forms the edge with ID i in the graph. + :type data_dict: adbdgl_adapter.typings.DGLDataDict + :param v_cols: The list of ArangoDB Vertex Collections. + :type v_cols: List[str] + :param edge_type_blacklist: The set of skipped edge types + :type edge_type_blacklist: Set[DGLCanonicalEType] + :return: The last DGL Edge id value. This is a useless return value, + but is needed for type hinting. + :rtype: int + """ + # 1. Split the ArangoDB _from & _to IDs into two columns + df[["from_col", "from_key"]] = self.__split_adb_ids(df["_from"]) + df[["to_col", "to_key"]] = self.__split_adb_ids(df["_to"]) + + # 2. Iterate over each edge type + for (from_col, to_col), count in ( + df[["from_col", "to_col"]].value_counts().items() + ): + edge_type: DGLCanonicalEType = (from_col, e_col, to_col) + + # 3. Check for partial Edge Collection import + if from_col not in v_cols or to_col not in v_cols: + logger.debug(f"Skipping {edge_type}") + edge_type_blacklist.add(edge_type) + continue + + logger.debug(f"Preparing {count} {edge_type} edges") + + # 4. Get the edge data corresponding to the current edge type + et_df = df[(df["from_col"] == from_col) & (df["to_col"] == to_col)] + + # 5. Map each ArangoDB from/to _key to the corresponding DGL node id + from_nodes = et_df["from_key"].map(adb_map[from_col]).tolist() + to_nodes = et_df["to_key"].map(adb_map[to_col]).tolist() + + # 6. Set/Update the DGL Edge Index + if edge_type not in data_dict: + data_dict[edge_type] = (tensor(from_nodes), tensor(to_nodes)) else: - from_col, e_col, to_col = c_etype + previous_from_nodes, previous_to_nodes = data_dict[edge_type] + data_dict[edge_type] = ( + cat((previous_from_nodes, tensor(from_nodes))), + cat((previous_to_nodes, tensor(to_nodes))), + ) - for i, (from_n, to_n) in enumerate(zip(*dgl_g.edges(etype=c_etype))): - logger.debug(f"E{i}: ({from_n}, {to_n})") + # 7. Set the DGL Edge Data + self.__set_dgl_data(edge_type, meta, edata, df) - adb_edge = { - "_from": f"{from_col}/{str(from_n.item())}", - "_to": f"{to_col}/{str(to_n.item())}", - } - self.__prepare_adb_attributes( - dgl_g.edata, - features, - i, - adb_edge, - e_col, - has_one_ecol, - c_etype, + return 1 # Useless return value, but needed for type hinting + + def __split_adb_ids(self, s: Series) -> Series: + """AranogDB -> DGL: Helper method to split the ArangoDB IDs + within a Series into two columns + + :param s: The Series containing the ArangoDB IDs. + :type s: pandas.Series + :return: A DataFrame with two columns: the ArangoDB Collection, + and the ArangoDB _key. + :rtype: pandas.Series + """ + return s.str.split(pat="/", n=1, expand=True) + + def __set_dgl_data( + self, + data_type: DGLDataTypes, + meta: Union[Set[str], Dict[str, ADBMetagraphValues]], + dgl_data: DGLData, + df: DataFrame, + ) -> None: + """AranogDB -> DGL: A helper method to build the DGL NodeSpace or + EdgeSpace object for the DGL graph. Is responsible for preparing the + input **meta** such that it becomes a dictionary, and building DGL-ready + tensors from the ArangoDB DataFrame **df**. + + :param data_type: The current node or edge type of the soon-to-be DGL graph. + :type data_type: str | tuple[str, str, str] + :param meta: The metagraph associated to the current ArangoDB vertex or + edge collection. e.g metagraph['vertexCollections']['Users'] + :type meta: Set[str] | Dict[str, adbdgl_adapter.typings.ADBMetagraphValues] + :param dgl_data: The (currently empty) DefaultDict object storing the node or + edge features of the soon-to-be DGL graph. + :type dgl_data: adbdgl_adapter.typings.DGLData + :param df: The DataFrame representing the ArangoDB collection data + :type df: pandas.DataFrame + """ + valid_meta: Dict[str, ADBMetagraphValues] + valid_meta = meta if type(meta) is dict else {m: m for m in meta} + + for k, v in valid_meta.items(): + t = self.__build_tensor_from_dataframe(df, k, v) + dgl_data[k][data_type] = cat((dgl_data[k][data_type], t)) + + def __build_tensor_from_dataframe( + self, + adb_df: DataFrame, + meta_key: str, + meta_val: ADBMetagraphValues, + ) -> Tensor: + """AranogDB -> DGL: Constructs a DGL-ready Tensor from a Pandas + Dataframe, based on the nature of the user-defined metagraph. + + :param adb_df: The Pandas Dataframe representing ArangoDB data. + :type adb_df: pandas.DataFrame + :param meta_key: The current ArangoDB-DGL metagraph key + :type meta_key: str + :param meta_val: The value mapped to **meta_key** to + help convert **df** into a DGL-ready Tensor. + e.g the value of `metagraph['vertexCollections']['users']['x']`. + :type meta_val: adbdgl_adapter.typings.ADBMetagraphValues + :return: A DGL-ready tensor equivalent to the dataframe + :rtype: torch.Tensor + :raise adbdgl_adapter.exceptions.ADBMetagraphError: If invalid **meta_val**. + """ + logger.debug( + f"__build_tensor_from_dataframe(df, '{meta_key}', {type(meta_val)})" + ) + + if type(meta_val) is str: + return tensor(adb_df[meta_val].to_list()) + + if type(meta_val) is dict: + data = [] + for attr, encoder in meta_val.items(): + if encoder is None: + data.append(tensor(adb_df[attr].to_list())) + elif callable(encoder): + data.append(encoder(adb_df[attr])) + else: # pragma: no cover + msg = f"Invalid encoder for ArangoDB attribute '{attr}': {encoder}" + raise ADBMetagraphError(msg) + + return cat(data, dim=-1) + + if callable(meta_val): + # **meta_val** is a user-defined that returns a tensor + user_defined_result = meta_val(adb_df) + + if type(user_defined_result) is not Tensor: # pragma: no cover + msg = f"Invalid return type for function {meta_val} ('{meta_key}')" + raise ADBMetagraphError(msg) + + return user_defined_result + + raise ADBMetagraphError(f"Invalid {meta_val} type") # pragma: no cover + + def __create_dgl_graph( + self, data_dict: DGLDataDict, adb_map: ADBMap, metagraph: ADBMetagraph + ) -> Union[DGLGraph, DGLHeteroGraph]: + """AranogDB -> DGL: Creates a DGL graph from the given DGL data. + + :param data_dict: The data for constructing a graph, + which takes the form of (U, V). + (U[i], V[i]) forms the edge with ID i in the graph. + :type data_dict: adbdgl_adapter.typings.DGLDataDict + :param adb_map: A mapping of ArangoDB IDs to DGL IDs. + :type adb_map: adbdgl_adapter.typings.ADBMap + :param metagraph: The ArangoDB metagraph. + :type metagraph: adbdgl_adapter.typings.ADBMetagraph + :return: A DGL Homogeneous or Heterogeneous graph object + :rtype: dgl.DGLGraph | dgl.DGLHeteroGraph + """ + is_homogeneous = ( + len(metagraph["vertexCollections"]) == 1 + and len(metagraph["edgeCollections"]) == 1 + ) + + if is_homogeneous: + v_col = next(iter(metagraph["vertexCollections"])) + data = next(iter(data_dict.values())) + + return graph(data, num_nodes=len(adb_map[v_col])) + + num_nodes_dict = {v_col: len(adb_map[v_col]) for v_col in adb_map} + return heterograph(data_dict, num_nodes_dict) + + def __link_dgl_data( + self, + dgl_data: Union[HeteroNodeDataView, HeteroEdgeDataView], + dgl_data_temp: DGLData, + has_one_type: bool, + ) -> None: + """Links **dgl_data_temp** to **dgl_data**. This method is (unfortunately) + required, since a dgl graph's `ndata` and `edata` properties can't be + manually set (i.e `g.ndata = ndata` is not possible). + + :param dgl_data: The (empty) ndata or edata instance attribute of a dgl graph, + which is about to receive **dgl_data_temp**. + :type dgl_data: Union[dgl.view.HeteroNodeDataView, dgl.view.HeteroEdgeDataView] + :param dgl_data_temp: A temporary place to store the ndata or edata features. + :type dgl_data_temp: adbdgl_adapter.typings.DGLData + :param has_one_type: Set to True if the DGL graph only has one + node type or edge type. + :type has_one_type: bool + """ + for feature_name, feature_map in dgl_data_temp.items(): + for data_type, dgl_tensor in feature_map.items(): + dgl_data[feature_name] = ( + dgl_tensor if has_one_type else {data_type: dgl_tensor} ) - e_col_docs.append(adb_edge) + ############################ + # Private: DGL -> ArangoDB # + ############################ - self.__insert_adb_docs(e_col, e_col_docs, import_options) - e_col_docs.clear() + def __get_node_and_edge_types( + self, + name: str, + dgl_g: DGLGraph, + metagraph: DGLMetagraph, + is_explicit_metagraph: bool, + ) -> Tuple[List[str], List[DGLCanonicalEType]]: + """DGL -> ArangoDB: Returns the node & edge types of the DGL graph, + based on the metagraph and whether the graph has default canonical etypes. - logger.info(f"Created ArangoDB '{name}' Graph") - return adb_graph + :param name: The DGL graph name. + :type name: str + :param dgl_g: The existing DGL graph. + :type dgl_g: dgl.DGLGraph + :param metagraph: The DGL Metagraph. + :type metagraph: adbdgl_adapter.typings.DGLMetagraph + :param is_explicit_metagraph: Take the metagraph at face value or not. + :type is_explicit_metagraph: bool + :return: The node & edge types of the DGL graph. + :rtype: Tuple[List[str], List[adbdgl_adapter.typings.DGLCanonicalEType]] + """ + node_types: List[str] + edge_types: List[DGLCanonicalEType] + + has_default_canonical_etypes = dgl_g.canonical_etypes == [("_N", "_E", "_N")] + + if is_explicit_metagraph: + node_types = metagraph.get("nodeTypes", {}).keys() # type: ignore + edge_types = metagraph.get("edgeTypes", {}).keys() # type: ignore + + elif has_default_canonical_etypes: + n_type = name + "_N" + node_types = [n_type] + edge_types = [(n_type, name + "_E", n_type)] + + else: + node_types = dgl_g.ntypes + edge_types = dgl_g.canonical_etypes - def etypes_to_edefinitions( - self, canonical_etypes: List[DGLCanonicalEType] + return node_types, edge_types + + def __etypes_to_edefinitions( + self, edge_types: List[DGLCanonicalEType] ) -> List[Json]: - """Converts a DGL graph's canonical_etypes property to ArangoDB graph edge definitions + """Converts DGL canonical_etypes to ArangoDB edge_definitions - :param canonical_etypes: A list of string triplets (str, str, str) for + :param edge_types: A list of string triplets (str, str, str) for source node type, edge type and destination node type. - :type canonical_etypes: List[adbdgl_adapter.typings.DGLCanonicalEType] + :type edge_types: List[adbdgl_adapter.typings.DGLCanonicalEType] :return: ArangoDB Edge Definitions :rtype: List[adbdgl_adapter.typings.Json] @@ -350,9 +1056,13 @@ def etypes_to_edefinitions( ] """ + if not edge_types: + return [] + edge_type_map: DefaultDict[str, DefaultDict[str, Set[str]]] edge_type_map = defaultdict(lambda: defaultdict(set)) - for edge_type in canonical_etypes: + + for edge_type in edge_types: from_col, e_col, to_col = edge_type edge_type_map[e_col]["from"].add(from_col) edge_type_map[e_col]["to"].add(to_col) @@ -369,120 +1079,362 @@ def etypes_to_edefinitions( return edge_definitions - def __prepare_dgl_features( + def __ntypes_to_ocollections( + self, node_types: List[str], edge_types: List[DGLCanonicalEType] + ) -> List[str]: + """Converts DGL node_types to ArangoDB orphan collections, if any. + + :param node_types: A list of strings representing the DGL node types. + :type node_types: List[str] + :param edge_types: A list of string triplets (str, str, str) for + source node type, edge type and destination node type. + :type edge_types: List[adbdgl_adapter.typings.DGLCanonicalEType] + :return: ArangoDB Orphan Collections + :rtype: List[str] + """ + + non_orphan_collections = set() + for from_col, _, to_col in edge_types: + non_orphan_collections.add(from_col) + non_orphan_collections.add(to_col) + + orphan_collections = set(node_types) ^ non_orphan_collections + return list(orphan_collections) + + def __create_adb_graph( self, - features_data: DefaultDict[Any, Any], - attributes: Set[str], - doc: Json, - col: Union[str, DGLCanonicalEType], - ) -> None: - """Convert a set of ArangoDB attributes into valid DGL features - - :param features_data: A dictionary storing the DGL features formatted as lists. - :type features_data: Defaultdict[Any, Any] - :param attributes: A set of ArangoDB attribute keys to convert into DGL features - :type attributes: Set[str] - :param doc: The current ArangoDB document - :type doc: adbdgl_adapter.typings.Json - :param col: The collection the current document belongs to. For edge - collections, the entire DGL Canonical eType is specified (src, e, dst) - :type col: str | Tuple[str, str, str] + name: str, + overwrite_graph: bool, + node_types: List[str], + edge_types: List[DGLCanonicalEType], + ) -> ADBGraph: + """Creates an ArangoDB graph. + + :param name: The ArangoDB graph name. + :type name: str + :param overwrite_graph: Overwrites the graph if it already exists. + Does not drop associated collections. Defaults to False. + :type overwrite_graph: bool + :param node_types: A list of strings representing the DGL node types. + :type node_types: List[str] + :param edge_types: A list of string triplets (str, str, str) for + source node type, edge type and destination node type. + :type edge_types: List[adbdgl_adapter.typings.DGLCanonicalEType] + :return: The ArangoDB Graph API wrapper. + :rtype: arango.graph.Graph """ - key: str - for key in attributes: - arr: List[Any] = features_data[key][col] - arr.append( - self.__cntrl._adb_attribute_to_dgl_feature(key, col, doc.get(key, None)) - ) + if overwrite_graph: + logger.debug("Overwrite graph flag is True. Deleting old graph.") + self.__db.delete_graph(name, ignore_missing=True) - def __insert_dgl_features( + if self.__db.has_graph(name): + return self.__db.graph(name) + + edge_definitions = self.__etypes_to_edefinitions(edge_types) + orphan_collections = self.__ntypes_to_ocollections(node_types, edge_types) + + return self.__db.create_graph( # type: ignore[return-value] + name, + edge_definitions, + orphan_collections, + ) + + def __process_dgl_node_batch( self, - features_data: DefaultDict[Any, Any], - data: Union[HeteroNodeDataView, HeteroEdgeDataView], - has_one_type: bool, - ) -> None: - """Insert valid DGL features into a DGL graph. - - :param features_data: A dictionary storing the DGL features formatted as lists. - :type features_data: Defaultdict[Any, Any] - :param data: The (empty) ndata or edata instance attribute of a dgl graph, - which is about to receive **features_data**. - :type data: Union[dgl.view.HeteroNodeDataView, dgl.view.HeteroEdgeDataView] - :param has_one_type: Set to True if the DGL graph only has one ntype, - or one etype. - :type has_one_type: bool + n_type: str, + ndata: NodeSpace, + ndata_size: int, + meta: Union[Set[str], Dict[Any, DGLMetagraphValues]], + is_explicit_metagraph: bool, + is_custom_controller: bool, + start_index: int, + end_index: int, + ) -> DataFrame: + """DGL -> ArangoDB: Processes the DGL Node batch + into an ArangoDB DataFrame. + + :param n_type: The DGL node type. + :type n_type: str + :param ndata: The DGL Node Space for the current **n_type**. + :type ndata: dgl.view.NodeSpace + :param ndata_size: The size of **ndata**. + :param ndata_size: int + :param meta: The metagraph for the current **n_type**. + :type meta: Set[str] | Dict[Any, adbdgl_adapter.typings.DGLMetagraphValues] + :param is_explicit_metagraph: Take the metagraph at face value or not. + :type is_explicit_metagraph: bool + :param is_custom_controller: Whether a custom controller is used. + :type is_custom_controller: bool + :param start_index: The start index of the current batch. + :type start_index: int + :param end_index: The end index of the current batch. + :type end_index: int + :return: The ArangoDB DataFrame representing the DGL Node batch. + :rtype: pandas.DataFrame """ - col_dict: Dict[str, List[Any]] - for key, col_dict in features_data.items(): - for col, array in col_dict.items(): - logger.debug(f"Inserting {len(array)} '{key}' features into '{col}'") - data[key] = tensor(array) if has_one_type else {col: tensor(array)} + # 1. Map each DGL node id to an ArangoDB _key + adb_keys = [{"_key": str(i)} for i in range(start_index, end_index)] + + # 2. Set the ArangoDB Node Data + df = self.__set_adb_data( + DataFrame(adb_keys, index=range(start_index, end_index)), + meta, + ndata, + ndata_size, + is_explicit_metagraph, + start_index, + end_index, + ) + + # 3. Apply the ArangoDB Node Controller (if provided) + if is_custom_controller: + f = lambda n: self.__cntrl._prepare_dgl_node(n, n_type) + df = df.apply(f, axis=1) - def __prepare_adb_attributes( + return df + + def __process_dgl_edge_batch( self, - data: Union[HeteroNodeDataView, HeteroEdgeDataView], - features: Set[Any], - id: Union[int, float, bool], - doc: Json, - col: str, - has_one_col: bool, - canonical_etype: Optional[DGLCanonicalEType] = None, - ) -> None: - """Convert DGL features into a set of ArangoDB attributes for a given document - - :param data: The ndata or edata instance attribute of a dgl graph, filled with - node or edge feature data. - :type data: Union[dgl.view.HeteroNodeDataView, dgl.view.HeteroEdgeDataView] - :param features: A set of DGL feature keys to convert into ArangoDB attributes - :type features: Set[Any] - :param id: The ID of the current DGL node / edge - :type id: Union[int, float, bool] - :param doc: The current ArangoDB document - :type doc: adbdgl_adapter.typings.Json - :param col: The collection the current document belongs to - :type col: str - :param has_one_col: Set to True if the ArangoDB graph has one - vertex collection or one edge collection only. - :type has_one_col: bool - :param canonical_etype: The DGL canonical edge type belonging to the current - **col**, provided that **col** is an edge collection (ignored otherwise). - :type canonical_etype: adbdgl_adapter.typings.DGLCanonicalEType + e_type: DGLCanonicalEType, + edata: EdgeSpace, + edata_size: int, + meta: Union[Set[str], Dict[Any, DGLMetagraphValues]], + from_nodes: Tensor, + to_nodes: Tensor, + is_explicit_metagraph: bool, + is_custom_controller: bool, + start_index: int, + end_index: int, + ) -> DataFrame: + """DGL -> ArangoDB: Processes the DGL Edge batch + into an ArangoDB DataFrame. + + :param e_type: The DGL edge type. + :type e_type: adbdgl_adapter.typings.DGLCanonicalEType + :param edata: The DGL EdgeSpace for the current **e_type**. + :type edata: dgl.view.EdgeSpace + :param edata_size: The size of **edata**. + :param edata_size: int + :param meta: The metagraph for the current **e_type**. + :type meta: Set[str] | Dict[Any, adbdgl_adapter.typings.DGLMetagraphValues] + :param from_nodes: Tensor representing the Source Nodes of the **e_type**. + :type from_nodes: torch.Tensor + :param to_nodes: Tensor representing the Destination Nodes of the **e_type**. + :type to_nodes: torch.Tensor + :param is_explicit_metagraph: Take the metagraph at face value or not. + :type is_explicit_metagraph: bool + :param is_custom_controller: Whether a custom controller is used. + :type is_custom_controller: bool + :param start_index: The start index of the current batch. + :type start_index: int + :param end_index: The end index of the current batch. + :type end_index: int + :return: The ArangoDB DataFrame representing the DGL Edge batch. + :rtype: pandas.DataFrame """ - for key in features: - tensor = data[key] if has_one_col else data[key][canonical_etype or col] - doc[key] = self.__cntrl._dgl_feature_to_adb_attribute(key, col, tensor[id]) + from_col, _, to_col = e_type - def __fetch_adb_docs(self, col: str, query_options: Any) -> Result[Cursor]: - """Fetches ArangoDB documents within a collection. + # 1. Map the DGL edges to ArangoDB _from & _to IDs + data = zip( + *( + from_nodes[start_index:end_index].tolist(), + to_nodes[start_index:end_index].tolist(), + ) + ) - :param col: The ArangoDB collection. - :type col: str - :param query_options: Keyword arguments to specify AQL query options - when fetching documents from the ArangoDB instance. - :type query_options: Any - :return: Result cursor. - :rtype: arango.cursor.Cursor + # 2. Set the ArangoDB Edge Data + df = self.__set_adb_data( + DataFrame( + data, + index=range(start_index, end_index), + columns=["_from", "_to"], + ), + meta, + edata, + edata_size, + is_explicit_metagraph, + start_index, + end_index, + ) + + df["_from"] = from_col + "/" + df["_from"].astype(str) + df["_to"] = to_col + "/" + df["_to"].astype(str) + + # 3. Apply the ArangoDB Edge Controller (if provided) + if is_custom_controller: + f = lambda e: self.__cntrl._prepare_dgl_edge(e, e_type) + df = df.apply(f, axis=1) + + return df + + def __set_adb_data( + self, + df: DataFrame, + meta: Union[Set[str], Dict[Any, DGLMetagraphValues]], + dgl_data: Union[NodeSpace, EdgeSpace], + dgl_data_size: int, + is_explicit_metagraph: bool, + start_index: int, + end_index: int, + ) -> DataFrame: + """A helper method to build the ArangoDB Dataframe for the given + collection. Is responsible for creating "sub-DataFrames" from DGL tensors, + and appending them to the main dataframe **df**. If the data + does not adhere to the supported types, or is not of specific length, + then it is silently skipped. + + :param df: The main ArangoDB DataFrame containing (at minimum) + the vertex/edge _id or _key attribute. + :type df: pandas.DataFrame + :param meta: The metagraph associated to the + current DGL node or edge type. e.g metagraph['nodeTypes']['v0'] + :type meta: Set[str] | Dict[Any, adbdgl_adapter.typings.DGLMetagraphValues] + :param dgl_data: The NodeSpace or EdgeSpace of the current + DGL node or edge type. + :type dgl_data: dgl.view.(NodeSpace | EdgeSpace) + :param dgl_data_size: The size of the NodeStorage or EdgeStorage of the + current DGL node or edge type. + :type dgl_data_size: int + :param is_explicit_metagraph: Take the metagraph at face value or not. + :type is_explicit_metagraph: bool + :param start_index: The starting index of the current batch to process. + :type start_index: int + :param end_index: The ending index of the current batch to process. + :type end_index: int + :return: The completed DataFrame for the (soon-to-be) ArangoDB collection. + :rtype: pandas.DataFrame + :raise ValueError: If an unsupported DGL data value is found. """ - aql = f""" - FOR doc IN {col} - RETURN doc + logger.debug( + f"__set_adb_data(df, {meta}, {type(dgl_data)}, {is_explicit_metagraph}" + ) + + valid_meta: Dict[Any, DGLMetagraphValues] + valid_meta = meta if type(meta) is dict else {m: m for m in meta} + + dgl_keys = set(valid_meta.keys()) if is_explicit_metagraph else dgl_data.keys() + for meta_key in dgl_keys: + data = dgl_data[meta_key] + meta_val = valid_meta.get(meta_key, str(meta_key)) + + if type(data) is Tensor and len(data) == dgl_data_size: + df = df.join( + self.__build_dataframe_from_tensor( + data[start_index:end_index], + start_index, + end_index, + meta_key, + meta_val, + ) + ) + + return df + + def __build_dataframe_from_tensor( + self, + dgl_tensor: Tensor, + start_index: int, + end_index: int, + meta_key: Any, + meta_val: DGLMetagraphValues, + ) -> DataFrame: + """Builds a Pandas DataFrame from DGL Tensor, based on + the nature of the user-defined metagraph. + + :param dgl_tensor: The Tensor representing DGL data. + :type dgl_tensor: torch.Tensor + :param meta_key: The current DGL-ArangoDB metagraph key + :type meta_key: Any + :param meta_val: The value mapped to the DGL-ArangoDB metagraph key to + help convert **tensor** into a Pandas Dataframe. + e.g the value of `metagraph['nodeTypes']['users']['x']`. + :type meta_val: adbdgl_adapter.typings.DGLMetagraphValues + :return: A Pandas DataFrame equivalent to the Tensor + :rtype: pandas.DataFrame + :raise adbdgl_adapter.exceptions.DGLMetagraphError: If invalid **meta_val**. """ + logger.debug( + f"__build_dataframe_from_tensor(df, '{meta_key}', {type(meta_val)})" + ) - return self.__db.aql.execute(aql, **query_options) + if type(meta_val) is str: + df = DataFrame(index=range(start_index, end_index), columns=[meta_val]) + df[meta_val] = dgl_tensor.tolist() + return df + + if type(meta_val) is list: + num_features = dgl_tensor.size()[-1] + if len(meta_val) != num_features: # pragma: no cover + msg = f""" + Invalid list length for **meta_val** ('{meta_key}'): + List length must match the number of + features found in the tensor ({num_features}). + """ + raise DGLMetagraphError(msg) + + df = DataFrame(index=range(start_index, end_index), columns=meta_val) + df[meta_val] = dgl_tensor.tolist() + return df + + if callable(meta_val): + # **meta_val** is a user-defined function that populates + # and returns the empty dataframe + empty_df = DataFrame(index=range(start_index, end_index)) + user_defined_result = meta_val(dgl_tensor, empty_df) + + if not isinstance(user_defined_result, DataFrame): # pragma: no cover + msg = f""" + Invalid return type for function {meta_val} ('{meta_key}'). + Function must return Pandas DataFrame. + """ + raise DGLMetagraphError(msg) + + if ( + user_defined_result.index.start != start_index + or user_defined_result.index.stop != end_index + ): # pragma: no cover + msg = f""" + User Defined Function {meta_val} ('{meta_key}') must return + DataFrame with start index {start_index} & stop index {end_index} + """ + raise DGLMetagraphError(msg) + + return user_defined_result + + raise DGLMetagraphError(f"Invalid {meta_val} type") # pragma: no cover def __insert_adb_docs( - self, col: str, docs: List[Json], import_options: Any + self, + spinner_progress: Progress, + df: DataFrame, + col: str, + use_async: bool, + **adb_import_kwargs: Any, ) -> None: - """Insert ArangoDB documents into their ArangoDB collection. + """DGL -> ArangoDB: Insert ArangoDB documents into their ArangoDB collection. - :param col: The ArangoDB collection name + :param spinner_progress: The spinner progress bar. + :type spinner_progress: rich.progress.Progress + :param df: To-be-inserted ArangoDB documents, formatted as a DataFrame + :type df: pandas.DataFrame + :param col: The ArangoDB collection name. :type col: str - :param docs: To-be-inserted ArangoDB documents - :type docs: List[Json] - :param import_options: Keyword arguments to specify additional + :param use_async: Performs asynchronous ArangoDB ingestion if enabled. + :type use_async: bool + :param adb_import_kwargs: Keyword arguments to specify additional parameters for ArangoDB document insertion. Full parameter list: https://docs.python-arango.com/en/main/specs.html#arango.collection.Collection.import_bulk + :param adb_import_kwargs: Any """ - logger.debug(f"Inserting {len(docs)} documents into '{col}'") - result = self.__db.collection(col).import_bulk(docs, **import_options) + action = f"ADB Import: '{col}' ({len(df)})" + spinner_progress_task = spinner_progress.add_task("", action=action) + + docs = df.to_dict("records") + db = self.__async_db if use_async else self.__db + result = db.collection(col).import_bulk(docs, **adb_import_kwargs) logger.debug(result) + + df.drop(df.index, inplace=True) + + spinner_progress.stop_task(spinner_progress_task) + spinner_progress.update(spinner_progress_task, visible=False) diff --git a/adbdgl_adapter/controller.py b/adbdgl_adapter/controller.py index cd1c0f5..77e9cc3 100644 --- a/adbdgl_adapter/controller.py +++ b/adbdgl_adapter/controller.py @@ -1,74 +1,52 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -from typing import Any, Union - -from torch import Tensor - -from adbdgl_adapter.typings import DGLCanonicalEType - from .abc import Abstract_ADBDGL_Controller +from .typings import DGLCanonicalEType, Json class ADBDGL_Controller(Abstract_ADBDGL_Controller): """ArangoDB-DGL controller. - Responsible for controlling how ArangoDB attributes - are converted into DGL features, and vice-versa. + Responsible for controlling how nodes & edges are handled when + transitioning from DGL to ArangoDB. - You can derive your own custom ADBDGL_Controller if you want to maintain - consistency between your ArangoDB attributes & your DGL features. + You can derive your own custom ADBDGL_Controller. """ - def _adb_attribute_to_dgl_feature( - self, key: str, col: Union[str, DGLCanonicalEType], val: Any - ) -> Any: - """ - Given an ArangoDB attribute key, its assigned value (for an arbitrary document), - and the collection it belongs to, convert it to a valid - DGL feature: https://docs.dgl.ai/en/0.6.x/guide/graph-feature.html. - - NOTE: You must override this function if you want to transfer non-numerical - ArangoDB attributes to DGL (DGL only accepts 'attributes' (a.k.a features) - of numerical types). Read more about DGL features here: - https://docs.dgl.ai/en/0.6.x/new-tutorial/2_dglgraph.html#assigning-node-and-edge-features-to-graph. - - :param key: The ArangoDB attribute key name - :type key: str - :param col: The ArangoDB collection of the ArangoDB document. - :type col: str - :param val: The assigned attribute value of the ArangoDB document. - :type val: Any - :return: The attribute's representation as a DGL Feature - :rtype: Any + def _prepare_dgl_node(self, dgl_node: Json, node_type: str) -> Json: + """Prepare a DGL node object before it gets inserted into its + designated ArangoDB collection. + + Given a JSON representation of a DGL node, you can modify it + before it gets inserted into its ArangoDB collection, + and/or derive a custom vertex id by updating the "_key" attribute + of the vertex (otherwise the vertex's current "_key" value will be used). + + :param dgl_node: The DGL node object to (optionally) modify. + :type dgl_node: adbnx_adapter.typings.Json + :param node_type: The DGL Node Type of the node + :type node_type: str + :return: The DGL Node object + :rtype: Dict[str, Any] """ - if type(val) in [int, float, bool]: - return val - - try: - return float(val) - except (ValueError, TypeError, SyntaxError): - return 0 - - def _dgl_feature_to_adb_attribute(self, key: str, col: str, val: Tensor) -> Any: - """ - Given a DGL feature key, its assigned value (for an arbitrary node or edge), - and the collection it belongs to, convert it to a valid ArangoDB attribute - (e.g string, list, number, ...). - - NOTE: No action is needed here if you want to keep the numerical-based values - of your DGL features. - - :param key: The DGL attribute key name - :type key: str - :param col: The ArangoDB collection of the (soon-to-be) ArangoDB document. - :type col: str - :param val: The assigned attribute value of the DGL node. - :type val: Tensor - :return: The feature's representation as an ArangoDB Attribute - :rtype: Any + return dgl_node # pragma: no cover + + def _prepare_dgl_edge(self, dgl_edge: Json, edge_type: DGLCanonicalEType) -> Json: + """Prepare a DGL edge object before it gets inserted into its + designated ArangoDB collection. + + Given a JSON representation of a DGL edge, you can modify it + before it gets inserted into its ArangoDB edge collection, + and/or derive a custom edge id by setting the "_key" attribute + of the edge (otherwise the "_key" will be randomly generated by ArangoDB). + + :param dgl_edge: The DGL edge object to (optionally) modify. + :type dgl_edge: adbnx_adapter.typings.Json + :param edge_type: The Edge Type of The DGL edge. Formatted + as (from_collection, edge_collection, to_collection) + :type edge_type: Tuple[str, str, str] + :return: The DGL Edge object + :rtype: Dict[str, Any] """ - try: - return val.item() - except ValueError: - return val.tolist() + return dgl_edge # pragma: no cover diff --git a/adbdgl_adapter/encoders.py b/adbdgl_adapter/encoders.py new file mode 100644 index 0000000..fca7574 --- /dev/null +++ b/adbdgl_adapter/encoders.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# See https://pytorch-geometric.readthedocs.io/en/latest/notes/load_csv.html +# for an example on encoders. + +from typing import Any, Dict, Optional + +from pandas import DataFrame +from torch import Tensor, from_numpy, zeros + + +class IdentityEncoder(object): + """Converts a list of floating-point values into a PyTorch tensor""" + + def __init__(self, dtype: Any = None) -> None: + self.dtype = dtype + + def __call__(self, df: DataFrame) -> Tensor: + return from_numpy(df.values).view(-1, 1).to(self.dtype) + + +class CategoricalEncoder(object): + """Converts a list of values into a PyTorch tensor through a mapping""" + + def __init__(self, mapping: Optional[Dict[Any, Any]] = None) -> None: + self.mapping = mapping + + def __call__(self, df: DataFrame) -> Tensor: + if self.mapping is None: + unique_vals = df.unique() + self.mapping = {u_v: i for i, u_v in enumerate(unique_vals)} + + x = zeros(len(df), 1) + for i, col in enumerate(df.values): + x[i, 0] = self.mapping[col] + + return x diff --git a/adbdgl_adapter/exceptions.py b/adbdgl_adapter/exceptions.py new file mode 100644 index 0000000..7fcc916 --- /dev/null +++ b/adbdgl_adapter/exceptions.py @@ -0,0 +1,19 @@ +class ADBDGLError(Exception): + """Base class for all exceptions in adbdgl-adapter.""" + + +class ADBDGLValidationError(ADBDGLError, TypeError): + """Base class for errors originating from adbdgl-adapter user input validation.""" + + +################## +# Metagraphs # +################## + + +class ADBMetagraphError(ADBDGLValidationError): + """Invalid ArangoDB Metagraph value""" + + +class DGLMetagraphError(ADBDGLValidationError): + """Invalid DGL Metagraph value""" diff --git a/adbdgl_adapter/typings.py b/adbdgl_adapter/typings.py index 22e86f7..05d7ec4 100644 --- a/adbdgl_adapter/typings.py +++ b/adbdgl_adapter/typings.py @@ -1,12 +1,39 @@ -__all__ = ["Json", "ArangoMetagraph", "DGLCanonicalEType"] +__all__ = [ + "Json", + "ADBMetagraph", + "ADBMetagraphValues", + "DGLMetagraph", + "DGLMetagraphValues", + "DGLCanonicalEType", + "DGLDataDict", + "ADBMap", + "DGLMap", +] -from typing import Any, Dict, Set, Tuple +from typing import Any, Callable, DefaultDict, Dict, List, Set, Tuple, Union +from pandas import DataFrame from torch import Tensor Json = Dict[str, Any] -ArangoMetagraph = Dict[str, Dict[str, Set[str]]] + +DataFrameToTensor = Callable[[DataFrame], Tensor] +TensorToDataFrame = Callable[[Tensor, DataFrame], DataFrame] + +ADBEncoders = Dict[str, DataFrameToTensor] +ADBMetagraphValues = Union[str, DataFrameToTensor, ADBEncoders] +ADBMetagraph = Dict[str, Dict[str, Union[Set[str], Dict[str, ADBMetagraphValues]]]] DGLCanonicalEType = Tuple[str, str, str] +DGLData = DefaultDict[str, DefaultDict[Union[str, DGLCanonicalEType], Tensor]] DGLDataDict = Dict[DGLCanonicalEType, Tuple[Tensor, Tensor]] + +DGLDataTypes = Union[str, DGLCanonicalEType] +DGLMetagraphValues = Union[str, List[str], TensorToDataFrame] +DGLMetagraph = Dict[ + str, Dict[DGLDataTypes, Union[Set[str], Dict[Any, DGLMetagraphValues]]] +] + +ADBMap = DefaultDict[DGLDataTypes, Dict[str, int]] +DGLMap = DefaultDict[DGLDataTypes, Dict[int, str]] diff --git a/adbdgl_adapter/utils.py b/adbdgl_adapter/utils.py index 3f3f894..b88dc73 100644 --- a/adbdgl_adapter/utils.py +++ b/adbdgl_adapter/utils.py @@ -1,5 +1,17 @@ import logging import os +from typing import Any, Dict, Set, Union + +from rich.progress import ( + BarColumn, + Progress, + SpinnerColumn, + TaskProgressColumn, + TextColumn, + TimeElapsedColumn, +) + +from .exceptions import ADBMetagraphError, DGLMetagraphError logger = logging.getLogger(__package__) handler = logging.StreamHandler() @@ -9,3 +21,159 @@ ) handler.setFormatter(formatter) logger.addHandler(handler) + + +def get_export_spinner_progress( + text: str, +) -> Progress: + return Progress( + TextColumn(text), + SpinnerColumn("aesthetic", "#5BC0DE"), + TimeElapsedColumn(), + transient=True, + ) + + +def get_import_spinner_progress(text: str) -> Progress: + return Progress( + TextColumn(text), + TextColumn("{task.fields[action]}"), + SpinnerColumn("aesthetic", "#5BC0DE"), + TimeElapsedColumn(), + transient=True, + ) + + +def get_bar_progress(text: str, color: str) -> Progress: + return Progress( + TextColumn(text), + BarColumn(complete_style=color, finished_style=color), + TaskProgressColumn(), + TextColumn("({task.completed}/{task.total})"), + TimeElapsedColumn(), + ) + + +def validate_adb_metagraph(metagraph: Dict[Any, Dict[Any, Any]]) -> None: + meta: Union[Set[Any], Dict[Any, Any]] + + if "vertexCollections" not in metagraph: + raise ADBMetagraphError("Missing 'vertexCollections' key in metagraph") + + if "edgeCollections" not in metagraph: + raise ADBMetagraphError("Missing 'edgeCollections' key in metagraph") + + for parent_key in ["vertexCollections", "edgeCollections"]: + sub_metagraph = metagraph[parent_key] + if not sub_metagraph or type(sub_metagraph) != dict: + raise ADBMetagraphError(f"{parent_key} must map to non-empty dictionary") + + for col, meta in sub_metagraph.items(): + if type(col) != str: + msg = f""" + Invalid {parent_key} sub-key type: + {col} must be str + """ + raise ADBMetagraphError(msg) + + if type(meta) == set: + for m in meta: + if type(m) != str: + msg = f""" + Invalid set value type for {meta}: + {m} must be str + """ + raise ADBMetagraphError(msg) + + elif type(meta) == dict: + for meta_key, meta_val in meta.items(): + if type(meta_key) != str: + msg = f""" + Invalid key type in {meta}: + {meta_key} must be str + """ + raise ADBMetagraphError(msg) + + if type(meta_val) not in [str, dict] and not callable(meta_val): + msg = f""" + Invalid mapped value type in {meta}: + {meta_val} must be + str | Dict[str, None | Callable] | Callable + """ + raise ADBMetagraphError(msg) + + if type(meta_val) == dict: + for k, v in meta_val.items(): + if type(k) != str: + msg = f""" + Invalid ArangoDB attribute key type: + {v} must be str + """ + raise ADBMetagraphError(msg) + + if v is not None and not callable(v): + msg = f""" + Invalid DGL Encoder type: + {v} must be None | Callable + """ + raise ADBMetagraphError(msg) + else: + msg = f""" + Invalid mapped value type for {col}: + {meta} must be dict | set + """ + raise ADBMetagraphError(msg) + + +def validate_dgl_metagraph(metagraph: Dict[Any, Dict[Any, Any]]) -> None: + meta: Union[Set[Any], Dict[Any, Any]] + + for node_type in metagraph.get("nodeTypes", {}).keys(): + if type(node_type) != str: + msg = f"Invalid nodeTypes sub-key: {node_type} is not str" + raise DGLMetagraphError(msg) + + for edge_type in metagraph.get("edgeTypes", {}).keys(): + if type(edge_type) != tuple: + msg = f"Invalid edgeTypes sub-key: {edge_type} must be Tuple[str, str, str]" + raise DGLMetagraphError(msg) + else: + for elem in edge_type: + if type(elem) != str: + msg = f"{elem} in {edge_type} must be str" + raise DGLMetagraphError(msg) + + for parent_key in ["nodeTypes", "edgeTypes"]: + for k, meta in metagraph.get(parent_key, {}).items(): + if type(meta) == set: + for m in meta: + if type(m) != str: + msg = f""" + Invalid set value type for {meta}: + {m} must be str + """ + raise DGLMetagraphError(msg) + + elif type(meta) == dict: + for meta_val in meta.values(): + if type(meta_val) not in [str, list] and not callable(meta_val): + msg = f""" + Invalid mapped value type in {meta}: + {meta_val} must be str | List[str] | Callable + """ + raise DGLMetagraphError(msg) + + if type(meta_val) == list: + for v in meta_val: + if type(v) != str: + msg = f""" + Invalid ArangoDB attribute key type: + {v} must be str + """ + raise DGLMetagraphError(msg) + else: + msg = f""" + Invalid mapped value type for {k}: + {meta} must be dict | set + """ + raise DGLMetagraphError(msg) diff --git a/examples/ArangoDB_DGL_Adapter.ipynb b/examples/ArangoDB_DGL_Adapter.ipynb index 918fecd..7ace981 100644 --- a/examples/ArangoDB_DGL_Adapter.ipynb +++ b/examples/ArangoDB_DGL_Adapter.ipynb @@ -15,7 +15,7 @@ "id": "U1d45V4OeG89" }, "source": [ - "\"Open" + "\"Open" ] }, { @@ -34,7 +34,7 @@ "id": "bpvZS-1aeG89" }, "source": [ - "Version: 2.1.0\n", + "Version: 3.0.0\n", "\n", "Objective: Export Graphs from [ArangoDB](https://www.arangodb.com/), a multi-model Graph Database, to [Deep Graph Library](https://www.dgl.ai/) (DGL), a python package for graph neural networks, and vice-versa." ] @@ -57,9 +57,9 @@ "outputs": [], "source": [ "%%capture\n", - "!pip install adbdgl-adapter==2.1.0\n", + "!pip install adbdgl-adapter==3.0.0\n", "!pip install adb-cloud-connector\n", - "!git clone -b 2.1.0 --single-branch https://github.com/arangoml/dgl-adapter.git\n", + "!git clone -b 3.0.0 --single-branch https://github.com/arangoml/dgl-adapter.git\n", "\n", "## For drawing purposes \n", "!pip install matplotlib\n", @@ -70,26 +70,27 @@ "cell_type": "code", "execution_count": null, "metadata": { - "id": "niijQHqBM6zp" + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "niijQHqBM6zp", + "outputId": "77df8f72-4000-44e8-9dd6-c56bbf33c07d" }, "outputs": [], "source": [ "# All imports\n", "\n", + "import pandas\n", + "import torch\n", "import dgl\n", - "from dgl import remove_self_loop\n", - "from dgl.data import MiniGCDataset\n", "from dgl.data import KarateClubDataset\n", "\n", - "import torch\n", - "from torch import Tensor\n", - "\n", - "from adbdgl_adapter import ADBDGL_Adapter, ADBDGL_Controller\n", - "from adbdgl_adapter.typings import Json, ArangoMetagraph, DGLCanonicalEType, DGLDataDict\n", - "\n", "from arango import ArangoClient\n", "from adb_cloud_connector import get_temp_credentials\n", "\n", + "from adbdgl_adapter import ADBDGL_Adapter, ADBDGL_Controller\n", + "from adbdgl_adapter.encoders import IdentityEncoder, CategoricalEncoder\n", + "\n", "import json\n", "import logging\n", "\n", @@ -130,7 +131,7 @@ "base_uri": "https://localhost:8080/" }, "id": "vf0350qvj8up", - "outputId": "fbf300df-5dcd-44e8-a746-cb554eba1dd8" + "outputId": "bb473200-893d-4d4e-ed6d-239ec497d0e3" }, "outputs": [], "source": [ @@ -163,7 +164,7 @@ "base_uri": "https://localhost:8080/" }, "id": "oOS3AVAnkQEV", - "outputId": "3a7403db-d11b-4f7a-a0b7-6e8220186273" + "outputId": "5b5feaaa-2a6f-4e0e-ef89-68b9e365a6db" }, "outputs": [], "source": [ @@ -199,7 +200,7 @@ "base_uri": "https://localhost:8080/" }, "id": "meLon-KgkU4h", - "outputId": "fa57e121-5294-45f9-b3d0-3a2cfa212da7" + "outputId": "7517b39b-adfa-426d-ccae-89254cf642b5" }, "outputs": [], "source": [ @@ -237,7 +238,7 @@ "base_uri": "https://localhost:8080/" }, "id": "zTebQ0LOlsGA", - "outputId": "f5c06fec-a3e3-41fb-b478-42e492af07de" + "outputId": "c871096b-b06e-4cd8-ad56-06758090600d" }, "outputs": [], "source": [ @@ -280,7 +281,7 @@ "base_uri": "https://localhost:8080/" }, "id": "KsxNujb0mSqZ", - "outputId": "0cf12da9-c754-41a3-9496-5aea0a0faac9" + "outputId": "0b7b4106-7385-4489-e49a-399efbef0cb8" }, "outputs": [], "source": [ @@ -323,7 +324,7 @@ "base_uri": "https://localhost:8080/" }, "id": "2ekGwnJDeG8-", - "outputId": "02cf35c6-9416-44fb-be44-5c0f517e0f78" + "outputId": "84a1c36b-3dc1-47e2-dadf-8a4ebefd98c0" }, "outputs": [], "source": [ @@ -359,7 +360,7 @@ "id": "BM0iRYPDeG8_" }, "source": [ - "For demo purposes, we will be using the [ArangoDB Fraud Detection example graph](https://colab.research.google.com/github/joerg84/Graph_Powered_ML_Workshop/blob/master/Fraud_Detection.ipynb)." + "For demo purposes, we will be using the [ArangoDB IMDB example graph](https://www.arangodb.com/docs/stable/arangosearch-example-datasets.html#imdb-movie-dataset)." ] }, { @@ -370,12 +371,38 @@ "base_uri": "https://localhost:8080/" }, "id": "7bgGJ3QkeG8_", - "outputId": "15b25959-5a2f-4d1c-852e-5019845716a4" + "outputId": "1f490370-72f3-4d1b-8950-ef1d0f690218" }, "outputs": [], "source": [ "!chmod -R 755 dgl-adapter/\n", - "!./dgl-adapter/tests/assets/arangorestore -c none --server.endpoint http+ssl://{con[\"hostname\"]}:{con[\"port\"]} --server.username {con[\"username\"]} --server.database {con[\"dbName\"]} --server.password {con[\"password\"]} --replication-factor 3 --input-directory \"dgl-adapter/examples/data/fraud_dump\" --include-system-collections true" + "!./dgl-adapter/tests/tools/arangorestore -c none --server.endpoint http+ssl://{con[\"hostname\"]}:{con[\"port\"]} --server.username {con[\"username\"]} --server.database {con[\"dbName\"]} --server.password {con[\"password\"]} --replication-factor 3 --input-directory \"dgl-adapter/tests/data/adb/imdb_dump\" --include-system-collections true" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "XLiXYJPRlVYZ", + "outputId": "2666c5b3-1f62-4bfc-c9af-53bc53f0ffd8" + }, + "outputs": [], + "source": [ + "# Create the IMDB graph\n", + "db.delete_graph(\"imdb\", ignore_missing=True)\n", + "db.create_graph(\n", + " \"imdb\",\n", + " edge_definitions=[\n", + " {\n", + " \"edge_collection\": \"Ratings\",\n", + " \"from_vertex_collections\": [\"Users\"],\n", + " \"to_vertex_collections\": [\"Movies\"],\n", + " },\n", + " ],\n", + ")" ] }, { @@ -404,7 +431,7 @@ "base_uri": "https://localhost:8080/" }, "id": "oG496kBeeG9A", - "outputId": "792a3ad2-3d04-4132-d878-a5e52c58dc17" + "outputId": "e5d8657f-a644-4493-ca16-16a300ac4a87" }, "outputs": [], "source": [ @@ -414,36 +441,35 @@ { "cell_type": "markdown", "metadata": { - "id": "uByvwf9feG9A" + "id": "bvzJXSHHTi3v" }, "source": [ - "# ArangoDB to DGL\n", - "\n" + "# DGL to ArangoDB" ] }, { "cell_type": "markdown", "metadata": { - "id": "ZrEDmtqCVD0W" + "id": "UafSB_3JZNwK" }, "source": [ - "#### Via ArangoDB Graph" + "#### Karate Graph" ] }, { "cell_type": "markdown", "metadata": { - "id": "H8nlvWCryPW0" + "id": "tx-tjPfx0U_h" }, "source": [ - "Data source\n", - "* ArangoDB Fraud-Detection Graph\n", + "Data\n", + "* [DGL Karate Graph](https://docs.dgl.ai/en/0.6.x/api/python/dgl.data.html#karate-club-dataset)\n", "\n", - "Package methods used\n", - "* [`adbdgl_adapter.adapter.arangodb_graph_to_dgl()`](https://github.com/arangoml/dgl-adapter/blob/2.0.0/adbdgl_adapter/adapter.py#L198-L213)\n", + "API\n", + "* `adbdgl_adapter.adapter.dgl_to_arangodb()`\n", "\n", - "Important notes\n", - "* The `name` parameter in this case must point to an existing ArangoDB graph in your ArangoDB instance. " + "Notes\n", + "* The `name` parameter in this case is simply for naming your ArangoDB graph." ] }, { @@ -451,54 +477,70 @@ "execution_count": null, "metadata": { "colab": { - "base_uri": "https://localhost:8080/" + "base_uri": "https://localhost:8080/", + "height": 577, + "referenced_widgets": [ + "61d2a0426c324309ab51111933276e3d", + "77c208846c1e4503bc22a5b5504f89ee", + "2d1fc41d509e481cb779603827359184", + "87d9c9de620847f48b4088e8577cd653" + ] }, - "id": "zZ-Hu3lLVHgd", - "outputId": "d1c38c22-eebb-456d-8e4c-140ddd9baed8" + "id": "eRVbiBy4ZdE4", + "outputId": "74ac6cb8-824b-443a-ad6e-9f36b23060a1" }, "outputs": [], "source": [ - "# Define graph name\n", - "graph_name = \"fraud-detection\"\n", + "# Create the DGL graph & draw it\n", + "dgl_karate_graph = KarateClubDataset()[0]\n", + "nx.draw(dgl_karate_graph.to_networkx(), with_labels=True)\n", "\n", - "# Create DGL graph from ArangoDB graph\n", - "dgl_g = adbdgl_adapter.arangodb_graph_to_dgl(graph_name)\n", + "name = \"Karate\"\n", "\n", - "# You can also provide valid Python-Arango AQL query options to the command above, like such:\n", - "# dgl_g = aadbdgl_adapter.arangodb_graph_to_dgl(graph_name, ttl=1000, stream=True)\n", - "# See the full parameter list at https://docs.python-arango.com/en/main/specs.html#arango.aql.AQL.execute\n", + "# Delete the graph if it already exists\n", + "db.delete_graph(name, drop_collections=True, ignore_missing=True)\n", + "\n", + "# Create the ArangoDB graph\n", + "adb_karate_graph = adbdgl_adapter.dgl_to_arangodb(name, dgl_karate_graph)\n", + "\n", + "# You can also provide valid Python-Arango Import Bulk options to the command above, like such:\n", + "# adb_karate_graph = adbdgl_adapter.dgl_to_arangodb(name, dgl_karate_graph, batch_size=5, on_duplicate=\"replace\")\n", + "# See the full parameter list at https://docs.python-arango.com/en/main/specs.html#arango.collection.Collection.import_bulk\n", "\n", - "# Show graph data\n", "print('\\n--------------------')\n", - "print(dgl_g)\n", - "print(dgl_g.ntypes)\n", - "print(dgl_g.etypes)" + "print(\"URL: \" + con[\"url\"])\n", + "print(\"Username: \" + con[\"username\"])\n", + "print(\"Password: \" + con[\"password\"])\n", + "print(\"Database: \" + con[\"dbName\"])\n", + "print('--------------------\\n')\n", + "print(f\"View the created graph here: {con['url']}/_db/{con['dbName']}/_admin/aardvark/index.html#graph/{name}\\n\")\n", + "print(f\"View the original graph below:\\n\")" ] }, { "cell_type": "markdown", "metadata": { - "id": "RQ4CknYfUEuz" + "id": "CNj1xKhwoJoL" }, "source": [ - "#### Via ArangoDB Collections" + "\n", + "#### FakeHeterogeneous Graph" ] }, { "cell_type": "markdown", "metadata": { - "id": "bRcCmqWGy1Kf" + "id": "CZ1UX9YX1Zzo" }, "source": [ - "Data source\n", - "* ArangoDB Fraud-Detection Collections\n", + "Data\n", + "* A fake DGL Heterogeneous graph\n", "\n", - "Package methods used\n", - "* [`adbdgl_adapter.adapter.arangodb_collections_to_dgl()`](https://github.com/arangoml/dgl-adapter/blob/2.0.0/adbdgl_adapter/adapter.py#L169-L196)\n", + "API\n", + "* `adbdgl_adapter.adapter.dgl_to_arangodb()`\n", "\n", - "Important notes\n", - "* The `name` parameter in this case is simply for naming your DGL graph.\n", - "* The `vertex_collections` & `edge_collections` parameters must point to existing ArangoDB collections within your ArangoDB instance." + "Notes\n", + "* The `name` parameter is used to name your ArangoDB graph." ] }, { @@ -506,55 +548,84 @@ "execution_count": null, "metadata": { "colab": { - "base_uri": "https://localhost:8080/" + "base_uri": "https://localhost:8080/", + "height": 408, + "referenced_widgets": [ + "3fc8b14d794a46118b328893bd216405", + "c7e222474ff445fe86e4e599848b2ae2", + "289a6e16c3d640c29d96edf09908bd0f", + "61f3832c906445a3ab7e7ba9b41c0127", + "99bbe81a24db49ff9352987fd97649cd", + "21e50aa61c3d4de19b5cc0bbe27d53c9", + "f9fdfe6ce44e4e1c8f513f82efca3e0d", + "9b2b3abbe2c04af0bc232c9b16bfd90d", + "8444e147be8f44aba06ec1f8a880104e", + "80e69b3aa98b44e295efe3940c1146c2", + "ec7b8b0b853f463fa079dda845891391", + "dd2376f84c794b4989f385a5bb147bd8" + ] }, - "id": "i4XOpdRLUNlJ", - "outputId": "4d53a3d0-316b-40c2-d841-5fb29fa1358b" + "id": "jbJsvMMaoJoT", + "outputId": "c1606984-c2ef-41c1-e8b1-78a4ae40d93c" }, "outputs": [], "source": [ - "# Define collection names\n", - "vertex_collections = {\"account\", \"Class\", \"customer\"}\n", - "edge_collections = {\"accountHolder\", \"Relationship\", \"transaction\"}\n", + "# Create the DGL graph\n", + "hetero_graph = dgl.heterograph({\n", + " (\"user\", \"follows\", \"user\"): (torch.tensor([0, 1]), torch.tensor([1, 2])),\n", + " (\"user\", \"follows\", \"topic\"): (torch.tensor([1, 1]), torch.tensor([1, 2])),\n", + " (\"user\", \"plays\", \"game\"): (torch.tensor([0, 3]), torch.tensor([3, 4])),\n", + "})\n", + "hetero_graph.nodes[\"user\"].data[\"features\"] = torch.tensor([21, 44, 16, 25])\n", + "hetero_graph.nodes[\"user\"].data[\"label\"] = torch.tensor([1, 2, 0, 1])\n", + "hetero_graph.nodes[\"game\"].data[\"features\"] = torch.tensor([[0, 0], [0, 1], [1, 0], [1, 1], [1, 1]])\n", + "hetero_graph.edges[(\"user\", \"plays\", \"game\")].data[\"features\"] = torch.tensor([[6, 1], [1000, 0]])\n", "\n", - "# Create DGL from ArangoDB collections\n", - "dgl_g = adbdgl_adapter.arangodb_collections_to_dgl(\"fraud-detection\", vertex_collections, edge_collections)\n", + "print(hetero_graph)\n", "\n", - "# You can also provide valid Python-Arango AQL query options to the command above, like such:\n", - "# dgl_g = adbdgl_adapter.arangodb_collections_to_dgl(\"fraud-detection\", vertex_collections, edge_collections, ttl=1000, stream=True)\n", - "# See the full parameter list at https://docs.python-arango.com/en/main/specs.html#arango.aql.AQL.execute\n", + "name = \"FakeHetero\"\n", + "\n", + "# Delete the graph if it already exists\n", + "db.delete_graph(name, drop_collections=True, ignore_missing=True)\n", + "\n", + "# Create the ArangoDB graphs\n", + "adb_hetero_graph = adbdgl_adapter.dgl_to_arangodb(name, hetero_graph)\n", "\n", - "# Show graph data\n", "print('\\n--------------------')\n", - "print(dgl_g)\n", - "print(dgl_g.ntypes)\n", - "print(dgl_g.etypes)" + "print(\"URL: \" + con[\"url\"])\n", + "print(\"Username: \" + con[\"username\"])\n", + "print(\"Password: \" + con[\"password\"])\n", + "print(\"Database: \" + con[\"dbName\"])\n", + "print('--------------------\\n')\n", + "print(f\"View the created graph here: {con['url']}/_db/{con['dbName']}/_admin/aardvark/index.html#graph/{name}\\n\")\n", + "print(f\"View the original graph below:\\n\")" ] }, { "cell_type": "markdown", "metadata": { - "id": "qEH6OdSB23Ya" + "id": "n08RC_GtkDrC" }, "source": [ - "#### Via ArangoDB Metagraph" + "\n", + "#### FakeHeterogeneous Graph with a DGL-ArangoDB metagraph" ] }, { "cell_type": "markdown", "metadata": { - "id": "PipFzJ0HzTMA" + "id": "rUD_y0yxkDrK" }, "source": [ - "Data source\n", - "* ArangoDB Fraud-Detection Collections\n", + "Data\n", + "* A fake DGL Heterogeneous graph\n", "\n", - "Package methods used\n", - "* [`adbdgl_adapter.adapter.arangodb_to_dgl()`](https://github.com/arangoml/dgl-adapter/blob/2.0.0/adbdgl_adapter/adapter.py#L70-L167)\n", + "API\n", + "* `adbdgl_adapter.adapter.dgl_to_arangodb()`\n", "\n", - "Important notes\n", - "* The `name` parameter in this case is simply for naming your DGL graph.\n", - "* The `metagraph` parameter should contain collections & associated document attributes names that exist within your ArangoDB instance." + "Notes\n", + "* The `name` parameter is used to name your ArangoDB graph.\n", + "* The `metagraph` parameter is an optional object mapping the DGL keys of the node & edge data to strings, list of strings, or user-defined functions." ] }, { @@ -562,69 +633,128 @@ "execution_count": null, "metadata": { "colab": { - "base_uri": "https://localhost:8080/" + "base_uri": "https://localhost:8080/", + "height": 408, + "referenced_widgets": [ + "345a5984959c4e57b7e2715fa8eeef8f", + "99e6613c4187459396eea503453934cb", + "968020b1388e4883843575d9198af1cd", + "f1a08470110e4099af2a3d4cf4d0f956", + "6744eb60dfa04a8598fca3b998ce3077", + "09d25097c75c4fa8a2c7376f1965afc5", + "cb8167f00277413eaaa2ad6e0e162fab", + "8128e6d80fcb4a8ca0a72097bb8b6521", + "575205f1a4e64c5d977e69d4939a5605", + "d20843bfa9064d56b37aaea011789a26", + "8bf075c6f7834d3fa905b7ddc37cf128", + "b080f26fe35241fb9cca48e97bc9ef0c" + ] }, - "id": "7Kz8lXXq23Yk", - "outputId": "7804e7ba-3760-4eb5-8669-f6fa20948262" + "id": "xAdjZiJ8kDrK", + "outputId": "2822ed4b-8199-48e2-a753-4b1f60d648a0" }, "outputs": [], "source": [ - "# Define Metagraph\n", - "fraud_detection_metagraph = {\n", - " \"vertexCollections\": {\n", - " \"account\": {\"rank\", \"Balance\", \"customer_id\"},\n", - " \"Class\": {\"concrete\"},\n", - " \"customer\": {\"rank\"},\n", + "# Create the DGL graph\n", + "hetero_graph = dgl.heterograph({\n", + " (\"user\", \"follows\", \"user\"): (torch.tensor([0, 1]), torch.tensor([1, 2])),\n", + " (\"user\", \"follows\", \"topic\"): (torch.tensor([1, 1]), torch.tensor([1, 2])),\n", + " (\"user\", \"plays\", \"game\"): (torch.tensor([0, 3]), torch.tensor([3, 4])),\n", + "})\n", + "hetero_graph.nodes[\"user\"].data[\"features\"] = torch.tensor([21, 44, 16, 25])\n", + "hetero_graph.nodes[\"user\"].data[\"label\"] = torch.tensor([1, 2, 0, 1])\n", + "hetero_graph.nodes[\"game\"].data[\"features\"] = torch.tensor([[0, 0], [0, 1], [1, 0], [1, 1], [1, 1]])\n", + "hetero_graph.edges[(\"user\", \"plays\", \"game\")].data[\"features\"] = torch.tensor([[6, 1], [1000, 0]])\n", + "\n", + "print(hetero_graph)\n", + "\n", + "name = \"FakeHetero\"\n", + "\n", + "# Define the metagraph\n", + "def label_tensor_to_2_column_dataframe(dgl_tensor, adb_df):\n", + " \"\"\"\n", + " A user-defined function to create two\n", + " ArangoDB attributes out of the 'user' label tensor\n", + "\n", + " :param dgl_tensor: The DGL Tensor containing the data\n", + " :type dgl_tensor: torch.Tensor\n", + " :param adb_df: The ArangoDB DataFrame to populate, whose\n", + " size is preset to the length of **dgl_tensor**.\n", + " :type adb_df: pandas.DataFrame\n", + "\n", + " NOTE: user-defined functions must return the modified **adb_df**\n", + " \"\"\"\n", + " label_map = {0: \"Class A\", 1: \"Class B\", 2: \"Class C\"}\n", + "\n", + " adb_df[\"label_num\"] = dgl_tensor.tolist()\n", + " adb_df[\"label_str\"] = adb_df[\"label_num\"].map(label_map)\n", + "\n", + " return adb_df\n", + "\n", + "\n", + "metagraph = {\n", + " \"nodeTypes\": {\n", + " \"user\": {\n", + " \"features\": \"user_age\", # 1) you can specify a string value for attribute renaming\n", + " \"label\": label_tensor_to_2_column_dataframe, # 2) you can specify a function for user-defined handling, as long as the function returns a Pandas DataFrame\n", + " },\n", + " # 3) You can specify set of strings if you want to preserve the same DGL attribute names for the node/edge type\n", + " \"game\": {\"features\"} # this is equivalent to {\"features\": \"features\"}\n", " },\n", - " \"edgeCollections\": {\n", - " \"accountHolder\": {},\n", - " \"Relationship\": {},\n", - " \"transaction\": {\"receiver_bank_id\", \"sender_bank_id\", \"transaction_amt\"},\n", + " \"edgeTypes\": {\n", + " (\"user\", \"plays\", \"game\"): {\n", + " # 4) you can specify a list of strings for tensor dissasembly (if you know the number of node/edge features in advance)\n", + " \"features\": [\"hours_played\", \"is_satisfied_with_game\"]\n", + " },\n", " },\n", "}\n", "\n", - "# Create DGL Graph from attributes\n", - "dgl_g = adbdgl_adapter.arangodb_to_dgl('FraudDetection', fraud_detection_metagraph)\n", + "# Delete the graph if it already exists\n", + "db.delete_graph(name, drop_collections=True, ignore_missing=True)\n", "\n", - "# You can also provide valid Python-Arango AQL query options to the command above, like such:\n", - "# dgl_g = adbdgl_adapter.arangodb_to_dgl(graph_name = 'FraudDetection', fraud_detection_metagraph, ttl=1000, stream=True)\n", - "# See the full parameter list at https://docs.python-arango.com/en/main/specs.html#arango.aql.AQL.execute\n", + "# Create the ArangoDB graphs\n", + "adb_hetero_graph = adbdgl_adapter.dgl_to_arangodb(name, hetero_graph, metagraph, explicit_metagraph=False)\n", "\n", - "# Show graph data\n", - "print('\\n--------------')\n", - "print(dgl_g)\n", - "print('\\n--------------')\n", - "print(dgl_g.ndata)\n", - "print('--------------\\n')\n", - "print(dgl_g.edata)" + "# Create the ArangoDB graph with `explicit_metagraph=True`\n", + "# With `explicit_metagraph=True`, the node & edge types omitted from the metagraph will NOT be converted to ArangoDB.\n", + "# Only 'user', 'game', and ('user', 'plays', 'game') will be brought over (i.e 'topic', ('user', 'follows', 'user'), ... are ignored)\n", + "## adb_hetero_graph = adbdgl_adapter.dgl_to_arangodb(name, hetero_graph, metagraph, explicit_metagraph=True)\n", + "\n", + "print('\\n--------------------')\n", + "print(\"URL: \" + con[\"url\"])\n", + "print(\"Username: \" + con[\"username\"])\n", + "print(\"Password: \" + con[\"password\"])\n", + "print(\"Database: \" + con[\"dbName\"])\n", + "print('--------------------\\n')\n", + "print(f\"View the created graph here: {con['url']}/_db/{con['dbName']}/_admin/aardvark/index.html#graph/{name}\\n\")\n", + "print(f\"View the original graph below:\\n\")" ] }, { "cell_type": "markdown", "metadata": { - "id": "DqIKT1lO4ASw" + "id": "mk6m0hBRkkkT" }, "source": [ - "#### Via ArangoDB Metagraph with a custom controller and verbose logging" + "\n", + "#### FakeHeterogeneous Graph with a user-defined ADBDGL Controller" ] }, { "cell_type": "markdown", "metadata": { - "id": "PGkGh_KjzlYM" + "id": "KG7kFoOUkkkb" }, "source": [ - "Data source\n", - "* ArangoDB Fraud-Detection Collections\n", + "Data\n", + "* A fake DGL Heterogeneous graph\n", "\n", - "Package methods used\n", - "* [`adbdgl_adapter.adapter.arangodb_to_dgl()`](https://github.com/arangoml/dgl-adapter/blob/2.0.0/adbdgl_adapter/adapter.py#L70-L167)\n", - "* [`adbdgl_adapter.controller._adb_attribute_to_dgl_feature()`](https://github.com/arangoml/dgl-adapter/blob/2.0.0/adbdgl_adapter/controller.py#L21-L47)\n", + "API\n", + "* `adbdgl_adapter.adapter.dgl_to_arangodb()`\n", "\n", - "Important notes\n", - "* The `name` parameter in this case is simply for naming your DGL graph.\n", - "* The `metagraph` parameter should contain collections & associated document attributes names that exist within your ArangoDB instance.\n", - "* We are creating a custom `ADBDGL_Controller` to specify *how* to convert our ArangoDB vertex/edge attributes into DGL node/edge features. View the default `ADBDGL_Controller` [here](https://github.com/arangoml/dgl-adapter/blob/2.0.0/adbdgl_adapter/controller.py#L11)." + "Notes\n", + "* The `name` parameter is used to name your ArangoDB graph.\n", + "* The `ADBDGL_Controller` is an optional user-defined class for controlling how nodes & edges are handled when transitioning from DGL to ArangoDB. **It is interpreted as the alternative to the `metagraph` parameter.**" ] }, { @@ -632,143 +762,158 @@ "execution_count": null, "metadata": { "colab": { - "base_uri": "https://localhost:8080/" + "base_uri": "https://localhost:8080/", + "height": 443, + "referenced_widgets": [ + "ea5e9803c5de4d2bbb48782069b9829b", + "3f633be94c7d466ea40571e805a76948", + "96e57d98afce44cd8269204dd19ff6e0", + "da43ef4a8c6a41f9bda153a0cd14c2d7", + "3bc228aa98454dc59a604c8f7ff6b2a0", + "65138d18c9c449d1aaaad387293c5ede", + "3ea99b2a6b4246d3abf628ca743f9f24", + "841ce4f5d391457e858c3c48185e259d", + "987bf80aee4b4b97bfad1699f8384af8", + "4ab3c113235746cab5fde158756ab420", + "09e8c93741bf45acb69ba9e757107564", + "d7d06973b2984eb19fa050409bf62222" + ] }, - "id": "U4_vSdU_4AS4", - "outputId": "8af82665-9ae6-40d4-ada2-248edd993291" + "id": "A-DtrD2Ykkkb", + "outputId": "f2672554-16e4-4b88-e24b-f567ff13bb3f" }, "outputs": [], "source": [ - "# Define Metagraph\n", - "fraud_detection_metagraph = {\n", - " \"vertexCollections\": {\n", - " \"account\": {\"rank\"},\n", - " \"Class\": {\"concrete\", \"name\"},\n", - " \"customer\": {\"Sex\", \"Ssn\", \"rank\"},\n", - " },\n", - " \"edgeCollections\": {\n", - " \"accountHolder\": {},\n", - " \"Relationship\": {},\n", - " \"transaction\": {\"receiver_bank_id\", \"sender_bank_id\", \"transaction_amt\", \"transaction_date\", \"trans_time\"},\n", - " },\n", - "}\n", + "# Create the DGL graph\n", + "hetero_graph = dgl.heterograph({\n", + " (\"user\", \"follows\", \"user\"): (torch.tensor([0, 1]), torch.tensor([1, 2])),\n", + " (\"user\", \"follows\", \"topic\"): (torch.tensor([1, 1]), torch.tensor([1, 2])),\n", + " (\"user\", \"plays\", \"game\"): (torch.tensor([0, 3]), torch.tensor([3, 4])),\n", + "})\n", + "hetero_graph.nodes[\"user\"].data[\"features\"] = torch.tensor([21, 44, 16, 25])\n", + "hetero_graph.nodes[\"user\"].data[\"label\"] = torch.tensor([1, 2, 0, 1])\n", + "hetero_graph.nodes[\"game\"].data[\"features\"] = torch.tensor([[0, 0], [0, 1], [1, 0], [1, 1], [1, 1]])\n", + "hetero_graph.edges[(\"user\", \"plays\", \"game\")].data[\"features\"] = torch.tensor([[6, 1], [1000, 0]])\n", "\n", - "# A user-defined Controller class is REQUIRED when converting non-numerical\n", - "# ArangoDB attributes to DGL features.\n", - "class FraudDetection_ADBDGL_Controller(ADBDGL_Controller):\n", - " \"\"\"ArangoDB-DGL controller.\n", + "print(hetero_graph)\n", "\n", - " Responsible for controlling how ArangoDB attributes\n", - " are converted into DGL features, and vice-versa.\n", + "name = \"FakeHetero\"\n", "\n", - " You can derive your own custom ADBDGL_Controller if you want to maintain\n", - " consistency between your ArangoDB attributes & your DGL features.\n", - " \"\"\"\n", + "# Create a custom ADBDGL_Controller\n", + "class Custom_ADBDGL_Controller(ADBDGL_Controller):\n", + " def _prepare_dgl_node(self, dgl_node: dict, node_type: str) -> dict:\n", + " \"\"\"Optionally modify a DGL node object before it gets inserted into its designated ArangoDB collection.\n", "\n", - " def _adb_attribute_to_dgl_feature(self, key: str, col: str, val):\n", + " :param dgl_node: The DGL node object to (optionally) modify.\n", + " :param node_type: The DGL Node Type of the node.\n", + " :return: The DGL Node object\n", " \"\"\"\n", - " Given an ArangoDB attribute key, its assigned value (for an arbitrary document),\n", - " and the collection it belongs to, convert it to a valid\n", - " DGL feature: https://docs.dgl.ai/en/0.6.x/guide/graph-feature.html.\n", - "\n", - " NOTE: You must override this function if you want to transfer non-numerical\n", - " ArangoDB attributes to DGL (DGL only accepts 'attributes' (a.k.a features)\n", - " of numerical types). Read more about DGL features here:\n", - " https://docs.dgl.ai/en/0.6.x/new-tutorial/2_dglgraph.html#assigning-node-and-edge-features-to-graph.\n", - "\n", - " :param key: The ArangoDB attribute key name\n", - " :type key: str\n", - " :param col: The ArangoDB collection of the ArangoDB document.\n", - " :type col: str\n", - " :param val: The assigned attribute value of the ArangoDB document.\n", - " :type val: Any\n", - " :return: The attribute's representation as a DGL Feature\n", - " :rtype: Any\n", + " dgl_node[\"foo\"] = \"bar\"\n", + " return dgl_node\n", + "\n", + " def _prepare_dgl_edge(self, dgl_edge: dict, edge_type: tuple) -> dict:\n", + " \"\"\"Optionally modify a DGL edge object before it gets inserted into its designated ArangoDB collection.\n", + "\n", + " :param dgl_edge: The DGL edge object to (optionally) modify.\n", + " :param edge_type: The Edge Type of the DGL edge. Formatted\n", + " as (from_collection, edge_collection, to_collection)\n", + " :return: The DGL Edge object\n", " \"\"\"\n", - " try:\n", - " if col == \"transaction\":\n", - " if key == \"transaction_date\":\n", - " return int(str(val).replace(\"-\", \"\"))\n", - " \n", - " if key == \"trans_time\":\n", - " return int(str(val).replace(\":\", \"\"))\n", - " \n", - " if col == \"customer\":\n", - " if key == \"Sex\":\n", - " return {\n", - " \"M\": 0,\n", - " \"F\": 1\n", - " }.get(val, -1)\n", - "\n", - " if key == \"Ssn\":\n", - " return int(str(val).replace(\"-\", \"\"))\n", - "\n", - " if col == \"Class\":\n", - " if key == \"name\":\n", - " return {\n", - " \"Bank\": 0,\n", - " \"Branch\": 1,\n", - " \"Account\": 2,\n", - " \"Customer\": 3\n", - " }.get(val, -1)\n", - "\n", - " except (ValueError, TypeError, SyntaxError):\n", - " return 0\n", - "\n", - " # Rely on the parent Controller as a final measure\n", - " return super()._adb_attribute_to_dgl_feature(key, col, val)\n", - "\n", - "# Instantiate the new adapter\n", - "fraud_adbdgl_adapter = ADBDGL_Adapter(db, FraudDetection_ADBDGL_Controller())\n", - "\n", - "# You can also change the adapter's logging level for access to \n", - "# silent, regular, or verbose logging (logging.WARNING, logging.INFO, logging.DEBUG)\n", - "fraud_adbdgl_adapter.set_logging(logging.DEBUG) # verbose logging\n", - "\n", - "# Create DGL Graph from attributes\n", - "dgl_g = fraud_adbdgl_adapter.arangodb_to_dgl('FraudDetection', fraud_detection_metagraph)\n", + " dgl_edge[\"bar\"] = \"foo\"\n", + " return dgl_edge\n", "\n", - "# Show graph data\n", - "print('\\n--------------')\n", - "print(dgl_g)\n", - "print('\\n--------------')\n", - "print(dgl_g.ndata)\n", - "print('--------------\\n')\n", - "print(dgl_g.edata)" + "# Delete the graph if it already exists\n", + "db.delete_graph(name, drop_collections=True, ignore_missing=True)\n", + "\n", + "# Create the ArangoDB graphs\n", + "adb_g = ADBDGL_Adapter(db, Custom_ADBDGL_Controller()).dgl_to_arangodb(name, hetero_graph)\n", + "\n", + "print('\\n--------------------')\n", + "print(\"URL: \" + con[\"url\"])\n", + "print(\"Username: \" + con[\"username\"])\n", + "print(\"Password: \" + con[\"password\"])\n", + "print(\"Database: \" + con[\"dbName\"])\n", + "print('--------------------\\n')\n", + "print(f\"View the created graph here: {con['url']}/_db/{con['dbName']}/_admin/aardvark/index.html#graph/{name}\\n\")\n", + "print(f\"View the original graph below:\\n\")" ] }, { "cell_type": "markdown", "metadata": { - "id": "bvzJXSHHTi3v" + "id": "uByvwf9feG9A" }, "source": [ - "# DGL to ArangoDB" + "# ArangoDB to DGL\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 165, + "referenced_widgets": [ + "c6cffa0a64434e56879ba2a8c9de018a", + "0083494093574c50952dd066502a708d", + "1dea128bde204a8fa53e094e014183fe", + "50f8ff3637ee4fc7af8c811cd5d177be", + "6582a9d3fe044d5380d8e918f3bc5a6d", + "40da9dd52dd6443684b990f74b6cb876", + "80d19dc0d20842c3b5c7313c0ad23d24", + "0478c90ef8234f3a8987dbe9cd3030b2", + "c61e3997250d4f93a8e0494db674892d", + "97e7543f202749c197515a9c5c79adbe", + "88e83ddc1ca1464291e1631b8fced847", + "a9c14a3f339445338119631c8e56ff68" + ] + }, + "id": "rnMe3iMz2K7j", + "outputId": "b1485ec1-64bf-43d5-a5fe-7d6bd5fc2da1" + }, + "outputs": [], + "source": [ + "# Start from scratch! (with the same DGL graph)\n", + "hetero_graph = dgl.heterograph({\n", + " (\"user\", \"follows\", \"user\"): (torch.tensor([0, 1]), torch.tensor([1, 2])),\n", + " (\"user\", \"follows\", \"topic\"): (torch.tensor([1, 1]), torch.tensor([1, 2])),\n", + " (\"user\", \"plays\", \"game\"): (torch.tensor([0, 3]), torch.tensor([3, 4])),\n", + "})\n", + "hetero_graph.nodes[\"user\"].data[\"features\"] = torch.tensor([21, 44, 16, 25])\n", + "hetero_graph.nodes[\"user\"].data[\"label\"] = torch.tensor([1, 2, 0, 1])\n", + "hetero_graph.nodes[\"game\"].data[\"features\"] = torch.tensor([[0, 0], [0, 1], [1, 0], [1, 1], [1, 1]])\n", + "hetero_graph.edges[(\"user\", \"plays\", \"game\")].data[\"features\"] = torch.tensor([[6, 1], [1000, 0]])\n", + "\n", + "db.delete_graph(\"FakeHetero\", drop_collections=True, ignore_missing=True)\n", + "adbdgl_adapter.dgl_to_arangodb(\"FakeHetero\", hetero_graph)" ] }, { "cell_type": "markdown", "metadata": { - "id": "UafSB_3JZNwK" + "id": "ZrEDmtqCVD0W" }, "source": [ - "#### Karate Graph" + "#### Via ArangoDB Graph" ] }, { "cell_type": "markdown", "metadata": { - "id": "tx-tjPfx0U_h" + "id": "H8nlvWCryPW0" }, "source": [ - "Data source\n", - "* [DGL Karate Graph](https://docs.dgl.ai/en/0.6.x/api/python/dgl.data.html#karate-club-dataset)\n", + "Data\n", + "* A fake DGL Heterogeneous graph\n", "\n", - "Package methods used\n", - "* [`adbdgl_adapter.adapter.dgl_to_arangodb()`](https://github.com/arangoml/dgl-adapter/blob/2.0.0/adbdgl_adapter/adapter.py#L215-L311)\n", + "API\n", + "* `adbdgl_adapter.adapter.arangodb_graph_to_dgl()`\n", "\n", - "Important notes\n", - "* The `name` parameter in this case is simply for naming your ArangoDB graph." + "Notes\n", + "* The `name` parameter in this case must point to an existing ArangoDB graph in your ArangoDB instance. \n", + "* Due to risk of ambiguity, this method does **not** carry over ArangoDB attributes to DGL." ] }, { @@ -777,63 +922,67 @@ "metadata": { "colab": { "base_uri": "https://localhost:8080/", - "height": 683 + "height": 184, + "referenced_widgets": [ + "9403e71c2bbe46bd9e6d49d555264554", + "34c4ef0c4aa5454893c0f0fa35902fbd", + "1690574b32cc4b48a8b87520458d5066", + "a9edf4f85a4a4504b155608bb740178a", + "fd2db543279f4a13ab6376b9c23160e0", + "5c310145af4f4c90b659dee771185ab6", + "31a9f782f36d407f8cc42b19679c5c2c", + "9fd8d07a43cd4c06a2d448047ede846c", + "2c2900512b5244d3a0fcaf7409446d0e", + "c5d064af7f4a49dca6716f98d052e951" + ] }, - "id": "eRVbiBy4ZdE4", - "outputId": "c629be2d-1bc9-4539-c7f2-d3ae46676659" + "id": "zZ-Hu3lLVHgd", + "outputId": "85729665-feb3-4382-e84b-4286162581c3" }, "outputs": [], "source": [ - "# Create the DGL graph & draw it\n", - "dgl_karate_graph = KarateClubDataset()[0]\n", - "nx.draw(dgl_karate_graph.to_networkx(), with_labels=True)\n", - "\n", - "name = \"Karate\"\n", + "# Define graph name\n", + "name = \"FakeHetero\"\n", "\n", - "# Delete the graph if it already exists\n", - "db.delete_graph(name, drop_collections=True, ignore_missing=True)\n", + "# Create the DGL Graph from the ArangoDB graph\n", + "dgl_g = adbdgl_adapter.arangodb_graph_to_dgl(name)\n", "\n", - "# Create the ArangoDB graph\n", - "adb_karate_graph = adbdgl_adapter.dgl_to_arangodb(name, dgl_karate_graph)\n", - "\n", - "# You can also provide valid Python-Arango Import Bulk options to the command above, like such:\n", - "# adb_karate_graph = adbdgl_adapter.dgl_to_arangodb(name, dgl_karate_graph, batch_size=5, on_duplicate=\"replace\")\n", - "# See the full parameter list at https://docs.python-arango.com/en/main/specs.html#arango.collection.Collection.import_bulk\n", + "# You can also provide valid Python-Arango AQL query options to the command above, like such:\n", + "# dgl_g = adbdgl_adapter.arangodb_graph_to_dgl(graph_name, ttl=1000, stream=True)\n", + "# See the full parameter list at https://docs.python-arango.com/en/main/specs.html#arango.aql.AQL.execute\n", "\n", + "# Show graph data\n", "print('\\n--------------------')\n", - "print(\"URL: \" + con[\"url\"])\n", - "print(\"Username: \" + con[\"username\"])\n", - "print(\"Password: \" + con[\"password\"])\n", - "print(\"Database: \" + con[\"dbName\"])\n", - "print('--------------------\\n')\n", - "print(f\"View the created graph here: {con['url']}/_db/{con['dbName']}/_admin/aardvark/index.html#graph/{name}\\n\")\n", - "print(f\"View the original graph below:\\n\")" + "print(dgl_g)\n", + "print(dgl_g.ndata) # note how this is empty\n", + "print(dgl_g.edata) # note how this is empty" ] }, { "cell_type": "markdown", "metadata": { - "id": "gshTlSX_ZZsS" + "id": "RQ4CknYfUEuz" }, "source": [ - "\n", - "#### MiniGCDataset Graphs" + "#### Via ArangoDB Collections" ] }, { "cell_type": "markdown", "metadata": { - "id": "KaExiE2x0-M6" + "id": "bRcCmqWGy1Kf" }, "source": [ - "Data source\n", - "* [DGL Mini Graph Classification Dataset](https://docs.dgl.ai/en/0.6.x/api/python/dgl.data.html#mini-graph-classification-dataset)\n", + "Data\n", + "* A fake DGL Heterogeneous graph\n", "\n", - "Package methods used\n", - "* [`adbdgl_adapter.adapter.dgl_to_arangodb()`](https://github.com/arangoml/dgl-adapter/blob/2.0.0/adbdgl_adapter/adapter.py#L215-L311)\n", + "API\n", + "* `adbdgl_adapter.adapter.arangodb_collections_to_dgl()`\n", "\n", - "Important notes\n", - "* The `name` parameters in this case are simply for naming your ArangoDB graph." + "Notes\n", + "* The `name` parameter is purely for documentation purposes in this case.\n", + "* The `vertex_collections` & `edge_collections` parameters must point to existing ArangoDB collections within your ArangoDB instance.\n", + "* Due to risk of ambiguity, this method does **not** carry over ArangoDB attributes to DGL." ] }, { @@ -842,82 +991,64 @@ "metadata": { "colab": { "base_uri": "https://localhost:8080/", - "height": 1000 + "height": 253, + "referenced_widgets": [ + "f01997b9b43d43368d632e26ba9732ad", + "14b29dc1f2b8454fa9acc1d79dcd4870", + "5f5c119141a24cab907ceb2da27e0244", + "46b88027e41a43578ebcc47513dd6911", + "7a43c4b816da4a40b0eed167a85eef22", + "eb376d5cf782424aaccbce31f0d3ede5", + "7a4db2b18c634bef932fb9b1157d4af1", + "b5be8c1e4ab3415c9fffbb61aeb0fff3", + "4e085418ce1b41e1bc24ad6acea92fc4", + "7b5dba3f4d50466eb2071cb13548ef1b" + ] }, - "id": "dADiexlAioGH", - "outputId": "9921ec34-b860-49e8-f8cb-0b403029ead4" + "id": "i4XOpdRLUNlJ", + "outputId": "c0fa5973-3e46-4227-8b0c-48b4f14736e5" }, "outputs": [], "source": [ - "# Load the dgl graphs & draw:\n", - "## 1) Lollipop Graph\n", - "dgl_lollipop_graph = remove_self_loop(MiniGCDataset(8, 7, 8)[3][0])\n", - "plt.figure(1)\n", - "nx.draw(dgl_lollipop_graph.to_networkx(), with_labels=True)\n", - "\n", - "## 2) Hypercube Graph\n", - "dgl_hypercube_graph = remove_self_loop(MiniGCDataset(8, 8, 9)[4][0])\n", - "plt.figure(2)\n", - "nx.draw(dgl_hypercube_graph.to_networkx(), with_labels=True)\n", - "\n", - "## 3) Clique Graph\n", - "dgl_clique_graph = remove_self_loop(MiniGCDataset(8, 6, 7)[6][0])\n", - "plt.figure(3)\n", - "nx.draw(dgl_clique_graph.to_networkx(), with_labels=True)\n", - "\n", - "lollipop = \"Lollipop\"\n", - "hypercube = \"Hypercube\"\n", - "clique = \"Clique\"\n", - "\n", - "# Delete the graphs from ArangoDB if they already exist\n", - "db.delete_graph(lollipop, drop_collections=True, ignore_missing=True)\n", - "db.delete_graph(hypercube, drop_collections=True, ignore_missing=True)\n", - "db.delete_graph(clique, drop_collections=True, ignore_missing=True)\n", + "name = \"FakeHetero\"\n", "\n", - "# Create the ArangoDB graphs\n", - "adb_lollipop_graph = adbdgl_adapter.dgl_to_arangodb(lollipop, dgl_lollipop_graph)\n", - "adb_hypercube_graph = adbdgl_adapter.dgl_to_arangodb(hypercube, dgl_hypercube_graph)\n", - "adb_clique_graph = adbdgl_adapter.dgl_to_arangodb(clique, dgl_clique_graph)\n", + "dgl_g = adbdgl_adapter.arangodb_collections_to_dgl(\n", + " name, \n", + " v_cols={\"user\", \"game\"},\n", + " e_cols={\"plays\", \"follows\"}\n", + ")\n", "\n", + "# Show graph data (notice that the \"topic\" data is skipped)\n", "print('\\n--------------------')\n", - "print(\"URL: \" + con[\"url\"])\n", - "print(\"Username: \" + con[\"username\"])\n", - "print(\"Password: \" + con[\"password\"])\n", - "print(\"Database: \" + con[\"dbName\"])\n", - "print('--------------------\\n')\n", - "print(\"View the created graphs here:\\n\")\n", - "print(f\"1) {con['url']}/_db/{con['dbName']}/_admin/aardvark/index.html#graph/{lollipop}\")\n", - "print(f\"2) {con['url']}/_db/{con['dbName']}/_admin/aardvark/index.html#graph/{hypercube}\")\n", - "print(f\"3) {con['url']}/_db/{con['dbName']}/_admin/aardvark/index.html#graph/{clique}\\n\")\n", - "print(f\"View the original graphs below:\\n\")" + "print(dgl_g)\n", + "print(dgl_g.ndata) # note how this is empty\n", + "print(dgl_g.edata) # note how this is empty" ] }, { "cell_type": "markdown", "metadata": { - "id": "CNj1xKhwoJoL" + "id": "qEH6OdSB23Ya" }, "source": [ - "\n", - "#### MiniGCDataset Graphs with attributes" + "#### Via ArangoDB-DGL metagraph 1" ] }, { "cell_type": "markdown", "metadata": { - "id": "CZ1UX9YX1Zzo" + "id": "PipFzJ0HzTMA" }, "source": [ - "Data source\n", - "* [DGL Mini Graph Classification Dataset](https://docs.dgl.ai/en/0.6.x/api/python/dgl.data.html#mini-graph-classification-dataset)\n", + "Data\n", + "* A fake DGL Heterogeneous graph\n", "\n", - "Package methods used\n", - "* [`adbdgl_adapter.adapter.dgl_to_arangodb()`](https://github.com/arangoml/dgl-adapter/blob/2.0.0/adbdgl_adapter/adapter.py#L215-L311)\n", - "* [`adbdgl_adapter.controller._dgl_feature_to_adb_attribute()`](https://github.com/arangoml/dgl-adapter/blob/2.0.0/adbdgl_adapter/controller.py#L49-L70)\n", + "API\n", + "* `adbdgl_adapter.adapter.arangodb_to_dgl()`\n", "\n", - "Important notes\n", - "* The `name` parameters in this case are simply for naming your ArangoDB graph.\n", - "* We are creating a custom `ADBDGL_Controller` to specify *how* to convert our DGL node/edge features into ArangoDB vertex/edge attributes. View the default `ADBDGL_Controller` [here](https://github.com/arangoml/dgl-adapter/blob/2.0.0/adbdgl_adapter/controller.py#L11)." + "Notes\n", + "* The `name` parameter is purely for documentation purposes in this case.\n", + "* The `metagraph` parameter is an object defining vertex & edge collections to import to DGL, along with collection-level specifications to indicate which ArangoDB attributes will become DGL features/labels. It should contain collections & associated document attributes names that exist within your ArangoDB instance." ] }, { @@ -925,118 +1056,238 @@ "execution_count": null, "metadata": { "colab": { - "base_uri": "https://localhost:8080/" + "base_uri": "https://localhost:8080/", + "height": 409, + "referenced_widgets": [ + "77b31c42e914410aaea93044f1390121", + "8349f1e6b1f34680bacd7de1a1937122", + "38aaa492d75c48f38de60ea0cc5fa93f", + "63845b04ecbc40de8bcc017d754ac907", + "4b7f5f21b98b4c5d8475929bf1f01a65", + "404a19cadaca4b85a957cad231b73cbb", + "bd8b6caa7d2d4df1a99b1870ecc0ae46", + "13d0f7da120b40b993ce3c0b257d5788", + "ea88ab86e9774ed78ea62daa6e338637", + "712770e675424d7eb0c8efd6c34f2012" + ] }, - "id": "jbJsvMMaoJoT", - "outputId": "6dba7563-84b8-4934-a07f-1525ef67bd5e" + "id": "7Kz8lXXq23Yk", + "outputId": "b17433d7-d344-4748-ffe3-f0abca6fb112" }, "outputs": [], "source": [ - "# Load the dgl graphs\n", - "dgl_lollipop_graph = remove_self_loop(MiniGCDataset(8, 7, 8)[3][0])\n", - "dgl_hypercube_graph = remove_self_loop(MiniGCDataset(8, 8, 9)[4][0])\n", - "dgl_clique_graph = remove_self_loop(MiniGCDataset(8, 6, 7)[6][0])\n", - "\n", - " # Add DGL Node & Edge Features to each graph\n", - "dgl_lollipop_graph.ndata[\"random_ndata\"] = torch.tensor(\n", - " [[i, i, i] for i in range(0, dgl_lollipop_graph.num_nodes())]\n", - ")\n", - "dgl_lollipop_graph.edata[\"random_edata\"] = torch.rand(dgl_lollipop_graph.num_edges())\n", + "# Define the Metagraph that transfers ArangoDB attributes \"as is\",\n", + "# meaning the data is already formatted to DGL data standards\n", + "metagraph_v1 = {\n", + " \"vertexCollections\": {\n", + " # Move the \"features\" & \"label\" ArangoDB attributes to DGL as \"features\" & \"label\" Tensors\n", + " \"user\": {\"features\", \"label\"}, # equivalent to {\"features\": \"features\", \"label\": \"label\"}\n", + " \"game\": {\"dgl_game_features\": \"features\"},\n", + " \"topic\": {},\n", + " },\n", + " \"edgeCollections\": {\n", + " \"plays\": {\"dgl_plays_features\": \"features\"}, \n", + " \"follows\": {}\n", + " },\n", + "}\n", "\n", - "dgl_hypercube_graph.ndata[\"random_ndata\"] = torch.rand(dgl_hypercube_graph.num_nodes())\n", - "dgl_hypercube_graph.edata[\"random_edata\"] = torch.tensor(\n", - " [[[i], [i], [i]] for i in range(0, dgl_hypercube_graph.num_edges())]\n", - ")\n", + "# Create the DGL graph\n", + "dgl_g = adbdgl_adapter.arangodb_to_dgl(\"FakeHetero\", metagraph_v1)\n", "\n", - "dgl_clique_graph.ndata['clique_ndata'] = torch.tensor([1,2,3,4,5,6])\n", - "dgl_clique_graph.edata['clique_edata'] = torch.tensor(\n", - " [1 if i % 2 == 0 else 0 for i in range(0, dgl_clique_graph.num_edges())]\n", - ")\n", + "# Show graph data\n", + "print('\\n--------------')\n", + "print(dgl_g)\n", + "print('\\n--------------')\n", + "print(dgl_g.ndata)\n", + "print('--------------\\n')\n", + "print(dgl_g.edata)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0806IB4o3WRz" + }, + "source": [ + "#### Via ArangoDB-DGL metagraph 2" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "cnByWtpa3WR7" + }, + "source": [ + "Data\n", + "* [ArangoDB IMDB Movie Dataset](https://www.arangodb.com/docs/stable/arangosearch-example-datasets.html#imdb-movie-dataset)\n", "\n", - "# A user-defined Controller class is OPTIONAL when converting DGL features\n", - "# to ArangoDB attributes. NOTE: A custom Controller is NOT needed if you want to\n", - "# keep the numerical-based values of your DGL features.\n", - "class Clique_ADBDGL_Controller(ADBDGL_Controller):\n", - " \"\"\"ArangoDB-DGL controller.\n", + "API\n", + "* `adbddgl_adapter.adapter.arangodb_to_dgl()`\n", "\n", - " Responsible for controlling how ArangoDB attributes\n", - " are converted into DGL features, and vice-versa.\n", + "Notes\n", + "* The `name` parameter is purely for documentation purposes in this case.\n", + "* The `metagraph` parameter is an object defining vertex & edge collections to import to DGL, along with collection-level specifications to indicate which ArangoDB attributes will become DGL features/labels. In this example, we rely on user-defined encoders to build DGL-ready tensors (i.e feature matrices) from ArangoDB attributes. See https://pytorch-geometric.readthedocs.io/en/latest/notes/load_csv.html for an example on using encoders." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 499, + "referenced_widgets": [ + "2b13e46a722e4be384fad74e1b3e6461", + "848230df62434c77b5b18f9a43e2d14f", + "59405e2d0c164d5b965680cc9d9cd8f3", + "2a380fe111794c3a951cdafa4a2bf0b3", + "3d081c88cd2945fa9534de722669ada9", + "82f996185e8444ada5e18602e2f8e105" + ] + }, + "id": "cKqLoawE3WR7", + "outputId": "02a8bfed-44ae-4c76-9eea-ba7348738707" + }, + "outputs": [], + "source": [ + "# Define the Metagraph that transfers attributes via user-defined encoders\n", + "metagraph_v2 = {\n", + " \"vertexCollections\": {\n", + " \"Movies\": {\n", + " \"features\": { # Build a feature matrix from the \"Action\" & \"Drama\" document attributes\n", + " \"Action\": IdentityEncoder(dtype=torch.long),\n", + " \"Drama\": IdentityEncoder(dtype=torch.long),\n", + " },\n", + " \"label\": \"Comedy\",\n", + " },\n", + " \"Users\": {\n", + " \"features\": {\n", + " \"Gender\": CategoricalEncoder(), # CategoricalEncoder(mapping={\"M\": 0, \"F\": 1}),\n", + " \"Age\": IdentityEncoder(dtype=torch.long),\n", + " }\n", + " },\n", + " },\n", + " \"edgeCollections\": {\"Ratings\": {\"weight\": \"Rating\"}},\n", + "}\n", "\n", - " You can derive your own custom ADBDGL_Controller if you want to maintain\n", - " consistency between your ArangoDB attributes & your DGL features.\n", - " \"\"\"\n", + "# Create the DGL Graph\n", + "dgl_g = adbdgl_adapter.arangodb_to_dgl(\"IMDB\", metagraph_v2)\n", "\n", - " def _dgl_feature_to_adb_attribute(self, key: str, col: str, val: Tensor):\n", - " \"\"\"\n", - " Given a DGL feature key, its assigned value (for an arbitrary node or edge),\n", - " and the collection it belongs to, convert it to a valid ArangoDB attribute\n", - " (e.g string, list, number, ...).\n", - "\n", - " NOTE: No action is needed here if you want to keep the numerical-based values\n", - " of your DGL features.\n", - "\n", - " :param key: The DGL attribute key name\n", - " :type key: str\n", - " :param col: The ArangoDB collection of the (soon-to-be) ArangoDB document.\n", - " :type col: str\n", - " :param val: The assigned attribute value of the DGL node.\n", - " :type val: Tensor\n", - " :return: The feature's representation as an ArangoDB Attribute\n", - " :rtype: Any\n", - " \"\"\"\n", + "# Show graph data\n", + "print('\\n--------------')\n", + "print(dgl_g)\n", + "print('\\n--------------')\n", + "print(dgl_g.ndata)\n", + "print('--------------\\n')\n", + "print(dgl_g.edata)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "d5ijSCcY4bYs" + }, + "source": [ + "#### Via ArangoDB-DGL metagraph 3" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "P1aKzxxZrUXJ" + }, + "source": [ + "Data\n", + "* A fake DGL Heterogeneous graph\n", "\n", - " if key == \"clique_ndata\":\n", - " try:\n", - " return [\"Eins\", \"Zwei\", \"Drei\", \"Vier\", \"Fünf\", \"Sechs\"][val-1]\n", - " except:\n", - " return -1\n", + "API\n", + "* `adbdgl_adapter.adapter.arangodb_to_dgl()`\n", "\n", - " if key == \"clique_edata\":\n", - " return bool(val)\n", + "Notes\n", + "* The `name` parameter is purely for documentation purposes in this case.\n", + "* The `metagraph` parameter is an object defining vertex & edge collections to import to DGL, along with collection-level specifications to indicate which ArangoDB attributes will become DGL features/labels. In this example, we rely on user-defined functions to handle ArangoDB attribute to DGL feature conversion." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 377, + "referenced_widgets": [ + "e4b7b35461e848f5819b9f38d67ee652", + "9968f928e28147f7a0956aff8412a608", + "54801c3c74494fe8bf9e2a7fb64bde48", + "903622e283524c7f89635599920c2b14", + "f0d4515c88a44775be59c4e1a0b3c60a", + "9e1eb071f0b24cb6a8d206477b10b831" + ] + }, + "id": "t-lNli3d4bY0", + "outputId": "7bc48392-81a7-4232-aad2-931ff3c8ca48" + }, + "outputs": [], + "source": [ + "# Define the metagraph that transfers attributes via user-defined functions\n", + "def udf_user_features(user_df):\n", + " # process the user_df Pandas DataFrame to return a feature matrix in a tensor\n", + " # user_df[\"features\"] = ...\n", + " return torch.tensor(user_df[\"features\"].to_list())\n", "\n", - " return super()._dgl_feature_to_adb_attribute(key, col, val)\n", "\n", - "# Re-instantiate a new adapter specifically for the Clique Graph Conversion\n", - "clique_adbgl_adapter = ADBDGL_Adapter(db, Clique_ADBDGL_Controller())\n", + "def udf_game_features(game_df):\n", + " # process the game_df Pandas DataFrame to return a feature matrix in a tensor\n", + " # game_df[\"features\"] = ...\n", + " return torch.tensor(game_df[\"features\"].to_list())\n", "\n", - "# Create the ArangoDB graphs\n", - "lollipop = \"Lollipop_With_Attributes\"\n", - "hypercube = \"Hypercube_With_Attributes\"\n", - "clique = \"Clique_With_Attributes\"\n", "\n", - "db.delete_graph(lollipop, drop_collections=True, ignore_missing=True)\n", - "db.delete_graph(hypercube, drop_collections=True, ignore_missing=True)\n", - "db.delete_graph(clique, drop_collections=True, ignore_missing=True)\n", + "metagraph_v3 = {\n", + " \"vertexCollections\": {\n", + " \"user\": {\n", + " \"features\": udf_user_features, # supports named functions\n", + " \"label\": lambda df: torch.tensor(df[\"label\"].to_list()), # also supports lambda functions\n", + " },\n", + " \"game\": {\"features\": udf_game_features},\n", + " },\n", + " \"edgeCollections\": {\n", + " \"plays\": {\"features\": (lambda df: torch.tensor(df[\"features\"].to_list()))},\n", + " },\n", + "}\n", "\n", - "adb_lollipop_graph = adbdgl_adapter.dgl_to_arangodb(lollipop, dgl_lollipop_graph)\n", - "adb_hypercube_graph = adbdgl_adapter.dgl_to_arangodb(hypercube, dgl_hypercube_graph)\n", - "adb_clique_graph = clique_adbgl_adapter.dgl_to_arangodb(clique, dgl_clique_graph) # Notice the new adapter here!\n", + "# Create the DGL Graph\n", + "dgl_g = adbdgl_adapter.arangodb_to_dgl(\"FakeHetero\", metagraph_v3)\n", "\n", - "print('\\n--------------------')\n", - "print(\"URL: \" + con[\"url\"])\n", - "print(\"Username: \" + con[\"username\"])\n", - "print(\"Password: \" + con[\"password\"])\n", - "print(\"Database: \" + con[\"dbName\"])\n", - "print('--------------------\\n')\n", - "print(\"View the created graphs here:\\n\")\n", - "print(f\"1) {con['url']}/_db/{con['dbName']}/_admin/aardvark/index.html#graph/{lollipop}\")\n", - "print(f\"2) {con['url']}/_db/{con['dbName']}/_admin/aardvark/index.html#graph/{hypercube}\")\n", - "print(f\"3) {con['url']}/_db/{con['dbName']}/_admin/aardvark/index.html#graph/{clique}\\n\")" + "# Show graph data\n", + "print('\\n--------------')\n", + "print(dgl_g)\n", + "print('\\n--------------')\n", + "print(dgl_g.ndata)\n", + "print('--------------\\n')\n", + "print(dgl_g.edata)" ] } ], "metadata": { "colab": { "collapsed_sections": [ - "KS9c-vE5eG89", "ot1oJqn7m78n", "Oc__NAd1eG8-", "7y81WHO8eG8_", "QfE_tKxneG9A", + "bvzJXSHHTi3v", + "UafSB_3JZNwK", + "CNj1xKhwoJoL", + "n08RC_GtkDrC", + "mk6m0hBRkkkT", "uByvwf9feG9A", - "bvzJXSHHTi3v" + "ZrEDmtqCVD0W", + "RQ4CknYfUEuz", + "qEH6OdSB23Ya", + "0806IB4o3WRz", + "d5ijSCcY4bYs" ], - "name": "ArangoDB_DGL_Adapter_v2.ipynb", + "name": "ArangoDB_DGL_Adapter_v3.ipynb", "provenance": [] }, "kernelspec": { diff --git a/examples/data/fraud_dump/Class_9bd81329febf6efe22788e03ddeaf0af.data.json.gz b/examples/data/fraud_dump/Class_9bd81329febf6efe22788e03ddeaf0af.data.json.gz deleted file mode 100644 index 3b56137..0000000 Binary files a/examples/data/fraud_dump/Class_9bd81329febf6efe22788e03ddeaf0af.data.json.gz and /dev/null differ diff --git a/examples/data/fraud_dump/Class_9bd81329febf6efe22788e03ddeaf0af.structure.json b/examples/data/fraud_dump/Class_9bd81329febf6efe22788e03ddeaf0af.structure.json deleted file mode 100644 index dc132f3..0000000 --- a/examples/data/fraud_dump/Class_9bd81329febf6efe22788e03ddeaf0af.structure.json +++ /dev/null @@ -1 +0,0 @@ -{"indexes":[],"parameters":{"allowUserKeys":true,"cacheEnabled":false,"cid":"63915","deleted":false,"globallyUniqueId":"c6251106/","id":"63915","isSmart":false,"isSystem":false,"keyOptions":{"allowUserKeys":true,"type":"traditional","lastValue":0},"minReplicationFactor":1,"name":"Class","numberOfShards":1,"planId":"6251106","replicationFactor":3,"shardKeys":["_key"],"shards":{"s6251107":["PRMR-drkxnewt","PRMR-mefeyznw","PRMR-9tthmtzr"]},"status":3,"type":2,"version":8,"waitForSync":false,"writeConcern":1}} \ No newline at end of file diff --git a/examples/data/fraud_dump/Relationship_fbc97786af4bf30dc5b07809a950792c.data.json.gz b/examples/data/fraud_dump/Relationship_fbc97786af4bf30dc5b07809a950792c.data.json.gz deleted file mode 100644 index 03244bc..0000000 Binary files a/examples/data/fraud_dump/Relationship_fbc97786af4bf30dc5b07809a950792c.data.json.gz and /dev/null differ diff --git a/examples/data/fraud_dump/Relationship_fbc97786af4bf30dc5b07809a950792c.structure.json b/examples/data/fraud_dump/Relationship_fbc97786af4bf30dc5b07809a950792c.structure.json deleted file mode 100644 index e4d2d9a..0000000 --- a/examples/data/fraud_dump/Relationship_fbc97786af4bf30dc5b07809a950792c.structure.json +++ /dev/null @@ -1 +0,0 @@ -{"indexes":[],"parameters":{"allowUserKeys":true,"cacheEnabled":false,"cid":"63926","deleted":false,"globallyUniqueId":"c6251114/","id":"63926","isSmart":false,"isSystem":false,"keyOptions":{"allowUserKeys":true,"type":"traditional","lastValue":0},"minReplicationFactor":1,"name":"Relationship","numberOfShards":1,"planId":"6251114","replicationFactor":3,"shardKeys":["_key"],"shards":{"s6251115":["PRMR-drkxnewt","PRMR-mefeyznw","PRMR-9tthmtzr"]},"status":3,"type":3,"version":8,"waitForSync":false,"writeConcern":1}} \ No newline at end of file diff --git a/examples/data/fraud_dump/Text_Search.view.json b/examples/data/fraud_dump/Text_Search.view.json deleted file mode 100644 index 432adb3..0000000 --- a/examples/data/fraud_dump/Text_Search.view.json +++ /dev/null @@ -1 +0,0 @@ -{"globallyUniqueId":"h7CC8359662CF/1626628","id":"1626628","name":"Text_Search","type":"arangosearch","cleanupIntervalStep":10,"commitIntervalMsec":1000,"consolidationIntervalMsec":60000,"consolidationPolicy":{"type":"bytes_accum","threshold":0.10000000149011612},"primarySort":[],"writebufferActive":0,"writebufferIdle":64,"writebufferSizeMax":33554432,"links":{}} \ No newline at end of file diff --git a/examples/data/fraud_dump/_analyzers_839c888a45b895a4783b6dbd338f0155.data.json.gz b/examples/data/fraud_dump/_analyzers_839c888a45b895a4783b6dbd338f0155.data.json.gz deleted file mode 100644 index d210671..0000000 Binary files a/examples/data/fraud_dump/_analyzers_839c888a45b895a4783b6dbd338f0155.data.json.gz and /dev/null differ diff --git a/examples/data/fraud_dump/_analyzers_839c888a45b895a4783b6dbd338f0155.structure.json b/examples/data/fraud_dump/_analyzers_839c888a45b895a4783b6dbd338f0155.structure.json deleted file mode 100644 index 74877aa..0000000 --- a/examples/data/fraud_dump/_analyzers_839c888a45b895a4783b6dbd338f0155.structure.json +++ /dev/null @@ -1 +0,0 @@ -{"indexes":[],"parameters":{"allowUserKeys":true,"cacheEnabled":false,"cid":"63866","deleted":false,"globallyUniqueId":"_analyzers","id":"63866","isSmart":false,"isSystem":true,"keyOptions":{"allowUserKeys":true,"type":"traditional","lastValue":0},"minReplicationFactor":1,"name":"_analyzers","numberOfShards":1,"planId":"63866","replicationFactor":1,"shardKeys":["_key"],"shards":{},"status":3,"type":2,"version":8,"waitForSync":false,"writeConcern":1}} \ No newline at end of file diff --git a/examples/data/fraud_dump/_appbundles_105ca6a6a72935fd370f79f3a3e62b0e.data.json.gz b/examples/data/fraud_dump/_appbundles_105ca6a6a72935fd370f79f3a3e62b0e.data.json.gz deleted file mode 100644 index d210671..0000000 Binary files a/examples/data/fraud_dump/_appbundles_105ca6a6a72935fd370f79f3a3e62b0e.data.json.gz and /dev/null differ diff --git a/examples/data/fraud_dump/_appbundles_105ca6a6a72935fd370f79f3a3e62b0e.structure.json b/examples/data/fraud_dump/_appbundles_105ca6a6a72935fd370f79f3a3e62b0e.structure.json deleted file mode 100644 index f3bdfc8..0000000 --- a/examples/data/fraud_dump/_appbundles_105ca6a6a72935fd370f79f3a3e62b0e.structure.json +++ /dev/null @@ -1 +0,0 @@ -{"indexes":[],"parameters":{"allowUserKeys":true,"cacheEnabled":false,"cid":"63881","deleted":false,"globallyUniqueId":"_appbundles","id":"63881","isSmart":false,"isSystem":true,"keyOptions":{"allowUserKeys":true,"type":"traditional","lastValue":0},"minReplicationFactor":1,"name":"_appbundles","numberOfShards":1,"planId":"63881","replicationFactor":1,"shardKeys":["_key"],"shards":{},"status":3,"type":2,"version":8,"waitForSync":false,"writeConcern":1}} \ No newline at end of file diff --git a/examples/data/fraud_dump/_apps_c3f2c8489196d21e33f194f4bafb3f05.data.json.gz b/examples/data/fraud_dump/_apps_c3f2c8489196d21e33f194f4bafb3f05.data.json.gz deleted file mode 100644 index d210671..0000000 Binary files a/examples/data/fraud_dump/_apps_c3f2c8489196d21e33f194f4bafb3f05.data.json.gz and /dev/null differ diff --git a/examples/data/fraud_dump/_apps_c3f2c8489196d21e33f194f4bafb3f05.structure.json b/examples/data/fraud_dump/_apps_c3f2c8489196d21e33f194f4bafb3f05.structure.json deleted file mode 100644 index fce1a9d..0000000 --- a/examples/data/fraud_dump/_apps_c3f2c8489196d21e33f194f4bafb3f05.structure.json +++ /dev/null @@ -1 +0,0 @@ -{"indexes":[{"id":"63893","type":"hash","name":"idx_1654880607689244672","fields":["mount"],"unique":true,"sparse":true,"deduplicate":true}],"parameters":{"allowUserKeys":true,"cacheEnabled":false,"cid":"63878","deleted":false,"globallyUniqueId":"_apps","id":"63878","isSmart":false,"isSystem":true,"keyOptions":{"allowUserKeys":true,"type":"traditional","lastValue":0},"minReplicationFactor":1,"name":"_apps","numberOfShards":1,"planId":"63878","replicationFactor":1,"shardKeys":["_key"],"shards":{},"status":3,"type":2,"version":8,"waitForSync":false,"writeConcern":1}} \ No newline at end of file diff --git a/examples/data/fraud_dump/_aqlfunctions_8293af7a2caabc3098bc21db7ce2759d.data.json.gz b/examples/data/fraud_dump/_aqlfunctions_8293af7a2caabc3098bc21db7ce2759d.data.json.gz deleted file mode 100644 index d210671..0000000 Binary files a/examples/data/fraud_dump/_aqlfunctions_8293af7a2caabc3098bc21db7ce2759d.data.json.gz and /dev/null differ diff --git a/examples/data/fraud_dump/_aqlfunctions_8293af7a2caabc3098bc21db7ce2759d.structure.json b/examples/data/fraud_dump/_aqlfunctions_8293af7a2caabc3098bc21db7ce2759d.structure.json deleted file mode 100644 index 9b42e3d..0000000 --- a/examples/data/fraud_dump/_aqlfunctions_8293af7a2caabc3098bc21db7ce2759d.structure.json +++ /dev/null @@ -1 +0,0 @@ -{"indexes":[],"parameters":{"allowUserKeys":true,"cacheEnabled":false,"cid":"63869","deleted":false,"globallyUniqueId":"_aqlfunctions","id":"63869","isSmart":false,"isSystem":true,"keyOptions":{"allowUserKeys":true,"type":"traditional","lastValue":0},"minReplicationFactor":1,"name":"_aqlfunctions","numberOfShards":1,"planId":"63869","replicationFactor":1,"shardKeys":["_key"],"shards":{},"status":3,"type":2,"version":8,"waitForSync":false,"writeConcern":1}} \ No newline at end of file diff --git a/examples/data/fraud_dump/_graphs_c827636f2b54efb49f1f02feeeacfb01.data.json.gz b/examples/data/fraud_dump/_graphs_c827636f2b54efb49f1f02feeeacfb01.data.json.gz deleted file mode 100644 index fe7a64b..0000000 Binary files a/examples/data/fraud_dump/_graphs_c827636f2b54efb49f1f02feeeacfb01.data.json.gz and /dev/null differ diff --git a/examples/data/fraud_dump/_graphs_c827636f2b54efb49f1f02feeeacfb01.structure.json b/examples/data/fraud_dump/_graphs_c827636f2b54efb49f1f02feeeacfb01.structure.json deleted file mode 100644 index 3823e26..0000000 --- a/examples/data/fraud_dump/_graphs_c827636f2b54efb49f1f02feeeacfb01.structure.json +++ /dev/null @@ -1 +0,0 @@ -{"indexes":[],"parameters":{"allowUserKeys":true,"cacheEnabled":false,"cid":"63863","deleted":false,"globallyUniqueId":"_graphs","id":"63863","isSmart":false,"isSystem":true,"keyOptions":{"allowUserKeys":true,"type":"traditional","lastValue":0},"minReplicationFactor":1,"name":"_graphs","numberOfShards":1,"planId":"63863","replicationFactor":1,"shardKeys":["_key"],"shards":{},"status":3,"type":2,"version":8,"waitForSync":false,"writeConcern":1}} \ No newline at end of file diff --git a/examples/data/fraud_dump/_modules_5a8c8ba0d331b61fccfd1e88cfedce00.data.json.gz b/examples/data/fraud_dump/_modules_5a8c8ba0d331b61fccfd1e88cfedce00.data.json.gz deleted file mode 100644 index d210671..0000000 Binary files a/examples/data/fraud_dump/_modules_5a8c8ba0d331b61fccfd1e88cfedce00.data.json.gz and /dev/null differ diff --git a/examples/data/fraud_dump/_modules_5a8c8ba0d331b61fccfd1e88cfedce00.structure.json b/examples/data/fraud_dump/_modules_5a8c8ba0d331b61fccfd1e88cfedce00.structure.json deleted file mode 100644 index 2d03476..0000000 --- a/examples/data/fraud_dump/_modules_5a8c8ba0d331b61fccfd1e88cfedce00.structure.json +++ /dev/null @@ -1 +0,0 @@ -{"indexes":[],"parameters":{"allowUserKeys":true,"cacheEnabled":false,"cid":"63887","deleted":false,"globallyUniqueId":"_modules","id":"63887","isSmart":false,"isSystem":true,"keyOptions":{"allowUserKeys":true,"type":"traditional","lastValue":0},"minReplicationFactor":1,"name":"_modules","numberOfShards":1,"planId":"63887","replicationFactor":1,"shardKeys":["_key"],"shards":{},"status":3,"type":2,"version":8,"waitForSync":false,"writeConcern":1}} \ No newline at end of file diff --git a/examples/data/fraud_dump/accountHolder_2e31953e2b3a86325411a027c406e65a.data.json.gz b/examples/data/fraud_dump/accountHolder_2e31953e2b3a86325411a027c406e65a.data.json.gz deleted file mode 100644 index 2d431e1..0000000 Binary files a/examples/data/fraud_dump/accountHolder_2e31953e2b3a86325411a027c406e65a.data.json.gz and /dev/null differ diff --git a/examples/data/fraud_dump/accountHolder_2e31953e2b3a86325411a027c406e65a.structure.json b/examples/data/fraud_dump/accountHolder_2e31953e2b3a86325411a027c406e65a.structure.json deleted file mode 100644 index 48627c6..0000000 --- a/examples/data/fraud_dump/accountHolder_2e31953e2b3a86325411a027c406e65a.structure.json +++ /dev/null @@ -1 +0,0 @@ -{"indexes":[],"parameters":{"allowUserKeys":true,"cacheEnabled":false,"cid":"63921","deleted":false,"globallyUniqueId":"c6251112/","id":"63921","isSmart":false,"isSystem":false,"keyOptions":{"allowUserKeys":true,"type":"traditional","lastValue":1000001610000049},"minReplicationFactor":1,"name":"accountHolder","numberOfShards":1,"planId":"6251112","replicationFactor":3,"shardKeys":["_key"],"shards":{"s6251113":["PRMR-drkxnewt","PRMR-mefeyznw","PRMR-9tthmtzr"]},"status":3,"type":3,"version":8,"waitForSync":false,"writeConcern":1}} \ No newline at end of file diff --git a/examples/data/fraud_dump/account_e268443e43d93dab7ebef303bbe9642f.data.json.gz b/examples/data/fraud_dump/account_e268443e43d93dab7ebef303bbe9642f.data.json.gz deleted file mode 100644 index 15cdb8c..0000000 Binary files a/examples/data/fraud_dump/account_e268443e43d93dab7ebef303bbe9642f.data.json.gz and /dev/null differ diff --git a/examples/data/fraud_dump/account_e268443e43d93dab7ebef303bbe9642f.structure.json b/examples/data/fraud_dump/account_e268443e43d93dab7ebef303bbe9642f.structure.json deleted file mode 100644 index 845b982..0000000 --- a/examples/data/fraud_dump/account_e268443e43d93dab7ebef303bbe9642f.structure.json +++ /dev/null @@ -1 +0,0 @@ -{"indexes":[{"id":"62842606","type":"persistent","name":"idx_1661656393880436736","fields":["branch_id"],"unique":false,"sparse":false,"deduplicate":false}],"parameters":{"allowUserKeys":true,"cacheEnabled":false,"cid":"63906","deleted":false,"globallyUniqueId":"c6251100/","id":"63906","isSmart":false,"isSystem":false,"keyOptions":{"allowUserKeys":true,"type":"traditional","lastValue":10000044},"minReplicationFactor":1,"name":"account","numberOfShards":1,"planId":"6251100","replicationFactor":3,"shardKeys":["_key"],"shards":{"s6251101":["PRMR-drkxnewt","PRMR-mefeyznw","PRMR-9tthmtzr"]},"status":3,"type":2,"version":8,"waitForSync":false,"writeConcern":1}} \ No newline at end of file diff --git a/examples/data/fraud_dump/bank_bd5af1f610a12434c9128e4a399cef8a.data.json.gz b/examples/data/fraud_dump/bank_bd5af1f610a12434c9128e4a399cef8a.data.json.gz deleted file mode 100644 index ba4383e..0000000 Binary files a/examples/data/fraud_dump/bank_bd5af1f610a12434c9128e4a399cef8a.data.json.gz and /dev/null differ diff --git a/examples/data/fraud_dump/bank_bd5af1f610a12434c9128e4a399cef8a.structure.json b/examples/data/fraud_dump/bank_bd5af1f610a12434c9128e4a399cef8a.structure.json deleted file mode 100644 index 7096615..0000000 --- a/examples/data/fraud_dump/bank_bd5af1f610a12434c9128e4a399cef8a.structure.json +++ /dev/null @@ -1 +0,0 @@ -{"indexes":[],"parameters":{"allowUserKeys":true,"cacheEnabled":false,"cid":"63909","deleted":false,"globallyUniqueId":"c6251102/","id":"63909","isSmart":false,"isSystem":false,"keyOptions":{"allowUserKeys":true,"type":"traditional","lastValue":1548226},"minReplicationFactor":1,"name":"bank","numberOfShards":1,"planId":"6251102","replicationFactor":3,"shardKeys":["_key"],"shards":{"s6251103":["PRMR-drkxnewt","PRMR-mefeyznw","PRMR-9tthmtzr"]},"status":3,"type":2,"version":8,"waitForSync":false,"writeConcern":1}} \ No newline at end of file diff --git a/examples/data/fraud_dump/branch_9603a224b40d7b67210b78f2e390d00f.data.json.gz b/examples/data/fraud_dump/branch_9603a224b40d7b67210b78f2e390d00f.data.json.gz deleted file mode 100644 index 601e87b..0000000 Binary files a/examples/data/fraud_dump/branch_9603a224b40d7b67210b78f2e390d00f.data.json.gz and /dev/null differ diff --git a/examples/data/fraud_dump/branch_9603a224b40d7b67210b78f2e390d00f.structure.json b/examples/data/fraud_dump/branch_9603a224b40d7b67210b78f2e390d00f.structure.json deleted file mode 100644 index e843f8a..0000000 --- a/examples/data/fraud_dump/branch_9603a224b40d7b67210b78f2e390d00f.structure.json +++ /dev/null @@ -1 +0,0 @@ -{"indexes":[],"parameters":{"allowUserKeys":true,"cacheEnabled":false,"cid":"63912","deleted":false,"globallyUniqueId":"c6251104/","id":"63912","isSmart":false,"isSystem":false,"keyOptions":{"allowUserKeys":true,"type":"traditional","lastValue":1548212},"minReplicationFactor":1,"name":"branch","numberOfShards":1,"planId":"6251104","replicationFactor":3,"shardKeys":["_key"],"shards":{"s6251105":["PRMR-drkxnewt","PRMR-mefeyznw","PRMR-9tthmtzr"]},"status":3,"type":2,"version":8,"waitForSync":false,"writeConcern":1}} \ No newline at end of file diff --git a/examples/data/fraud_dump/customer_91ec1f9324753048c0096d036a694f86.data.json.gz b/examples/data/fraud_dump/customer_91ec1f9324753048c0096d036a694f86.data.json.gz deleted file mode 100644 index 29c9e9c..0000000 Binary files a/examples/data/fraud_dump/customer_91ec1f9324753048c0096d036a694f86.data.json.gz and /dev/null differ diff --git a/examples/data/fraud_dump/customer_91ec1f9324753048c0096d036a694f86.structure.json b/examples/data/fraud_dump/customer_91ec1f9324753048c0096d036a694f86.structure.json deleted file mode 100644 index a9750ca..0000000 --- a/examples/data/fraud_dump/customer_91ec1f9324753048c0096d036a694f86.structure.json +++ /dev/null @@ -1 +0,0 @@ -{"indexes":[{"id":"48122621","type":"persistent","name":"idx_1653203216113860608","fields":["Ssn"],"unique":false,"sparse":false,"deduplicate":false}],"parameters":{"allowUserKeys":true,"cacheEnabled":false,"cid":"63918","deleted":false,"globallyUniqueId":"c6251108/","id":"63918","isSmart":false,"isSystem":false,"keyOptions":{"allowUserKeys":true,"type":"traditional","lastValue":10000016},"minReplicationFactor":1,"name":"customer","numberOfShards":1,"planId":"6251108","replicationFactor":3,"shardKeys":["_key"],"shards":{"s6251109":["PRMR-drkxnewt","PRMR-mefeyznw","PRMR-9tthmtzr"]},"status":3,"type":2,"version":8,"waitForSync":false,"writeConcern":1}} \ No newline at end of file diff --git a/examples/data/fraud_dump/dump.json b/examples/data/fraud_dump/dump.json deleted file mode 100644 index 34d27a7..0000000 --- a/examples/data/fraud_dump/dump.json +++ /dev/null @@ -1 +0,0 @@ -{"database":"fraud-detection","lastTickAtDumpStart":"63082802","properties":{"id":"63861","name":"fraud-detection","isSystem":true}} \ No newline at end of file diff --git a/examples/data/fraud_dump/transaction_f4d5b76a2418eba4baeabc1ed9142b54.data.json.gz b/examples/data/fraud_dump/transaction_f4d5b76a2418eba4baeabc1ed9142b54.data.json.gz deleted file mode 100644 index e2c2d28..0000000 Binary files a/examples/data/fraud_dump/transaction_f4d5b76a2418eba4baeabc1ed9142b54.data.json.gz and /dev/null differ diff --git a/examples/data/fraud_dump/transaction_f4d5b76a2418eba4baeabc1ed9142b54.structure.json b/examples/data/fraud_dump/transaction_f4d5b76a2418eba4baeabc1ed9142b54.structure.json deleted file mode 100644 index 569ceb7..0000000 --- a/examples/data/fraud_dump/transaction_f4d5b76a2418eba4baeabc1ed9142b54.structure.json +++ /dev/null @@ -1 +0,0 @@ -{"indexes":[],"parameters":{"allowUserKeys":true,"cacheEnabled":false,"cid":"63931","deleted":false,"globallyUniqueId":"c6251116/","id":"63931","isSmart":false,"isSystem":false,"keyOptions":{"allowUserKeys":true,"type":"traditional","lastValue":3152813},"minReplicationFactor":1,"name":"transaction","numberOfShards":1,"planId":"6251116","replicationFactor":3,"shardKeys":["_key"],"shards":{"s6251117":["PRMR-drkxnewt","PRMR-mefeyznw","PRMR-9tthmtzr"]},"status":3,"type":3,"version":8,"waitForSync":false,"writeConcern":1}} \ No newline at end of file diff --git a/examples/outputs/ArangoDB_DGL_Adapter_output.ipynb b/examples/outputs/ArangoDB_DGL_Adapter_output.ipynb index fa9100a..0dc3cbd 100644 --- a/examples/outputs/ArangoDB_DGL_Adapter_output.ipynb +++ b/examples/outputs/ArangoDB_DGL_Adapter_output.ipynb @@ -15,7 +15,7 @@ "id": "U1d45V4OeG89" }, "source": [ - "\"Open" + "\"Open" ] }, { @@ -34,7 +34,7 @@ "id": "bpvZS-1aeG89" }, "source": [ - "Version: 2.0.0\n", + "Version: 3.0.0\n", "\n", "Objective: Export Graphs from [ArangoDB](https://www.arangodb.com/), a multi-model Graph Database, to [Deep Graph Library](https://www.dgl.ai/) (DGL), a python package for graph neural networks, and vice-versa." ] @@ -57,39 +57,55 @@ "outputs": [], "source": [ "%%capture\n", - "!pip install adbdgl-adapter==2.0.0\n", + "!pip install adbdgl-adapter==3.0.0\n", "!pip install adb-cloud-connector\n", - "!git clone -b 2.0.0 --single-branch https://github.com/arangoml/dgl-adapter.git\n", + "!git clone -b 3.0.0 --single-branch https://github.com/arangoml/dgl-adapter.git\n", "\n", - "## For drawing purposes \n", + "## For drawing purposes\n", "!pip install matplotlib\n", - "!pip install networkx " + "!pip install networkx" ] }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": { - "id": "niijQHqBM6zp" + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "niijQHqBM6zp", + "outputId": "77df8f72-4000-44e8-9dd6-c56bbf33c07d" }, - "outputs": [], + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "DGL backend not selected or invalid. Assuming PyTorch for now.\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Setting the default backend to \"pytorch\". You can change it in the ~/.dgl/config.json file or export the DGLBACKEND environment variable. Valid options are: pytorch, mxnet, tensorflow (all lowercase)\n" + ] + } + ], "source": [ "# All imports\n", "\n", + "import pandas\n", + "import torch\n", "import dgl\n", - "from dgl import remove_self_loop\n", - "from dgl.data import MiniGCDataset\n", "from dgl.data import KarateClubDataset\n", "\n", - "import torch\n", - "from torch import Tensor\n", - "\n", - "from adbdgl_adapter import ADBDGL_Adapter, ADBDGL_Controller\n", - "from adbdgl_adapter.typings import Json, ArangoMetagraph, DGLCanonicalEType, DGLDataDict\n", - "\n", "from arango import ArangoClient\n", "from adb_cloud_connector import get_temp_credentials\n", "\n", + "from adbdgl_adapter import ADBDGL_Adapter, ADBDGL_Controller\n", + "from adbdgl_adapter.encoders import IdentityEncoder, CategoricalEncoder\n", + "\n", "import json\n", "import logging\n", "\n", @@ -119,18 +135,18 @@ "\n", "DGL represents a directed graph as a `DGLGraph` object. You can construct a graph by specifying the number of nodes in the graph as well as the list of source and destination nodes. **Nodes in the graph have consecutive IDs starting from 0.**\n", "\n", - "The following code constructs a directed \"star\" homogeneous graph with 6 nodes and 5 edges. \n" + "The following code constructs a directed \"star\" homogeneous graph with 6 nodes and 5 edges.\n" ] }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "vf0350qvj8up", - "outputId": "fbf300df-5dcd-44e8-a746-cb554eba1dd8" + "outputId": "bb473200-893d-4d4e-ed6d-239ec497d0e3" }, "outputs": [ { @@ -169,13 +185,13 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "oOS3AVAnkQEV", - "outputId": "3a7403db-d11b-4f7a-a0b7-6e8220186273" + "outputId": "5b5feaaa-2a6f-4e0e-ef89-68b9e365a6db" }, "outputs": [ { @@ -221,13 +237,13 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "meLon-KgkU4h", - "outputId": "fa57e121-5294-45f9-b3d0-3a2cfa212da7" + "outputId": "7517b39b-adfa-426d-ccae-89254cf642b5" }, "outputs": [ { @@ -240,11 +256,11 @@ "\n", "Node Data X attribute: tensor([151, 124, 41, 89, 76, 55])\n", "\n", - "Edge Data A attribute: tensor([[-0.6538, 1.5450, -1.7828, 1.2241],\n", - " [ 1.3176, -0.0545, 0.8196, 0.0695],\n", - " [-0.8568, 1.3135, 0.4980, -0.4290],\n", - " [ 1.5448, 0.2502, 2.3616, 1.2318],\n", - " [-0.9194, 0.2285, 0.0267, -0.0482]])\n" + "Edge Data A attribute: tensor([[ 0.6125, 0.4397, -0.4108, -0.6406],\n", + " [-0.4089, -0.3135, -0.8268, 0.2150],\n", + " [-0.5285, -1.7320, 0.5904, -0.2922],\n", + " [ 0.3878, 0.1858, 0.9546, -0.4877],\n", + " [ 1.4629, -1.9385, -2.1406, -0.1621]])\n" ] } ], @@ -277,13 +293,13 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "zTebQ0LOlsGA", - "outputId": "f5c06fec-a3e3-41fb-b478-42e492af07de" + "outputId": "c871096b-b06e-4cd8-ad56-06758090600d" }, "outputs": [ { @@ -330,13 +346,13 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "KsxNujb0mSqZ", - "outputId": "0cf12da9-c754-41a3-9496-5aea0a0faac9" + "outputId": "0b7b4106-7385-4489-e49a-399efbef0cb8" }, "outputs": [ { @@ -367,7 +383,7 @@ "id": "1M_isKWLnCfr" }, "source": [ - "For more info, visit https://docs.dgl.ai/en/0.6.x/. " + "For more info, visit https://docs.dgl.ai/en/0.6.x/." ] }, { @@ -381,13 +397,13 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "2ekGwnJDeG8-", - "outputId": "02cf35c6-9416-44fb-be44-5c0f517e0f78" + "outputId": "84a1c36b-3dc1-47e2-dadf-8a4ebefd98c0" }, "outputs": [ { @@ -397,9 +413,9 @@ "Log: requesting new credentials...\n", "Succcess: new credentials acquired\n", "{\n", - " \"dbName\": \"TUT56z6dbtgsoeu5cc6aixs7d\",\n", - " \"username\": \"TUTtj3263blez70kmqdi3ts\",\n", - " \"password\": \"TUTf6tursgxqogdo3ww3nplb\",\n", + " \"dbName\": \"TUTk9nlikuz4zowwxfkusway\",\n", + " \"username\": \"TUT6h05us6483maimfr7o28jq\",\n", + " \"password\": \"TUTis4noysrzjeig2bqpdccaa\",\n", " \"hostname\": \"tutorials.arangodb.cloud\",\n", " \"port\": 8529,\n", " \"url\": \"https://tutorials.arangodb.cloud:8529\"\n", @@ -440,81 +456,83 @@ "id": "BM0iRYPDeG8_" }, "source": [ - "For demo purposes, we will be using the [ArangoDB Fraud Detection example graph](https://colab.research.google.com/github/joerg84/Graph_Powered_ML_Workshop/blob/master/Fraud_Detection.ipynb)." + "For demo purposes, we will be using the [ArangoDB IMDB example graph](https://www.arangodb.com/docs/stable/arangosearch-example-datasets.html#imdb-movie-dataset)." ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "7bgGJ3QkeG8_", - "outputId": "15b25959-5a2f-4d1c-852e-5019845716a4" + "outputId": "1f490370-72f3-4d1b-8950-ef1d0f690218" }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "\u001b[0m2022-05-25T17:23:07Z [272] INFO [05c30] {restore} Connected to ArangoDB 'http+ssl://tutorials.arangodb.cloud:8529'\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:08Z [272] INFO [abeb4] {restore} Database name in source dump is 'fraud-detection'\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:08Z [272] INFO [9b414] {restore} # Re-creating document collection '_analyzers'...\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:08Z [272] INFO [9b414] {restore} # Re-creating document collection '_appbundles'...\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:12Z [272] INFO [9b414] {restore} # Re-creating document collection '_apps'...\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:13Z [272] INFO [9b414] {restore} # Re-creating document collection '_aqlfunctions'...\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:17Z [272] INFO [9b414] {restore} # Re-creating document collection '_graphs'...\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:17Z [272] INFO [9b414] {restore} # Re-creating document collection '_modules'...\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:17Z [272] INFO [9b414] {restore} # Re-creating document collection 'account'...\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:18Z [272] INFO [9b414] {restore} # Re-creating document collection 'bank'...\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:18Z [272] INFO [9b414] {restore} # Re-creating document collection 'branch'...\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:18Z [272] INFO [9b414] {restore} # Re-creating document collection 'Class'...\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:18Z [272] INFO [9b414] {restore} # Re-creating document collection 'customer'...\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:19Z [272] INFO [9b414] {restore} # Re-creating edge collection 'accountHolder'...\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:19Z [272] INFO [9b414] {restore} # Re-creating edge collection 'Relationship'...\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:19Z [272] INFO [9b414] {restore} # Re-creating edge collection 'transaction'...\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:20Z [272] INFO [94913] {restore} # Loading data into document collection '_analyzers', data size: 20 byte(s)\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:20Z [272] INFO [6ae09] {restore} # Successfully restored document collection '_analyzers'\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:20Z [272] INFO [f723c] {restore} # Creating views...\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:20Z [272] INFO [6d69f] {restore} # Dispatched 14 job(s), using 2 worker(s)\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:20Z [272] INFO [d88c6] {restore} # Creating indexes for collection '_apps'...\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:20Z [272] INFO [94913] {restore} # Loading data into document collection '_appbundles', data size: 20 byte(s)\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:20Z [272] INFO [6ae09] {restore} # Successfully restored document collection '_appbundles'\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:20Z [272] INFO [94913] {restore} # Loading data into document collection '_aqlfunctions', data size: 20 byte(s)\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:20Z [272] INFO [6ae09] {restore} # Successfully restored document collection '_aqlfunctions'\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:20Z [272] INFO [94913] {restore} # Loading data into document collection '_graphs', data size: 292 byte(s)\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:20Z [272] INFO [6ae09] {restore} # Successfully restored document collection '_graphs'\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:20Z [272] INFO [94913] {restore} # Loading data into document collection '_modules', data size: 20 byte(s)\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:20Z [272] INFO [6ae09] {restore} # Successfully restored document collection '_modules'\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:20Z [272] INFO [d88c6] {restore} # Creating indexes for collection 'account'...\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:20Z [272] INFO [94913] {restore} # Loading data into document collection '_apps', data size: 20 byte(s)\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:20Z [272] INFO [6ae09] {restore} # Successfully restored document collection '_apps'\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:20Z [272] INFO [94913] {restore} # Loading data into document collection 'bank', data size: 183 byte(s)\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:20Z [272] INFO [94913] {restore} # Loading data into document collection 'account', data size: 1696 byte(s)\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:20Z [272] INFO [6ae09] {restore} # Successfully restored document collection 'bank'\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:20Z [272] INFO [94913] {restore} # Loading data into document collection 'branch', data size: 465 byte(s)\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:20Z [272] INFO [6ae09] {restore} # Successfully restored document collection 'account'\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:20Z [272] INFO [94913] {restore} # Loading data into document collection 'Class', data size: 196 byte(s)\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:20Z [272] INFO [6ae09] {restore} # Successfully restored document collection 'branch'\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:20Z [272] INFO [d88c6] {restore} # Creating indexes for collection 'customer'...\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:20Z [272] INFO [6ae09] {restore} # Successfully restored document collection 'Class'\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:20Z [272] INFO [94913] {restore} # Loading data into edge collection 'accountHolder', data size: 1076 byte(s)\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:20Z [272] INFO [94913] {restore} # Loading data into document collection 'customer', data size: 794 byte(s)\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:20Z [272] INFO [6ae09] {restore} # Successfully restored edge collection 'accountHolder'\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:20Z [272] INFO [94913] {restore} # Loading data into edge collection 'Relationship', data size: 275 byte(s)\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:20Z [272] INFO [6ae09] {restore} # Successfully restored document collection 'customer'\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:20Z [272] INFO [94913] {restore} # Loading data into edge collection 'transaction', data size: 2292 byte(s)\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:20Z [272] INFO [6ae09] {restore} # Successfully restored edge collection 'Relationship'\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:20Z [272] INFO [6ae09] {restore} # Successfully restored edge collection 'transaction'\n", - "\u001b[0m\u001b[0m2022-05-25T17:23:20Z [272] INFO [a66e1] {restore} Processed 14 collection(s) in 13.360950 s, read 50480 byte(s) from datafiles, sent 9 data batch(es) of 50471 byte(s) total size\n", + "\u001b[0m2022-08-05T20:32:43Z [308] INFO [05c30] {restore} Connected to ArangoDB 'http+ssl://tutorials.arangodb.cloud:8529'\n", + "\u001b[0m\u001b[0m2022-08-05T20:32:43Z [308] INFO [abeb4] {restore} Database name in source dump is 'TUTdit9ohpgz1ntnbetsjstwi'\n", + "\u001b[0m\u001b[0m2022-08-05T20:32:43Z [308] INFO [9b414] {restore} # Re-creating document collection 'Movies'...\n", + "\u001b[0m\u001b[0m2022-08-05T20:32:43Z [308] INFO [9b414] {restore} # Re-creating document collection 'Users'...\n", + "\u001b[0m\u001b[0m2022-08-05T20:32:44Z [308] INFO [9b414] {restore} # Re-creating edge collection 'Ratings'...\n", + "\u001b[0m\u001b[0m2022-08-05T20:32:44Z [308] INFO [6d69f] {restore} # Dispatched 3 job(s), using 2 worker(s)\n", + "\u001b[0m\u001b[0m2022-08-05T20:32:44Z [308] INFO [94913] {restore} # Loading data into document collection 'Movies', data size: 68107 byte(s)\n", + "\u001b[0m\u001b[0m2022-08-05T20:32:44Z [308] INFO [94913] {restore} # Loading data into document collection 'Users', data size: 16717 byte(s)\n", + "\u001b[0m\u001b[0m2022-08-05T20:32:44Z [308] INFO [6ae09] {restore} # Successfully restored document collection 'Users'\n", + "\u001b[0m\u001b[0m2022-08-05T20:32:44Z [308] INFO [94913] {restore} # Loading data into edge collection 'Ratings', data size: 1407601 byte(s)\n", + "\u001b[0m\u001b[0m2022-08-05T20:32:44Z [308] INFO [6ae09] {restore} # Successfully restored document collection 'Movies'\n", + "\u001b[0m\u001b[0m2022-08-05T20:32:49Z [308] INFO [75e65] {restore} # Current restore progress: restored 2 of 3 collection(s), read 9270558 byte(s) from datafiles, sent 3 data batch(es) of 881948 byte(s) total size, queued jobs: 0, workers: 2\n", + "\u001b[0m\u001b[0m2022-08-05T20:32:52Z [308] INFO [69a73] {restore} # Still loading data into edge collection 'Ratings', 10660073 byte(s) restored\n", + "\u001b[0m\u001b[0m2022-08-05T20:32:52Z [308] INFO [6ae09] {restore} # Successfully restored edge collection 'Ratings'\n", + "\u001b[0m\u001b[0m2022-08-05T20:32:52Z [308] INFO [a66e1] {restore} Processed 3 collection(s) in 9.925065 s, read 11542023 byte(s) from datafiles, sent 4 data batch(es) of 11542020 byte(s) total size\n", "\u001b[0m" ] } ], "source": [ "!chmod -R 755 dgl-adapter/\n", - "!./dgl-adapter/tests/assets/arangorestore -c none --server.endpoint http+ssl://{con[\"hostname\"]}:{con[\"port\"]} --server.username {con[\"username\"]} --server.database {con[\"dbName\"]} --server.password {con[\"password\"]} --replication-factor 3 --input-directory \"dgl-adapter/examples/data/fraud_dump\" --include-system-collections true" + "!./dgl-adapter/tests/tools/arangorestore -c none --server.endpoint http+ssl://{con[\"hostname\"]}:{con[\"port\"]} --server.username {con[\"username\"]} --server.database {con[\"dbName\"]} --server.password {con[\"password\"]} --replication-factor 3 --input-directory \"dgl-adapter/tests/data/adb/imdb_dump\" --include-system-collections true" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "XLiXYJPRlVYZ", + "outputId": "2666c5b3-1f62-4bfc-c9af-53bc53f0ffd8" + }, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Create the IMDB graph\n", + "db.delete_graph(\"imdb\", ignore_missing=True)\n", + "db.create_graph(\n", + " \"imdb\",\n", + " edge_definitions=[\n", + " {\n", + " \"edge_collection\": \"Ratings\",\n", + " \"from_vertex_collections\": [\"Users\"],\n", + " \"to_vertex_collections\": [\"Movies\"],\n", + " },\n", + " ],\n", + ")" ] }, { @@ -537,20 +555,20 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "oG496kBeeG9A", - "outputId": "792a3ad2-3d04-4132-d878-a5e52c58dc17" + "outputId": "e5d8657f-a644-4493-ca16-16a300ac4a87" }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - "[2022/05/25 17:23:34 +0000] [60] [INFO] - adbdgl_adapter: Instantiated ADBDGL_Adapter with database 'TUT56z6dbtgsoeu5cc6aixs7d'\n" + "[2022/08/05 20:33:59 +0000] [61] [INFO] - adbdgl_adapter: Instantiated ADBDGL_Adapter with database 'TUTk9nlikuz4zowwxfkusway'\n" ] } ], @@ -561,130 +579,134 @@ { "cell_type": "markdown", "metadata": { - "id": "uByvwf9feG9A" + "id": "bvzJXSHHTi3v" }, "source": [ - "# ArangoDB to DGL\n", - "\n" + "# DGL to ArangoDB" ] }, { "cell_type": "markdown", "metadata": { - "id": "ZrEDmtqCVD0W" + "id": "UafSB_3JZNwK" }, "source": [ - "#### Via ArangoDB Graph" + "#### Karate Graph" ] }, { "cell_type": "markdown", "metadata": { - "id": "H8nlvWCryPW0" + "id": "tx-tjPfx0U_h" }, "source": [ - "Data source\n", - "* ArangoDB Fraud-Detection Graph\n", + "Data\n", + "* [DGL Karate Graph](https://docs.dgl.ai/en/0.6.x/api/python/dgl.data.html#karate-club-dataset)\n", "\n", - "Package methods used\n", - "* [`adbdgl_adapter.adapter.arangodb_graph_to_dgl()`](https://github.com/arangoml/dgl-adapter/blob/2.0.0/adbdgl_adapter/adapter.py#L198-L213)\n", + "API\n", + "* `adbdgl_adapter.adapter.dgl_to_arangodb()`\n", "\n", - "Important notes\n", - "* The `name` parameter in this case must point to an existing ArangoDB graph in your ArangoDB instance. " + "Notes\n", + "* The `name` parameter in this case is simply for naming your ArangoDB graph." ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": { "colab": { - "base_uri": "https://localhost:8080/" + "base_uri": "https://localhost:8080/", + "height": 577, + "referenced_widgets": [ + "61d2a0426c324309ab51111933276e3d", + "77c208846c1e4503bc22a5b5504f89ee", + "2d1fc41d509e481cb779603827359184", + "87d9c9de620847f48b4088e8577cd653" + ] }, - "id": "zZ-Hu3lLVHgd", - "outputId": "d1c38c22-eebb-456d-8e4c-140ddd9baed8" + "id": "eRVbiBy4ZdE4", + "outputId": "74ac6cb8-824b-443a-ad6e-9f36b23060a1" }, "outputs": [ { - "name": "stderr", - "output_type": "stream", - "text": [ - "[2022/05/25 17:23:40 +0000] [60] [INFO] - adbdgl_adapter: Created DGL 'fraud-detection' Graph\n" - ] + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "61d2a0426c324309ab51111933276e3d", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "--------------------\n", - "Graph(num_nodes={'account': 54, 'customer': 17},\n", - " num_edges={('account', 'accountHolder', 'customer'): 54, ('account', 'transaction', 'account'): 62},\n", - " metagraph=[('account', 'customer', 'accountHolder'), ('account', 'account', 'transaction')])\n", - "['account', 'customer']\n", - "['accountHolder', 'transaction']\n" - ] - } - ], - "source": [ - "# Define graph name\n", - "graph_name = \"fraud-detection\"\n", - "\n", - "# Create DGL graph from ArangoDB graph\n", - "dgl_g = adbdgl_adapter.arangodb_graph_to_dgl(graph_name)\n", - "\n", - "# You can also provide valid Python-Arango AQL query options to the command above, like such:\n", - "# dgl_g = aadbdgl_adapter.arangodb_graph_to_dgl(graph_name, ttl=1000, stream=True)\n", - "# See more here: https://docs.python-arango.com/en/main/specs.html#arango.aql.AQL.execute\n", - "\n", - "# Show graph data\n", - "print('\\n--------------------')\n", - "print(dgl_g)\n", - "print(dgl_g.ntypes)\n", - "print(dgl_g.etypes)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "RQ4CknYfUEuz" - }, - "source": [ - "#### Via ArangoDB Collections" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "bRcCmqWGy1Kf" - }, - "source": [ - "Data source\n", - "* ArangoDB Fraud-Detection Collections\n", - "\n", - "Package methods used\n", - "* [`adbdgl_adapter.adapter.arangodb_collections_to_dgl()`](https://github.com/arangoml/dgl-adapter/blob/2.0.0/adbdgl_adapter/adapter.py#L169-L196)\n", - "\n", - "Important notes\n", - "* The `name` parameter in this case is simply for naming your DGL graph.\n", - "* The `vertex_collections` & `edge_collections` parameters must point to existing ArangoDB collections within your ArangoDB instance." - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "2d1fc41d509e481cb779603827359184", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" }, - "id": "i4XOpdRLUNlJ", - "outputId": "4d53a3d0-316b-40c2-d841-5fb29fa1358b" - }, - "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - "[2022/05/25 17:23:46 +0000] [60] [INFO] - adbdgl_adapter: Created DGL 'fraud-detection' Graph\n" + "[2022/08/05 20:34:04 +0000] [61] [INFO] - adbdgl_adapter: Created ArangoDB 'Karate' Graph\n" ] }, { @@ -693,467 +715,345 @@ "text": [ "\n", "--------------------\n", - "Graph(num_nodes={'Class': 4, 'account': 54, 'customer': 17},\n", - " num_edges={('Class', 'Relationship', 'Class'): 4, ('account', 'accountHolder', 'customer'): 54, ('account', 'transaction', 'account'): 62},\n", - " metagraph=[('Class', 'Class', 'Relationship'), ('account', 'customer', 'accountHolder'), ('account', 'account', 'transaction')])\n", - "['Class', 'account', 'customer']\n", - "['Relationship', 'accountHolder', 'transaction']\n" + "URL: https://tutorials.arangodb.cloud:8529\n", + "Username: TUT6h05us6483maimfr7o28jq\n", + "Password: TUTis4noysrzjeig2bqpdccaa\n", + "Database: TUTk9nlikuz4zowwxfkusway\n", + "--------------------\n", + "\n", + "View the created graph here: https://tutorials.arangodb.cloud:8529/_db/TUTk9nlikuz4zowwxfkusway/_admin/aardvark/index.html#graph/Karate\n", + "\n", + "View the original graph below:\n", + "\n" ] + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" } ], "source": [ - "# Define collection names\n", - "vertex_collections = {\"account\", \"Class\", \"customer\"}\n", - "edge_collections = {\"accountHolder\", \"Relationship\", \"transaction\"}\n", + "# Create the DGL graph & draw it\n", + "dgl_karate_graph = KarateClubDataset()[0]\n", + "nx.draw(dgl_karate_graph.to_networkx(), with_labels=True)\n", "\n", - "# Create DGL from ArangoDB collections\n", - "dgl_g = adbdgl_adapter.arangodb_collections_to_dgl(\"fraud-detection\", vertex_collections, edge_collections)\n", + "name = \"Karate\"\n", "\n", - "# You can also provide valid Python-Arango AQL query options to the command above, like such:\n", - "# dgl_g = adbdgl_adapter.arangodb_collections_to_dgl(\"fraud-detection\", vertex_collections, edge_collections, ttl=1000, stream=True)\n", - "# See more here: https://docs.python-arango.com/en/main/specs.html#arango.aql.AQL.execute\n", + "# Delete the graph if it already exists\n", + "db.delete_graph(name, drop_collections=True, ignore_missing=True)\n", + "\n", + "# Create the ArangoDB graph\n", + "adb_karate_graph = adbdgl_adapter.dgl_to_arangodb(name, dgl_karate_graph)\n", + "\n", + "# You can also provide valid Python-Arango Import Bulk options to the command above, like such:\n", + "# adb_karate_graph = adbdgl_adapter.dgl_to_arangodb(name, dgl_karate_graph, batch_size=5, on_duplicate=\"replace\")\n", + "# See the full parameter list at https://docs.python-arango.com/en/main/specs.html#arango.collection.Collection.import_bulk\n", "\n", - "# Show graph data\n", "print('\\n--------------------')\n", - "print(dgl_g)\n", - "print(dgl_g.ntypes)\n", - "print(dgl_g.etypes)" + "print(\"URL: \" + con[\"url\"])\n", + "print(\"Username: \" + con[\"username\"])\n", + "print(\"Password: \" + con[\"password\"])\n", + "print(\"Database: \" + con[\"dbName\"])\n", + "print('--------------------\\n')\n", + "print(f\"View the created graph here: {con['url']}/_db/{con['dbName']}/_admin/aardvark/index.html#graph/{name}\\n\")\n", + "print(f\"View the original graph below:\\n\")" ] }, { "cell_type": "markdown", "metadata": { - "id": "qEH6OdSB23Ya" + "id": "CNj1xKhwoJoL" }, "source": [ - "#### Via ArangoDB Metagraph" + "\n", + "#### FakeHeterogeneous Graph" ] }, { "cell_type": "markdown", "metadata": { - "id": "PipFzJ0HzTMA" + "id": "CZ1UX9YX1Zzo" }, "source": [ - "Data source\n", - "* ArangoDB Fraud-Detection Collections\n", + "Data\n", + "* A fake DGL Heterogeneous graph\n", "\n", - "Package methods used\n", - "* [`adbdgl_adapter.adapter.arangodb_to_dgl()`](https://github.com/arangoml/dgl-adapter/blob/2.0.0/adbdgl_adapter/adapter.py#L70-L167)\n", + "API\n", + "* `adbdgl_adapter.adapter.dgl_to_arangodb()`\n", "\n", - "Important notes\n", - "* The `name` parameter in this case is simply for naming your DGL graph.\n", - "* The `metagraph` parameter should contain collections & associated document attributes names that exist within your ArangoDB instance." + "Notes\n", + "* The `name` parameter is used to name your ArangoDB graph." ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": { "colab": { - "base_uri": "https://localhost:8080/" + "base_uri": "https://localhost:8080/", + "height": 408, + "referenced_widgets": [ + "3fc8b14d794a46118b328893bd216405", + "c7e222474ff445fe86e4e599848b2ae2", + "289a6e16c3d640c29d96edf09908bd0f", + "61f3832c906445a3ab7e7ba9b41c0127", + "99bbe81a24db49ff9352987fd97649cd", + "21e50aa61c3d4de19b5cc0bbe27d53c9", + "f9fdfe6ce44e4e1c8f513f82efca3e0d", + "9b2b3abbe2c04af0bc232c9b16bfd90d", + "8444e147be8f44aba06ec1f8a880104e", + "80e69b3aa98b44e295efe3940c1146c2", + "ec7b8b0b853f463fa079dda845891391", + "dd2376f84c794b4989f385a5bb147bd8" + ] }, - "id": "7Kz8lXXq23Yk", - "outputId": "7804e7ba-3760-4eb5-8669-f6fa20948262" + "id": "jbJsvMMaoJoT", + "outputId": "c1606984-c2ef-41c1-e8b1-78a4ae40d93c" }, "outputs": [ { - "name": "stderr", + "name": "stdout", "output_type": "stream", "text": [ - "[2022/05/25 17:23:50 +0000] [60] [INFO] - adbdgl_adapter: Created DGL 'FraudDetection' Graph\n" + "Graph(num_nodes={'game': 5, 'topic': 3, 'user': 4},\n", + " num_edges={('user', 'follows', 'topic'): 2, ('user', 'follows', 'user'): 2, ('user', 'plays', 'game'): 2},\n", + " metagraph=[('user', 'topic', 'follows'), ('user', 'user', 'follows'), ('user', 'game', 'plays')])\n" ] }, { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "--------------\n", - "Graph(num_nodes={'Class': 4, 'account': 54, 'customer': 17},\n", - " num_edges={('Class', 'Relationship', 'Class'): 4, ('account', 'accountHolder', 'customer'): 54, ('account', 'transaction', 'account'): 62},\n", - " metagraph=[('Class', 'Class', 'Relationship'), ('account', 'customer', 'accountHolder'), ('account', 'account', 'transaction')])\n", - "\n", - "--------------\n", - "defaultdict(, {'concrete': {'Class': tensor([True, True, True, True])}, 'customer_id': {'account': tensor([10000009, 10000004, 10000004, 10000010, 10000002, 10000011, 10000015,\n", - " 10000006, 10000010, 10810, 10000002, 10000014, 10000008, 0,\n", - " 10000002, 0, 10000008, 10000006, 10000012, 10000015, 10000001,\n", - " 10000010, 10000015, 10000005, 10000009, 10000008, 10000011, 10000014,\n", - " 10000010, 10000006, 10000002, 10000007, 10000006, 10000005, 0,\n", - " 10000010, 10810, 0, 10000009, 10000006, 10000002, 10000005,\n", - " 10000009, 10000012, 10000007, 10000002, 10000014, 0, 10810,\n", - " 10000016, 10000006, 10000016, 10000013, 10810])}, 'Balance': {'account': tensor([5331, 7630, 1433, 2201, 4837, 5817, 1689, 1042, 4104, 10, 2338, 10,\n", - " 3779, 0, 529, 0, 1992, 2912, 6367, 1819, 0, 221, 5062, 2372,\n", - " 841, 5393, 1138, 8414, 4064, 5686, 6294, 6540, 7358, 3452, 0, 3993,\n", - " 10, 0, 471, 8148, 5832, 1758, 1747, 1679, 6789, 1599, 8320, 0,\n", - " 10, 8626, 7199, 8644, 3879, 10])}, 'rank': {'account': tensor([0.0021, 0.0031, 0.0052, 0.0021, 0.0046, 0.0037, 0.0032, 0.0042, 0.0021,\n", - " 0.0021, 0.0030, 0.0037, 0.0040, 0.0037, 0.0021, 0.0046, 0.0040, 0.0030,\n", - " 0.0026, 0.0032, 0.0021, 0.0034, 0.0032, 0.0021, 0.0021, 0.0035, 0.0026,\n", - " 0.0026, 0.0046, 0.0021, 0.0021, 0.0035, 0.0036, 0.0036, 0.0038, 0.0055,\n", - " 0.0021, 0.0041, 0.0044, 0.0021, 0.0030, 0.0035, 0.0033, 0.0026, 0.0071,\n", - " 0.0036, 0.0032, 0.0059, 0.0021, 0.0090, 0.0057, 0.0032, 0.0026, 0.0021]), 'customer': tensor([0.0135, 0.0050, 0.0062, 0.0066, 0.0096, 0.0088, 0.0089, 0.0047, 0.0066,\n", - " 0.0045, 0.0062, 0.0103, 0.0081, 0.0039, 0.0054, 0.0044, 0.0093])}})\n", - "--------------\n", - "\n", - "defaultdict(, {'receiver_bank_id': {('account', 'transaction', 'account'): tensor([10000000003, 10000000003, 10000000001, 10000000002, 10000000002,\n", - " 10000000003, 10000000001, 10000000003, 10000000001, 10000000003,\n", - " 10000000002, 10000000003, 0, 10000000003, 10000000003,\n", - " 0, 10000000001, 0, 10000000002, 10000000003,\n", - " 10000000003, 10000000003, 10000000001, 0, 10000000003,\n", - " 10000000002, 10000000003, 10000000003, 10000000001, 10000000001,\n", - " 10000000003, 10000000003, 10000000003, 10000000003, 10000000001,\n", - " 10000000002, 0, 10000000001, 10000000001, 10000000002,\n", - " 10000000001, 10000000003, 10000000003, 10000000003, 10000000001,\n", - " 10000000003, 10000000002, 10000000003, 10000000002, 10000000001,\n", - " 10000000003, 0, 10000000003, 10000000003, 0,\n", - " 10000000003, 10000000002, 10000000002, 10000000001, 10000000003,\n", - " 10000000003, 10000000003])}, 'sender_bank_id': {('account', 'transaction', 'account'): tensor([10000000003, 10000000002, 10000000001, 10000000001, 10000000002,\n", - " 10000000003, 10000000003, 10000000002, 10000000002, 10000000003,\n", - " 10000000001, 10000000001, 0, 10000000003, 10000000003,\n", - " 0, 10000000002, 0, 10000000001, 10000000003,\n", - " 10000000001, 10000000003, 10000000002, 0, 10000000003,\n", - " 10000000003, 10000000003, 10000000003, 10000000001, 10000000001,\n", - " 10000000002, 10000000001, 10000000003, 10000000003, 10000000001,\n", - " 10000000001, 0, 10000000003, 10000000002, 10000000001,\n", - " 10000000002, 10000000003, 10000000003, 10000000003, 10000000002,\n", - " 10000000003, 10000000002, 10000000003, 10000000002, 10000000001,\n", - " 10000000001, 0, 10000000003, 10000000003, 0,\n", - " 10000000003, 10000000003, 10000000001, 10000000001, 10000000003,\n", - " 10000000003, 10000000002])}, 'transaction_amt': {('account', 'transaction', 'account'): tensor([9000, 299, 498, 954, 756, 627, 142, 946, 920, 9000, 421, 343,\n", - " 9000, 457, 9000, 9000, 53, 9000, 284, 120, 441, 9000, 364, 901,\n", - " 9000, 279, 9000, 9000, 273, 127, 952, 354, 795, 9000, 835, 761,\n", - " 9000, 478, 172, 804, 665, 995, 9000, 9000, 670, 9000, 340, 9000,\n", - " 747, 347, 52, 911, 762, 9000, 0, 790, 619, 491, 954, 9000,\n", - " 9000, 843])}})\n" - ] - } - ], - "source": [ - "# Define Metagraph\n", - "fraud_detection_metagraph = {\n", - " \"vertexCollections\": {\n", - " \"account\": {\"rank\", \"Balance\", \"customer_id\"},\n", - " \"Class\": {\"concrete\"},\n", - " \"customer\": {\"rank\"},\n", - " },\n", - " \"edgeCollections\": {\n", - " \"accountHolder\": {},\n", - " \"Relationship\": {},\n", - " \"transaction\": {\"receiver_bank_id\", \"sender_bank_id\", \"transaction_amt\"},\n", - " },\n", - "}\n", - "\n", - "# Create DGL Graph from attributes\n", - "dgl_g = adbdgl_adapter.arangodb_to_dgl('FraudDetection', fraud_detection_metagraph)\n", - "\n", - "# You can also provide valid Python-Arango AQL query options to the command above, like such:\n", - "# dgl_g = adbdgl_adapter.arangodb_to_dgl(graph_name = 'FraudDetection', fraud_detection_metagraph, ttl=1000, stream=True)\n", - "# See more here: https://docs.python-arango.com/en/main/specs.html#arango.aql.AQL.execute\n", - "\n", - "# Show graph data\n", - "print('\\n--------------')\n", - "print(dgl_g)\n", - "print('\\n--------------')\n", - "print(dgl_g.ndata)\n", - "print('--------------\\n')\n", - "print(dgl_g.edata)" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "DqIKT1lO4ASw" - }, - "source": [ - "#### Via ArangoDB Metagraph with a custom controller and verbose logging" - ] - }, - { - "cell_type": "markdown", - "metadata": { - "id": "PGkGh_KjzlYM" - }, - "source": [ - "Data source\n", - "* ArangoDB Fraud-Detection Collections\n", - "\n", - "Package methods used\n", - "* [`adbdgl_adapter.adapter.arangodb_to_dgl()`](https://github.com/arangoml/dgl-adapter/blob/2.0.0/adbdgl_adapter/adapter.py#L70-L167)\n", - "* [`adbdgl_adapter.controller._adb_attribute_to_dgl_feature()`](https://github.com/arangoml/dgl-adapter/blob/2.0.0/adbdgl_adapter/controller.py#L21-L47)\n", - "\n", - "Important notes\n", - "* The `name` parameter in this case is simply for naming your DGL graph.\n", - "* The `metagraph` parameter should contain collections & associated document attributes names that exist within your ArangoDB instance.\n", - "* We are creating a custom `ADBDGL_Controller` to specify *how* to convert our ArangoDB vertex/edge attributes into DGL node/edge features. View the default `ADBDGL_Controller` [here](https://github.com/arangoml/dgl-adapter/blob/2.0.0/adbdgl_adapter/controller.py#L11)." - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "metadata": { - "colab": { - "base_uri": "https://localhost:8080/" + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "3fc8b14d794a46118b328893bd216405", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" }, - "id": "U4_vSdU_4AS4", - "outputId": "8af82665-9ae6-40d4-ada2-248edd993291" - }, - "outputs": [ { - "name": "stderr", - "output_type": "stream", - "text": [ - "[2022/05/25 17:23:56 +0000] [60] [INFO] - adbdgl_adapter: Instantiated ADBDGL_Adapter with database 'TUT56z6dbtgsoeu5cc6aixs7d'\n", - "[2022/05/25 17:23:56 +0000] [60] [DEBUG] - adbdgl_adapter: Starting arangodb_to_dgl(FraudDetection, ...):\n", - "[2022/05/25 17:23:56 +0000] [60] [DEBUG] - adbdgl_adapter: Preparing 'account' vertices\n", - "[2022/05/25 17:23:56 +0000] [60] [DEBUG] - adbdgl_adapter: Preparing 'Class' vertices\n", - "[2022/05/25 17:23:57 +0000] [60] [DEBUG] - adbdgl_adapter: Preparing 'customer' vertices\n", - "[2022/05/25 17:23:57 +0000] [60] [DEBUG] - adbdgl_adapter: Preparing 'accountHolder' edges\n", - "[2022/05/25 17:23:57 +0000] [60] [DEBUG] - adbdgl_adapter: Preparing 'Relationship' edges\n", - "[2022/05/25 17:23:57 +0000] [60] [DEBUG] - adbdgl_adapter: Preparing 'transaction' edges\n", - "[2022/05/25 17:23:57 +0000] [60] [DEBUG] - adbdgl_adapter: Is graph 'FraudDetection' homogenous? False\n", - "[2022/05/25 17:23:57 +0000] [60] [DEBUG] - adbdgl_adapter: Inserting 54 'rank' features into 'account'\n", - "[2022/05/25 17:23:57 +0000] [60] [DEBUG] - adbdgl_adapter: Inserting 17 'rank' features into 'customer'\n", - "[2022/05/25 17:23:57 +0000] [60] [DEBUG] - adbdgl_adapter: Inserting 4 'name' features into 'Class'\n", - "[2022/05/25 17:23:57 +0000] [60] [DEBUG] - adbdgl_adapter: Inserting 4 'concrete' features into 'Class'\n", - "[2022/05/25 17:23:57 +0000] [60] [DEBUG] - adbdgl_adapter: Inserting 17 'Ssn' features into 'customer'\n", - "[2022/05/25 17:23:57 +0000] [60] [DEBUG] - adbdgl_adapter: Inserting 17 'Sex' features into 'customer'\n", - "[2022/05/25 17:23:57 +0000] [60] [DEBUG] - adbdgl_adapter: Inserting 62 'trans_time' features into 'transaction'\n", - "[2022/05/25 17:23:57 +0000] [60] [DEBUG] - adbdgl_adapter: Inserting 62 'transaction_amt' features into 'transaction'\n", - "[2022/05/25 17:23:57 +0000] [60] [DEBUG] - adbdgl_adapter: Inserting 62 'receiver_bank_id' features into 'transaction'\n", - "[2022/05/25 17:23:57 +0000] [60] [DEBUG] - adbdgl_adapter: Inserting 62 'transaction_date' features into 'transaction'\n", - "[2022/05/25 17:23:57 +0000] [60] [DEBUG] - adbdgl_adapter: Inserting 62 'sender_bank_id' features into 'transaction'\n", - "[2022/05/25 17:23:57 +0000] [60] [INFO] - adbdgl_adapter: Created DGL 'FraudDetection' Graph\n" - ] + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
         },
         {
-          "name": "stdout",
-          "output_type": "stream",
-          "text": [
-            "\n",
-            "--------------\n",
-            "Graph(num_nodes={'Class': 4, 'account': 54, 'customer': 17},\n",
-            "      num_edges={('Class', 'Relationship', 'Class'): 4, ('account', 'accountHolder', 'customer'): 54, ('account', 'transaction', 'account'): 62},\n",
-            "      metagraph=[('Class', 'Class', 'Relationship'), ('account', 'customer', 'accountHolder'), ('account', 'account', 'transaction')])\n",
-            "\n",
-            "--------------\n",
-            "defaultdict(, {'name': {'Class': tensor([0, 1, 2, 3])}, 'concrete': {'Class': tensor([True, True, True, True])}, 'rank': {'account': tensor([0.0021, 0.0031, 0.0052, 0.0021, 0.0046, 0.0037, 0.0032, 0.0042, 0.0021,\n",
-            "        0.0021, 0.0030, 0.0037, 0.0040, 0.0037, 0.0021, 0.0046, 0.0040, 0.0030,\n",
-            "        0.0026, 0.0032, 0.0021, 0.0034, 0.0032, 0.0021, 0.0021, 0.0035, 0.0026,\n",
-            "        0.0026, 0.0046, 0.0021, 0.0021, 0.0035, 0.0036, 0.0036, 0.0038, 0.0055,\n",
-            "        0.0021, 0.0041, 0.0044, 0.0021, 0.0030, 0.0035, 0.0033, 0.0026, 0.0071,\n",
-            "        0.0036, 0.0032, 0.0059, 0.0021, 0.0090, 0.0057, 0.0032, 0.0026, 0.0021]), 'customer': tensor([0.0135, 0.0050, 0.0062, 0.0066, 0.0096, 0.0088, 0.0089, 0.0047, 0.0066,\n",
-            "        0.0045, 0.0062, 0.0103, 0.0081, 0.0039, 0.0054, 0.0044, 0.0093])}, 'Ssn': {'customer': tensor([123456786, 123456780, 123456780, 123456787, 123456780, 123456789,\n",
-            "        123456780, 123456785, 123456783, 123456784, 123456780, 123456788,\n",
-            "        123456782, 123456781, 123456780, 123456780, 111223333])}, 'Sex': {'customer': tensor([1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 1, 0, 0, 0, 1])}})\n",
-            "--------------\n",
-            "\n",
-            "defaultdict(, {'trans_time': {('account', 'transaction', 'account'): tensor([1136, 1516, 1340, 1030, 1552, 1116, 1450,  924, 1046, 1426, 1247, 1459,\n",
-            "           0, 1459, 1258,    0, 1758,    0, 1230, 1210, 1252, 1039, 1741,    0,\n",
-            "        1420, 1713, 1710, 1028, 1636, 1054, 1658, 1332, 1316,  955, 1629, 1642,\n",
-            "           0, 1710,  932, 1652, 1018, 1527, 1555, 1640, 1158, 1035, 1015, 1133,\n",
-            "        1320, 1514, 1213,    0, 1133, 1340,    0, 1026, 1312, 1027, 1745, 1342,\n",
-            "        1520, 1141])}, 'transaction_amt': {('account', 'transaction', 'account'): tensor([9000,  299,  498,  954,  756,  627,  142,  946,  920, 9000,  421,  343,\n",
-            "        9000,  457, 9000, 9000,   53, 9000,  284,  120,  441, 9000,  364,  901,\n",
-            "        9000,  279, 9000, 9000,  273,  127,  952,  354,  795, 9000,  835,  761,\n",
-            "        9000,  478,  172,  804,  665,  995, 9000, 9000,  670, 9000,  340, 9000,\n",
-            "         747,  347,   52,  911,  762, 9000,    0,  790,  619,  491,  954, 9000,\n",
-            "        9000,  843])}, 'receiver_bank_id': {('account', 'transaction', 'account'): tensor([10000000003, 10000000003, 10000000001, 10000000002, 10000000002,\n",
-            "        10000000003, 10000000001, 10000000003, 10000000001, 10000000003,\n",
-            "        10000000002, 10000000003,           0, 10000000003, 10000000003,\n",
-            "                  0, 10000000001,           0, 10000000002, 10000000003,\n",
-            "        10000000003, 10000000003, 10000000001,           0, 10000000003,\n",
-            "        10000000002, 10000000003, 10000000003, 10000000001, 10000000001,\n",
-            "        10000000003, 10000000003, 10000000003, 10000000003, 10000000001,\n",
-            "        10000000002,           0, 10000000001, 10000000001, 10000000002,\n",
-            "        10000000001, 10000000003, 10000000003, 10000000003, 10000000001,\n",
-            "        10000000003, 10000000002, 10000000003, 10000000002, 10000000001,\n",
-            "        10000000003,           0, 10000000003, 10000000003,           0,\n",
-            "        10000000003, 10000000002, 10000000002, 10000000001, 10000000003,\n",
-            "        10000000003, 10000000003])}, 'transaction_date': {('account', 'transaction', 'account'): tensor([  201966,   201721,  2017528,  2018924,  2017516,  2018128,  2019213,\n",
-            "          201847,  2017914,   201966,  2017810, 20181020,        0,  2017724,\n",
-            "          201966,        0,  2019311,        0,  2018211,  2018125,   201932,\n",
-            "          201966,   201795,        0,   201966,  2017111,   201966,   201966,\n",
-            "         2019822,  2017317,  2019124,  2017121,  2017110,   201966,  2017717,\n",
-            "        20181012,        0, 20181023,  2019724,  2019611,  2019928,  2019117,\n",
-            "          201966,   201966,  2017328,   201966,  2019316,   201966,  2017914,\n",
-            "         2017521,   201713,        0,  2018124,   201966,        0,   201784,\n",
-            "          201713, 20171212,  2019413,   201966,   201966,   201887])}, 'sender_bank_id': {('account', 'transaction', 'account'): tensor([10000000003, 10000000002, 10000000001, 10000000001, 10000000002,\n",
-            "        10000000003, 10000000003, 10000000002, 10000000002, 10000000003,\n",
-            "        10000000001, 10000000001,           0, 10000000003, 10000000003,\n",
-            "                  0, 10000000002,           0, 10000000001, 10000000003,\n",
-            "        10000000001, 10000000003, 10000000002,           0, 10000000003,\n",
-            "        10000000003, 10000000003, 10000000003, 10000000001, 10000000001,\n",
-            "        10000000002, 10000000001, 10000000003, 10000000003, 10000000001,\n",
-            "        10000000001,           0, 10000000003, 10000000002, 10000000001,\n",
-            "        10000000002, 10000000003, 10000000003, 10000000003, 10000000002,\n",
-            "        10000000003, 10000000002, 10000000003, 10000000002, 10000000001,\n",
-            "        10000000001,           0, 10000000003, 10000000003,           0,\n",
-            "        10000000003, 10000000003, 10000000001, 10000000001, 10000000003,\n",
-            "        10000000003, 10000000002])}})\n"
-          ]
-        }
-      ],
-      "source": [
-        "# Define Metagraph\n",
-        "fraud_detection_metagraph = {\n",
-        "    \"vertexCollections\": {\n",
-        "        \"account\": {\"rank\"},\n",
-        "        \"Class\": {\"concrete\", \"name\"},\n",
-        "        \"customer\": {\"Sex\", \"Ssn\", \"rank\"},\n",
-        "    },\n",
-        "    \"edgeCollections\": {\n",
-        "        \"accountHolder\": {},\n",
-        "        \"Relationship\": {},\n",
-        "        \"transaction\": {\"receiver_bank_id\", \"sender_bank_id\", \"transaction_amt\", \"transaction_date\", \"trans_time\"},\n",
-        "    },\n",
-        "}\n",
-        "\n",
-        "# A user-defined Controller class is REQUIRED when converting non-numerical\n",
-        "# ArangoDB attributes to DGL features.\n",
-        "class FraudDetection_ADBDGL_Controller(ADBDGL_Controller):\n",
-        "    \"\"\"ArangoDB-DGL controller.\n",
-        "\n",
-        "    Responsible for controlling how ArangoDB attributes\n",
-        "    are converted into DGL features, and vice-versa.\n",
-        "\n",
-        "    You can derive your own custom ADBDGL_Controller if you want to maintain\n",
-        "    consistency between your ArangoDB attributes & your DGL features.\n",
-        "    \"\"\"\n",
-        "\n",
-        "    def _adb_attribute_to_dgl_feature(self, key: str, col: str, val):\n",
-        "        \"\"\"\n",
-        "        Given an ArangoDB attribute key, its assigned value (for an arbitrary document),\n",
-        "        and the collection it belongs to, convert it to a valid\n",
-        "        DGL feature: https://docs.dgl.ai/en/0.6.x/guide/graph-feature.html.\n",
-        "\n",
-        "        NOTE: You must override this function if you want to transfer non-numerical\n",
-        "        ArangoDB attributes to DGL (DGL only accepts 'attributes' (a.k.a features)\n",
-        "        of numerical types). Read more about DGL features here:\n",
-        "        https://docs.dgl.ai/en/0.6.x/new-tutorial/2_dglgraph.html#assigning-node-and-edge-features-to-graph.\n",
-        "\n",
-        "        :param key: The ArangoDB attribute key name\n",
-        "        :type key: str\n",
-        "        :param col: The ArangoDB collection of the ArangoDB document.\n",
-        "        :type col: str\n",
-        "        :param val: The assigned attribute value of the ArangoDB document.\n",
-        "        :type val: Any\n",
-        "        :return: The attribute's representation as a DGL Feature\n",
-        "        :rtype: Any\n",
-        "        \"\"\"\n",
-        "        try:\n",
-        "          if col == \"transaction\":\n",
-        "            if key == \"transaction_date\":\n",
-        "              return int(str(val).replace(\"-\", \"\"))\n",
-        "    \n",
-        "            if key == \"trans_time\":\n",
-        "              return int(str(val).replace(\":\", \"\"))\n",
-        "    \n",
-        "          if col == \"customer\":\n",
-        "            if key == \"Sex\":\n",
-        "              return {\n",
-        "                  \"M\": 0,\n",
-        "                  \"F\": 1\n",
-        "              }.get(val, -1)\n",
-        "\n",
-        "            if key == \"Ssn\":\n",
-        "              return int(str(val).replace(\"-\", \"\"))\n",
-        "\n",
-        "          if col == \"Class\":\n",
-        "            if key == \"name\":\n",
-        "              return {\n",
-        "                  \"Bank\": 0,\n",
-        "                  \"Branch\": 1,\n",
-        "                  \"Account\": 2,\n",
-        "                  \"Customer\": 3\n",
-        "              }.get(val, -1)\n",
-        "\n",
-        "        except (ValueError, TypeError, SyntaxError):\n",
-        "          return 0\n",
-        "\n",
-        "        # Rely on the parent Controller as a final measure\n",
-        "        return super()._adb_attribute_to_dgl_feature(key, col, val)\n",
-        "\n",
-        "# Instantiate the new adapter\n",
-        "fraud_adbdgl_adapter = ADBDGL_Adapter(db, FraudDetection_ADBDGL_Controller())\n",
-        "\n",
-        "# You can also change the adapter's logging level for access to \n",
-        "# silent, regular, or verbose logging (logging.WARNING, logging.INFO, logging.DEBUG)\n",
-        "fraud_adbdgl_adapter.set_logging(logging.DEBUG) # verbose logging\n",
-        "\n",
-        "# Create DGL Graph from attributes\n",
-        "dgl_g = fraud_adbdgl_adapter.arangodb_to_dgl('FraudDetection',  fraud_detection_metagraph)\n",
-        "\n",
-        "# You can also provide valid Python-Arango AQL query options to the command above, like such:\n",
-        "# dgl_g = fraud_adbdgl_adapter.arangodb_to_dgl(graph_name = 'FraudDetection',  fraud_detection_metagraph, ttl=1000, stream=True)\n",
-        "# See more here: https://docs.python-arango.com/en/main/specs.html#arango.aql.AQL.execute\n",
-        "\n",
-        "# Show graph data\n",
-        "print('\\n--------------')\n",
-        "print(dgl_g)\n",
-        "print('\\n--------------')\n",
-        "print(dgl_g.ndata)\n",
-        "print('--------------\\n')\n",
-        "print(dgl_g.edata)"
-      ]
-    },
-    {
-      "cell_type": "markdown",
-      "metadata": {
-        "id": "bvzJXSHHTi3v"
-      },
-      "source": [
-        "# DGL to ArangoDB"
-      ]
-    },
-    {
-      "cell_type": "markdown",
-      "metadata": {
-        "id": "UafSB_3JZNwK"
-      },
-      "source": [
-        "#### Karate Graph"
-      ]
-    },
-    {
-      "cell_type": "markdown",
-      "metadata": {
-        "id": "tx-tjPfx0U_h"
-      },
-      "source": [
-        "Data source\n",
-        "* [DGL Karate Graph](https://docs.dgl.ai/en/0.6.x/api/python/dgl.data.html#karate-club-dataset)\n",
-        "\n",
-        "Package methods used\n",
-        "* [`adbdgl_adapter.adapter.dgl_to_arangodb()`](https://github.com/arangoml/dgl-adapter/blob/2.0.0/adbdgl_adapter/adapter.py#L215-L311)\n",
-        "\n",
-        "Important notes\n",
-        "* The `name` parameter in this case is simply for naming your ArangoDB graph."
-      ]
-    },
-    {
-      "cell_type": "code",
-      "execution_count": 10,
-      "metadata": {
-        "colab": {
-          "base_uri": "https://localhost:8080/",
-          "height": 683
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "289a6e16c3d640c29d96edf09908bd0f", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "99bbe81a24db49ff9352987fd97649cd", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "f9fdfe6ce44e4e1c8f513f82efca3e0d", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "8444e147be8f44aba06ec1f8a880104e", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "ec7b8b0b853f463fa079dda845891391", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" }, - "id": "eRVbiBy4ZdE4", - "outputId": "c629be2d-1bc9-4539-c7f2-d3ae46676659" - }, - "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - "[2022/05/25 17:24:04 +0000] [60] [DEBUG] - adbdgl_adapter: Starting dgl_to_arangodb(Karate, ...):\n", - "[2022/05/25 17:24:04 +0000] [60] [DEBUG] - adbdgl_adapter: Is graph 'Karate' using default canonical_etypes? True\n", - "[2022/05/25 17:24:04 +0000] [60] [DEBUG] - adbdgl_adapter: Is graph 'Karate' homogenous? True\n", - "[2022/05/25 17:24:04 +0000] [60] [DEBUG] - adbdgl_adapter: Preparing 34 'Karate_N' DGL nodes\n", - "[2022/05/25 17:24:04 +0000] [60] [DEBUG] - adbdgl_adapter: Preparing 156 'Karate_E' DGL edges\n", - "[2022/05/25 17:24:04 +0000] [60] [DEBUG] - adbdgl_adapter: Inserting last 34 documents into 'Karate_N'\n", - "[2022/05/25 17:24:05 +0000] [60] [DEBUG] - adbdgl_adapter: Inserting last 156 documents into 'Karate_E'\n", - "[2022/05/25 17:24:05 +0000] [60] [INFO] - adbdgl_adapter: Created ArangoDB 'Karate' Graph\n" + "[2022/08/05 20:35:24 +0000] [61] [INFO] - adbdgl_adapter: Created ArangoDB 'FakeHetero' Graph\n", + "INFO:adbdgl_adapter:Created ArangoDB 'FakeHetero' Graph\n" ] }, { @@ -1163,41 +1063,39 @@ "\n", "--------------------\n", "URL: https://tutorials.arangodb.cloud:8529\n", - "Username: TUTtj3263blez70kmqdi3ts\n", - "Password: TUTf6tursgxqogdo3ww3nplb\n", - "Database: TUT56z6dbtgsoeu5cc6aixs7d\n", + "Username: TUT6h05us6483maimfr7o28jq\n", + "Password: TUTis4noysrzjeig2bqpdccaa\n", + "Database: TUTk9nlikuz4zowwxfkusway\n", "--------------------\n", "\n", - "View the created graph here: https://tutorials.arangodb.cloud:8529/_db/TUT56z6dbtgsoeu5cc6aixs7d/_admin/aardvark/index.html#graph/Karate\n", - "\n", + "View the created graph here: https://tutorials.arangodb.cloud:8529/_db/TUTk9nlikuz4zowwxfkusway/_admin/aardvark/index.html#graph/FakeHetero\n", "\n", "View the original graph below:\n", "\n" ] - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" } ], "source": [ - "# Create the DGL graph & draw it\n", - "dgl_karate_graph = KarateClubDataset()[0]\n", - "nx.draw(dgl_karate_graph.to_networkx(), with_labels=True)\n", + "# Create the DGL graph\n", + "hetero_graph = dgl.heterograph({\n", + " (\"user\", \"follows\", \"user\"): (torch.tensor([0, 1]), torch.tensor([1, 2])),\n", + " (\"user\", \"follows\", \"topic\"): (torch.tensor([1, 1]), torch.tensor([1, 2])),\n", + " (\"user\", \"plays\", \"game\"): (torch.tensor([0, 3]), torch.tensor([3, 4])),\n", + "})\n", + "hetero_graph.nodes[\"user\"].data[\"features\"] = torch.tensor([21, 44, 16, 25])\n", + "hetero_graph.nodes[\"user\"].data[\"label\"] = torch.tensor([1, 2, 0, 1])\n", + "hetero_graph.nodes[\"game\"].data[\"features\"] = torch.tensor([[0, 0], [0, 1], [1, 0], [1, 1], [1, 1]])\n", + "hetero_graph.edges[(\"user\", \"plays\", \"game\")].data[\"features\"] = torch.tensor([[6, 1], [1000, 0]])\n", "\n", - "name = \"Karate\"\n", + "print(hetero_graph)\n", + "\n", + "name = \"FakeHetero\"\n", "\n", "# Delete the graph if it already exists\n", "db.delete_graph(name, drop_collections=True, ignore_missing=True)\n", "\n", - "# Create the ArangoDB graph\n", - "adb_karate_graph = adbdgl_adapter.dgl_to_arangodb(name, dgl_karate_graph)\n", + "# Create the ArangoDB graphs\n", + "adb_hetero_graph = adbdgl_adapter.dgl_to_arangodb(name, hetero_graph)\n", "\n", "print('\\n--------------------')\n", "print(\"URL: \" + con[\"url\"])\n", @@ -1212,98 +1110,74 @@ { "cell_type": "markdown", "metadata": { - "id": "gshTlSX_ZZsS" + "id": "n08RC_GtkDrC" }, "source": [ "\n", - "#### MiniGCDataset Graphs" + "#### FakeHeterogeneous Graph with a DGL-ArangoDB metagraph" ] }, { "cell_type": "markdown", "metadata": { - "id": "KaExiE2x0-M6" + "id": "rUD_y0yxkDrK" }, "source": [ - "Data source\n", - "* [DGL Mini Graph Classification Dataset](https://docs.dgl.ai/en/0.6.x/api/python/dgl.data.html#mini-graph-classification-dataset)\n", + "Data\n", + "* A fake DGL Heterogeneous graph\n", "\n", - "Package methods used\n", - "* [`adbdgl_adapter.adapter.dgl_to_arangodb()`](https://github.com/arangoml/dgl-adapter/blob/2.0.0/adbdgl_adapter/adapter.py#L215-L311)\n", + "API\n", + "* `adbdgl_adapter.adapter.dgl_to_arangodb()`\n", "\n", - "Important notes\n", - "* The `name` parameters in this case are simply for naming your ArangoDB graph." + "Notes\n", + "* The `name` parameter is used to name your ArangoDB graph.\n", + "* The `metagraph` parameter is an optional object mapping the DGL keys of the node & edge data to strings, list of strings, or user-defined functions." ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "metadata": { "colab": { "base_uri": "https://localhost:8080/", - "height": 1000 + "height": 408, + "referenced_widgets": [ + "345a5984959c4e57b7e2715fa8eeef8f", + "99e6613c4187459396eea503453934cb", + "968020b1388e4883843575d9198af1cd", + "f1a08470110e4099af2a3d4cf4d0f956", + "6744eb60dfa04a8598fca3b998ce3077", + "09d25097c75c4fa8a2c7376f1965afc5", + "cb8167f00277413eaaa2ad6e0e162fab", + "8128e6d80fcb4a8ca0a72097bb8b6521", + "575205f1a4e64c5d977e69d4939a5605", + "d20843bfa9064d56b37aaea011789a26", + "8bf075c6f7834d3fa905b7ddc37cf128", + "b080f26fe35241fb9cca48e97bc9ef0c" + ] }, - "id": "dADiexlAioGH", - "outputId": "9921ec34-b860-49e8-f8cb-0b403029ead4" + "id": "xAdjZiJ8kDrK", + "outputId": "2822ed4b-8199-48e2-a753-4b1f60d648a0" }, "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "[2022/05/25 17:24:48 +0000] [60] [DEBUG] - adbdgl_adapter: Starting dgl_to_arangodb(Lollipop, ...):\n", - "[2022/05/25 17:24:48 +0000] [60] [DEBUG] - adbdgl_adapter: Is graph 'Lollipop' using default canonical_etypes? True\n", - "[2022/05/25 17:24:48 +0000] [60] [DEBUG] - adbdgl_adapter: Is graph 'Lollipop' homogenous? True\n", - "[2022/05/25 17:24:48 +0000] [60] [DEBUG] - adbdgl_adapter: Preparing 7 'Lollipop_N' DGL nodes\n", - "[2022/05/25 17:24:48 +0000] [60] [DEBUG] - adbdgl_adapter: Preparing 24 'Lollipop_E' DGL edges\n", - "[2022/05/25 17:24:48 +0000] [60] [DEBUG] - adbdgl_adapter: Inserting last 7 documents into 'Lollipop_N'\n", - "[2022/05/25 17:24:48 +0000] [60] [DEBUG] - adbdgl_adapter: Inserting last 24 documents into 'Lollipop_E'\n", - "[2022/05/25 17:24:48 +0000] [60] [INFO] - adbdgl_adapter: Created ArangoDB 'Lollipop' Graph\n", - "[2022/05/25 17:24:48 +0000] [60] [DEBUG] - adbdgl_adapter: Starting dgl_to_arangodb(Hypercube, ...):\n", - "[2022/05/25 17:24:48 +0000] [60] [DEBUG] - adbdgl_adapter: Is graph 'Hypercube' using default canonical_etypes? True\n", - "[2022/05/25 17:24:49 +0000] [60] [DEBUG] - adbdgl_adapter: Is graph 'Hypercube' homogenous? True\n", - "[2022/05/25 17:24:49 +0000] [60] [DEBUG] - adbdgl_adapter: Preparing 8 'Hypercube_N' DGL nodes\n", - "[2022/05/25 17:24:49 +0000] [60] [DEBUG] - adbdgl_adapter: Preparing 24 'Hypercube_E' DGL edges\n", - "[2022/05/25 17:24:49 +0000] [60] [DEBUG] - adbdgl_adapter: Inserting last 8 documents into 'Hypercube_N'\n", - "[2022/05/25 17:24:49 +0000] [60] [DEBUG] - adbdgl_adapter: Inserting last 24 documents into 'Hypercube_E'\n", - "[2022/05/25 17:24:49 +0000] [60] [INFO] - adbdgl_adapter: Created ArangoDB 'Hypercube' Graph\n", - "[2022/05/25 17:24:49 +0000] [60] [DEBUG] - adbdgl_adapter: Starting dgl_to_arangodb(Clique, ...):\n", - "[2022/05/25 17:24:49 +0000] [60] [DEBUG] - adbdgl_adapter: Is graph 'Clique' using default canonical_etypes? True\n", - "[2022/05/25 17:24:49 +0000] [60] [DEBUG] - adbdgl_adapter: Is graph 'Clique' homogenous? True\n", - "[2022/05/25 17:24:49 +0000] [60] [DEBUG] - adbdgl_adapter: Preparing 6 'Clique_N' DGL nodes\n", - "[2022/05/25 17:24:49 +0000] [60] [DEBUG] - adbdgl_adapter: Preparing 30 'Clique_E' DGL edges\n", - "[2022/05/25 17:24:49 +0000] [60] [DEBUG] - adbdgl_adapter: Inserting last 6 documents into 'Clique_N'\n", - "[2022/05/25 17:24:49 +0000] [60] [DEBUG] - adbdgl_adapter: Inserting last 30 documents into 'Clique_E'\n", - "[2022/05/25 17:24:49 +0000] [60] [INFO] - adbdgl_adapter: Created ArangoDB 'Clique' Graph\n" - ] - }, { "name": "stdout", "output_type": "stream", "text": [ - "\n", - "--------------------\n", - "URL: https://tutorials.arangodb.cloud:8529\n", - "Username: TUTtj3263blez70kmqdi3ts\n", - "Password: TUTf6tursgxqogdo3ww3nplb\n", - "Database: TUT56z6dbtgsoeu5cc6aixs7d\n", - "--------------------\n", - "\n", - "\\View the created graphs here:\n", - "\n", - "1) https://tutorials.arangodb.cloud:8529/_db/TUT56z6dbtgsoeu5cc6aixs7d/_admin/aardvark/index.html#graph/Lollipop\n", - "2) https://tutorials.arangodb.cloud:8529/_db/TUT56z6dbtgsoeu5cc6aixs7d/_admin/aardvark/index.html#graph/Hypercube\n", - "3) https://tutorials.arangodb.cloud:8529/_db/TUT56z6dbtgsoeu5cc6aixs7d/_admin/aardvark/index.html#graph/Clique\n", - "\n", - "View the original graphs below:\n", - "\n" + "Graph(num_nodes={'game': 5, 'topic': 3, 'user': 4},\n", + " num_edges={('user', 'follows', 'topic'): 2, ('user', 'follows', 'user'): 2, ('user', 'plays', 'game'): 2},\n", + " metagraph=[('user', 'topic', 'follows'), ('user', 'user', 'follows'), ('user', 'game', 'plays')])\n" ] }, { "data": { - "image/png": "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", + "application/vnd.jupyter.widget-view+json": { + "model_id": "345a5984959c4e57b7e2715fa8eeef8f", + "version_major": 2, + "version_minor": 0 + }, "text/plain": [ - "
" + "Output()" ] }, "metadata": {}, @@ -1311,55 +1185,299 @@ }, { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAb4AAAEuCAYAAADx63eqAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4yLjIsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+WH4yJAAAgAElEQVR4nOzdeUDUdf4/8OccwHDDDMwMiAJegCia4pl3a+aRrmWm6eZq5ZZZdny71iPdcn+6ZWWux9qdWqurVp6Z5X2geCdyCCrIPQfDMMx9/P6wmSTAAwbeM595Pf7aYJh5jgs8+Xw+7/frw3M4HA4QQgghPoLPOgAhhBDSmqj4CCGE+BQqPkIIIT6Fio8QQohPoeIjhBDiU6j4CCGE+BQqPkIIIT6Fio8QQohPoeIjhBDiU6j4CCGE+BQqPkIIIT6Fio8QQohPoeIjhBDiU6j4CCGE+BQqPkIIIT6Fio8QQohPoeIjhBDiU6j4CCGE+BQqPkIIIT6Fio8QQohPoeIjhBDiU6j4CCGE+BQh6wCEEO+g1Jmw5Uwxcsq10BqtCBMJkSwPw2O94iAJCWAdj5C7xnM4HA7WIQghnuvCDQ1WHczHoTwFAMBktbs+JxLy4QAwNCkas4d0RPe2EYxSEnL3qPgIIY3akHEdS3bnwGi14Xa/KXg8QCQUYN7oZEzrl9Bq+QhpCjrVSQhp0M3Sy4bBYr/jYx0OwGCxYcnubACg8iMejRa3EELquXBDgyW7cxotPYu6BIXvTYByx/t1Pm6w2LFkdw4uFmtaIyYhTULFRwipZ9XBfBittkY/r/5pLQJiOjX4OaPVhtUH81sqGiHNRsVHCKlDqTPhUJ6i0Wt6tZcPgS8Khii+e4OfdziAA7kKqHSmFkxJSNNR8RHio6xWK7Zt24aampo6H99yprjRr7Gb9NAc2YjI4U/f9rl5ALacbfx5CGGJio8QH1VYWIiJEydCKpXiiSeewIkTJ+BwOJBTrq2zZeFWmsPrEdL9QQjDom773EarHTllNbd9DCGs0KpOQnyE3W5HSUkJsrKykJeXh9zcXACA0WjEt99+i2+//RYCgQDxf/l/gLxLva83V1yFsfACYmasuKvX0xotbs1PiLtQ8RHi5aqqqpCVlYXc3FwUFBSgsLAQpaWlqKysRFVVFWpqamA0GmG1WgEAfn5+CAwMRGhoKJzbeAUCAUQiEV5++WUoOg7Ejznqeq9jLPoV1uoKFK+eAQBwmI2Aw44y5dwGyzBM5NeC75qQpqPiI8QDmUwm5OTkICcnB1euXMH169dRXFyMiooKqFQqaLVaGAwGmM1mAL8XV0hICCIjIxEdHY1u3bqhXbt2aN++PZKSktClSxfIZLI6ryOXy1FVVYXnn38e7777LoKCgrD2UAEO5Gvqne4M6TESwSmDXf+tPbUN1uoKiEc+Xy+/SMhHckxoC/zLENJ8VHyEtBK73Y7CwkLXqcZr167hxo0bKCsrg0qlgkajQW1tLcxmM+x2O3g8HgICAhAcHIzw8HBERUUhPj4eAwcOREJCAjp37ozU1FQkJCSAz2/a5foPPvgAaWlp6Nq1q+tjE3vF4cOf8+o9lu8nAvxErv/m+YnAE/pDEBRe77EOABN7xjUpEyEtjUaWEdJMCoWizqnGoqIilJaWQqFQoKqqCjqdDkajETbbzX1x/v7+CAwMRFhYGCQSCWQyGeLi4hAfH49OnTohOTkZycnJEIlEd3jlljNr/Wnsy6647ZiyRjnscNy4gEmx1ejRowfkcjnkcjmSkpIgFNLf2oQ9Kj5CGqDX63H58mXk5OQgPz8fhYWFKC4uRmVlJdRqtetUo8VycwGHUCiESCRCaGgoIiMjIZVKERsbi3bt2qFDhw5ITk5Gly5dIBaLGb+zu3PhhgaTP8mAwdL4JvbGiPz4qNj4FjRXL0AoFCI4OBharRbr16/H1KlTWyAtIfeGio/4DKvVioKCAmRnZ7tONRYXF6O8vBxKpRJardZ1qtHhcIDP5yMgIAAhISGIiIhAVFQU5HI52rZti/bt27tONcbFxTX5VKMnu5dZnU6BfnzMG52CPmIzunXr5roGKZFIUFJSgoAAun0RYY+Kj3g1h8OB8vJy16nGq1evuk41KpVKaDQa16lG53Uzf39/BAUF1TvVmJiYiE6dOiElJQWdOnWCv78/67fHXHPuzvDmm29ixYoVsFgssNls6NevH3bs2IGoqNvvASSkpVHxEY9UU1ODrKws16nGoqIilJSUuE411tTUwGAwuJboC4VC1xJ9sVgMqVSKNm3aID4+Hh07dkRycjJSUlIQFhbG+J15n4vFGqw+mI8DuQrwcHNzupPzfnzDkqIxe2hHpMX9fj8+g8GADh06YNCgQZg3bx4efvhhlJaW4u2338b8+fNb/40Q8hsqPtJqzGYz8vLykJ2djfz8fNepRucS/erqauj1elgsFjgcDggEgjqnGqOjoxETE+O6bpaUlITU1FTI5XLWb80nqHQmbDlbjLc/WIeY+PYYOqAvkmNCMbFn43dgLy0tRWRkJAIDAwEAS5YswaJFixATE4Pt27ejR48erfkWCAHgo8Wn1Jmw5Uwxcsq10BqtCBMJkSwPw2O9Gv8BJg2z2+0oLi52LdG/evWqa4m+81RjbW0tTCZTnVONziX6EonEdd0sMTERnTt3RpcuXZCYmEgrAD3Qvn378OCDDyIqKgqVlZXg8Xj3/BxKpRLjxo1DRkYGHn/8caxfv57+vyatyqeK78INDVYdzMehPAUA1Nmg6zxlMzQpGrOHdET3thGNPItvUKvVrlWNDU0D0el0MBgMriX6zmkgYWFhEIvFkMlkaNOmDRISEtCxY0ekpKQgOTkZQUFBjN8ZaSqbzYbOnTvj6tWr8Pf3x759+zB48OA7f2Ejvv/+ezz55JNwOBz44osvMHHiRDemJaRxPlN8zblIzxVGoxE5OTnIzs52TQMpKSlBRUWFa4m+81Qj8Ps0kNDQUERERDS6RJ8WK/iGzz//HC+++CJqa2sBAMOHD8cvv/zSrOe0Wq2YOXMmNmzYgPT0dOzcuRNSqdQdcQlplE8UX3OWZXt6+dntdly7dg1ZWVm4cuUKrl69iuLi4jrTQPR6PUwmk2uJvvNUo3OJfkxMTL0l+m3btuXkEn3SdCNHjsRPP/0E4OYRvtVqhVqtRkRE88+OXLp0CQ8//DCKi4sxb948LFq0qNnPSUhjOF98jW3EtRlqoNq9Asbr58APDEPkkOkITh1a5zGBfgJsmtWvzkq11lJZWYlLly65lugXFhairKwMCoWizhJ9m80GHo8HPz+/BpfoJyQkoFOnTujSpQs6d+5MS/RJs1itVgQEBOCHH35Az549ERsb69bnX7p0KRYuXAipVIrt27ejZ8+ebn1+QgAfKL7GRi8pfvgX4HBAMvpFmCuuonLLYsinvQf/6HjXY3g8YGQXGdZOS3d97Mcff0RmZiYWLFhwz1l0Oh2ys7NdqxoLCwsbXKLf0DQQ5xL92NhY1xJ95+Bhd/zFTcjd4vP5yM7ORlJSUos8v1qtxvjx43Hs2DE8+uij2LhxI/3BRtyK08Wn1Jlw/7L99abM281G3PhoMmKfXgU/cZubj92xHIJQCSKH/rXOYwOEfBx/YzjMNWrMmjUL+/btQ3BwMFQqFYCbfwFfuXIFly9frrNEv7y8vM4S/camgdy6RD8xMdFVZrGxsXSqkXgkHo8HlUrV4uPXdu7cialTp8Jms+GTTz7BlClTWvT1iO/g9BriLWeKG/y4VV0CHl/gKj0A8JMmwlT0a73HOhx2jJ7zD5zZsMy1gtFkMiEwMLDOFH3nNJBbl+inp6fXmwZCy7aJN3OOIGuNswxjx46FSqXCM888g6lTp+L999/Hrl27aN8maTZO/xbOKdfWO9oDALvFAF5AYJ2P8QOCYDcb6j3WbAMKq62ugnNurF6+fDl69+6NlJQUhISEtNh7IMSTKJVKAGi1sxFCoRBffPEFXn/9dTz88MOIi4vDG2+8gSVLlrTK6xNu4vS5NK3R2uDH+X6BcJjqlpzDpAffP7DBx4/580QUFxfjH//4B2QyGWw2G/70pz+hd+/eVHrEpyiVyiZtWm+ulJQU5Ofn41//+hfee+89xMbG4uTJk62eg3ADp4svTNTwAa1Q3AYOuw0WdYnrY+bKa/C7ZWFL3efxQ2xsLObPn4/S0lKcO3cOHTt2bJHMhHgypVIJgUDA7PVfeeUVVFZWIikpCf3798eECRNgNBqZ5SHeidPFlywPQ4Cw/lvk+4sQlNQfmiMbYTcbYSy+DH3+SQSnDqv3WJGQj+SY0N+/ls9Hjx49aOEJ8UlVVVVMiw+4eX3xwIED2LVrFw4ePAixWIwNGzYwzUS8C6d/e0/sFdfo58QPzobDakbxyqlQbn8Pkgdn19nK4OQAMLFn489DiC+pqqrymAVao0aNgkqlwrRp0zB9+nT06NEDxcUNL2gj5FacLr6okAAM6RyNhi5JCAJDIX10Ptq9uhVxs7+ot3kdAOCwIynUCrtBC7v97qe+EMJVGo0Gfn5+rGO48Pl8rFu3Djk5OdDr9UhISMDrr79OP6/ktjhdfADw/NCOEAmbdmqGZ7dh74evQC6Xw9/fH2KxGMOHD3dzQkK8R3V1tUduJu/UqRPy8vLwwQcfYMWKFYiNjcWJEydYxyIeivPF171tBOaNTkag37291UA/PhZP6A6p8Oadu202G7RaLWJiYlooKSGer7q6GgEBnnvrrhdffBEKhQJdu3bF/fffj4cffpgWv5B6OF98ADCtXwLmjU5BoJ+gwdOet+Lxbs7onDc6BU/2T8QPP/zguommzWbDlStXoFarWyE1IZ6npqbGo4sPAMLCwvDzzz9j7969OH78OMRiMb744gvWsYgH8YniA26W36ZZ/TCyiwwBQj549rp7/ERCPgKEfIzsIsOmWf1cd2Xo2bMnnnjiCQDAu+++i7KyMsjlcrz33nut/RYIYa6mpsb1h6CnGzFiBBQKBf7617/i6aefRlpaGoqKiljHIh6A07M6G3Pi7K94aPYizHh5PmqMVoSJ/JAcE4qJPRu+A7tGo8Hnn3+Ol19+GTweDwsXLsQ///lPxMfHY+fOnUhJSWHwLghpfQ899BCqqqq8bvN4QUEBxo4di7y8PMydOxfvv/8+bUnyYT5XfA6HA2lpabh06RIyMzORnp5+5y9qQHl5OcaMGYNz585h5syZWLduHf0gEc4bPHgwBAIBDhw4wDpKk6xevRqvvPIKwsLCsG3bNgwcOJB1JMKAz/2m/vzzz5GbmwsAzdr0KpfLcebMGXz99df45ptvIJFIsHfvXnfFJMQj6fV6BAcHs47RZLNnz4ZSqcR9992HwYMHY/To0dDr9axjkVbmU8V3/fp1zJ0713W/u40bN6K5B7zTpk2DWq3GkCFDMGrUKDzwwAPQ6XTuiEuIx9Hr9QgNDb3zAz1YSEgI9u7di19++QWnTp2CRCLBunXrWMcircinim/z5s0wGAzg8XgQCoVQqVQ4ffp0s59XJBLh+++/x7Fjx3Dp0iVIJBKsXr3aDYkJ8Swmk4kzg9mHDRuGyspKPPPMM3juueeQmpqK69evs45FWoFPFd/rr78OrVaLkJAQTJkyBe+//z7i4tw3jqx///4oKyvDnDlz8MILLyAlJQXXrl1z2/MTwprRaERYWBjrGG7D5/Px8ccf4+rVqwCADh064IUXXqDJLxznU8UHAMHBwdDr9XjmmWfwyiuvuH1DOp/Px/Lly3Ht2jUIBAJ07NgRL774Iv0gEU4wmUwIDw9nHcPt4uPjkZWVhbVr1+LTTz+FVCrF/v37WcciLcTniq+qqgo2mw19+vRp0ddp164dLl26hDVr1mDdunWQyWQ4fPhwi74mIS3NYrFwsvicnnnmGahUKvTt2xd/+tOfMHLkSLpmz0E+V3xHjx6Fn59fq02fmDVrFtRqNXr16oWhQ4dizJgxNEKJeC2LxYLIyEjWMVpUUFAQdu3ahcOHD+PcuXOIioqia/Yc43PFd/LkSURERLTqawYFBeHHH3/Ezz//jIyMDERGRuKrr75q1QyEuIPNZoNYLGYdo1UMHDgQ5eXleP755/HCCy8gOTkZBQUFrGMRN/C54vv1118RGxvL5LWHDx8OhUKBGTNmYObMmejevTtKS0uZZCGkKWw2G+eP+G516zV7f39/dO7cGc899xxds/dyPld8BQUF6NixI7PX5/P5WL16NXJycmAwGNCuXTu89dZbzPIQci/sdjuioqJYx2h17dq1w8WLF/Hpp5/iyy+/RHR0NPbt28c6Fmkinyu+8vJypKWlsY7hun/Y8uXL8cEHHyA2NhaZmZmsYxFyWw6HA9HR0axjMDNjxgxUVVVh4MCBGDlyJB544AFotVrWscg98rniq66uRr9+/VjHcJk7dy4UCgU6d+6Mvn37YuLEiTCbzaxjEVKP8/uyta+RexqRSIQffvgBx44dQ1ZWFqKjo/Hxxx+zjkXugU8VX21tLaxWKwYMGMA6Sh1hYWE4ePAgdu7ciZ9//hlisRibN29mHYuQOpRKJQDQMPbf9O/fH6WlpXjppZfwyiuvoHPnzsjLy2Mdi9wFn/oOPnHiBIRCoceOXBo9ejSUSiUmTpyIyZMno2/fvlAoFKxjEQLgZvHx7nQnZx/D5/OxbNkyFBUVITg4GCkpKXjmmWdo8YuH87ni8/RxS0KhEF9++SUuXryIiooKxMbG4t1332UdixCo1WoIBALWMTxSbGwszp07hy+//BIbN26ERCLBnj17WMcijfCp4rt48aLbR5S1lK5du+L69etYtGgRFi9ejPj4eFy6dIl1LOLDVCoVFd8d/OUvf4FarcawYcMwZswYDB06FBqNhnUs8gc+VXz5+fno0KED6xj3ZN68eSgrK0NMTAzS0tLw5JNPwmq1so5FfFBVVRWEQiHrGB5PJBJh27ZtOHnyJPLy8iCVSvHBBx+wjkVu4VPFV1JSgq5du7KOcc+ioqKQkZGBzZs3Y9u2bZBIJNi5cyfrWMTHVFdXw8/Pj3UMr9G7d2+Ulpbitddew+uvv44OHTogOzubdSwCHyu+6upq9O3bl3WMJps4cSLUajVGjhyJcePGYfDgwXQahbQajUYDf39/1jG8zpIlS1BcXIzIyEikpqZixowZtPiFMZ8pPrPZDLPZjIEDB7KO0iz+/v7YvHkzTp06hYKCAjqNQlqNVqul4msiuVyO06dPY+PGjfjf//4HsVhMZ20Y8pniO3XqFAQCAWcG7Kanp6OkpMR1GqVTp064cuUK61iEw7RaLUQiEesYXm3KlClQq9UYMWIExo0bh4EDB0KtVrOO5XN8pviOHz+O0NBQ1jHcbsmSJSgqKkJISAiSk5Px7LPP0mkU0iJqamoQGBjIOobX8/f3x//+9z9kZmbi+vXrkMvlWLZsGetYPsVniu/ChQuQyWSsY7QI5x6iL774Al9//TWio6Pxyy+/sI5FOKa2tpaO+NyoV69eKC4uxt///nfMmzcPiYmJtGWplfhM8eXl5SExMZF1jBb15JNPQq1WY8CAARgxYgQefPBBuns0cZva2loEBwezjsE5ixYtQmlpKaRSKW1ZaiU+U3wlJSVITU1lHaPFiUQi7NixA4cPH8b58+cRFRWF//znP6xjEQ7Q6/VUfC1EKpXi5MmT2LRpE7Zt2waxWIzvvvuOdSzO8pniU6vV6N27N+sYrcZ59+jnnnsOs2fPRmpqKgoLC1nHIl7MYDB47Jxbrnjssceg0WgwduxYPProoxgwYIBrODhxH58oPpvNBpPJhEGDBrGO0qr4fD4+/PBDXL16FQ6HA+3bt8crr7xCi19IkxiNRk4uEPM0QqEQ33zzDc6ePYvi4mLExMTQvF4384niO3/+PPh8PmJjY1lHYSI+Ph6XL1/Gv//9b6xevRoxMTE4duwY61jEyxiNRo8f8s4lPXr0QFFREd5++20sXrwY7dq1w/nz51nH4gSfKL5jx47RtQkAzz33HJRKJdLS0jBo0CCMHz8eRqORdSziJcxmMxUfA/Pnz0dZWRni4uLQs2dPPPHEE7T4pZl8ovjOnTsHqVTKOoZHCAkJwb59+7B3714cOXIEYrEYGzZsYB2LeAGz2ezzd19nJSoqCsePH8fWrVuxc+dOREZGYsuWLaxjeS2fKL7c3FzEx8ezjuFRRowYAaVSiWnTpmH69Ono2bMnysvLWcciHsxisSAyMpJ1DJ82YcIEqNVqTJgwAZMmTUKfPn1QWVnJOpbX8Yniu3HjBrp06cI6hsfh8/lYt24dLl++DK1Wi7i4OCxYsIB1LOKhbDYbHfF5AKFQiK+//hoXL16EQqFAmzZtsHjxYtaxvIpPFJ9KpUJ6ejrrGB4rKSkJ+fn5WLZsGZYtW4a4uDicPXuWdSziYWw2GyQSCesY5Dddu3bFtWvX8M477+Ddd9+ln9t7wPnis9vtMBgMGDx4MOsoHu/VV19FZWUlEhMTkZ6ejsmTJ9NFdOJit9sRFRXFOgb5gzfffBMVFRWun9tJkybBbDazjuXROF98OTk54PF4nB9X5i4RERE4cuQIvv/+e+zevRuRkZHYunUr61jEAzgcDjri81BisRhHjhzB9u3bsXfvXojFYmzatIl1LI/F+eI7evQogoKCWMfwOuPGjYNarcb48ePx2GOPoX///nT7FB/mPILgym29uGrs2LFQqVR47LHHMGXKFKSnp9OitQZwvvjOnDlDp2eaSCgUYsOGDTh//jxKSkogk8mwdOlS1rEIA86xWXw+539leD2hUIgvvvgCWVlZqKqqokVrDeD8d3F2djbatWvHOoZXS0tLQ1FRERYsWIAFCxYgMTERWVlZrGORVqRWq8Hj8VjHIPcgJSUFBQUFrkVrsbGxyMzMZB3LI3C++IqKipCSksI6BicsXLgQJSUliIqKQrdu3TBjxgya++kjlEolBAIB6xikCZyL1jp37oy+ffvikUce8fnFL5wvPqVSiZ49e7KOwRlSqRSZmZn49ttvsXnzZojFYuzZs4d1LNLCVCoVFZ8Xi4iIwMGDB7Fr1y4cOHAAkZGRPj2xifPFp9frMXDgQNYxOOfxxx9HVVUVHnjgAYwZMwbDhg2DVqtlHYu0EI1GA6FQyDoGaaZRo0ZBpVJh6tSpmD59Ou677z6UlpayjtXqOF18ztvx0KnOluHv74+tW7ciIyMDOTk5iI6OxsqVK1nHIi2Aio87bp3YVFtbi3bt2uHNN9+Ew+FgHa3VcLr4jhw5gsDAQFqJ1sL69OmDsrIyvPTSS3j55ZeRlJSEgoIC1rGIG2k0Gvj7+7OOQdwoKSkJeXl5WL58OT788EPExsbixIkTAIA9e/ZgzZo1jBO2HE43wunTp2nDbStatmwZrl+/joCAAHTu3BnPP/88LX7hCK1Wi4CAANYxSAuYO3cuFAoFunTpgvvvvx+jR4/G5MmT8fLLL+PKlSuNfp1SZ8LaQwV4adM5zPwqEy9tOoe1hwqg0plaMX3T8BwcPr594IEHYDAYcPz4cdZRfM5nn32GOXPmIDg4GFu2bMHQoUNZRyLNMGPGDBw9evS2vwiJ99u3bx/Gjh3rWvU5YMAAHD16tM5Wlgs3NFh1MB+H8hQAAJP19z9uRUI+HACGJkVj9pCO6N7WM4eac/qIr7CwEJ07d2Ydwyc99dRTUKlU6NOnD4YPH45Ro0ZBr9ezjkWaSKfTQSQSsY5BWlhYWFid+bwnTpzAqlWrXP+9IeM6Jn+SgX3ZFTBZ7XVKDwCMv33sp8sVmPxJBjZkXG+t6PeE08WnUChw3333sY7hs4KCgrB7924cPHgQmZmZkEgk+Oyzz1jHIk2g0+kQGBjIOgZpYVqtFr1790ZCQgKCg4PhcDjw4osvYufOndiQcR1LdmfDYLHhTucJHQ7AYLFhye5sjyw/Tp/qFAgEOHnyJN2SyAPY7XbMnTsXq1evRpcuXbB79260bduWdSxylwYPHgyBQIADBw6wjkJakdVqxf79+7H3dA52GzvDYLHVe0zt5UPQHPsWNq0CguBISMa8BFHbrq7PB/oJsGlWP6TFec5pT84e8ZWVlcFut6NHjx6soxDcXEK9cuVK5OXlwWKxICEhAa+99hotfvESer2ehr37IKFQiAcffBA1bQfAaK1feoZr51B18EtEjX4JbV/5H2RTl0IYIa/zGKPVhtUH81sr8l3hbPEdPnwYAQEBtPfIw3To0AE5OTlYsWIFPv74Y8TGxuLkyZOsY5E7MBgMCA0NZR2DMKDUmXAoT9Hg6c3qoxsRfv8UBLRJBo/HhzA0CsLQujcFcDiAA7kKj1rtydniy8zMRGRkJOsYpBFz5sxxLaHu378/JkyY4PPzAz2Z0Wik4uO45557Dt26dcN///tfWCwW18e3nClu8PEOuw2msnzY9dUoWfsMildNh/qnNbBb6hccD8CWsw0/DwucLb6srCzExcWxjkFuIywsDPv378euXbtw8OBBREZG4ttvv2UdizTAZDJR8XFcdXU1Ll26hJkzZ0IsFuPRRx/Fd999h/PXFfVWbwKArVYD2K3Q5x6DbNoyxMz4GOaKq6g+Xv8GuEarHTllNa3xNu4KZ88DXrt2jYZTewnn/MCnn34aU6dOxfLly7Fz507I5fI7fzFpFSaTCeHh4axjkD+w2+1Qq9UoLy9HWVkZKisroVAooFKpoFKpoNFooNFooNVqodPpUFtbC4PBAIPBALPZDIvFAqvVCpvt9+t3BoMBALBt2zZcvHgRnZ/+AA0dI/H8bg40CO31MIQhN29QHNr7z6g+vgmRQ56s93it0VLvY6xwtvgqKipoYYsX4fP5+Pzzz/Haa69hzJgxiIuLw/z587Fo0SLXY0wmE00PYcRsNiMiwnNW5Xkzu90OrVaLsrKyOmWlUChQVVUFtVqN6upqV1npdDro9XoYjUaYTCZXWdntdtd8TR6PB4FAAD8/P/j7+yMgIACBgYEICgpCSEgIQkNDIZVKERERgcjISERFRUEikUAmk0EqlSI2NhaffPIJlixZgqCgIAwePBirVq1C+/bt8dKmc8g6X3+QtUAUAsEfrufd7p6NYSI/9/5DNgNni6+mpgYDBgxgHYPco5SUFFy9ehVLly7FggUL8DEXVhgAACAASURBVNlnn2HHjh0oKSnBlClTkJOTg9jY2Aa/VqkzYcuZYuSUa6E1WhEmEiJZHobHesVBEkKF2RxWq9Xni0+n06G0tBTl5eUoLy+HUqmEQqGAWq1GVVWV68iqpqYGtbW1rrIyGo11jqz+WFZCobBeWQUHByM0NBTt2rVzlZVEIoFEIoFUKoVMJkNMTAxiYmLctto2KSkJqampWL16NQYPHuz6eLI8DAHC8gZPd4Z0+xNqzuxEYPtegEAIbeb3COrYu97jREI+kmM851Q5J/fxqdVqSCQSmEwmGqzrxdRqNcaOHYuMjAz4+/vDbDZj3Lhx+P777+s8jgsjlDxdQEAA/ve//2HcuHGso9wTvV7vOrKqqKiAQqGAUqmESqWqU1ZardZVVgaDASaTCWazucGy4vP58PPzg5+fn6usAgMDERISgpCQEISFhSEyMhIRERH1ykoulyMmJgZhYWGM/2XunlJnwv3L9jdYfA6bFeqf16H28iHwhH4ITh6EyGEzwBPW/b0bIOTj+BvDPeYPUE4W3/bt2/HYY4/BZPKc5bOk6UaPHu262a2/vz/27NmD4cOHA8Bv0yRyYLTefpoEjweIhALMG52Maf0SWiE1twiFQhw6dAj3339/i7+W2WyuU1YVFRX1yqq6uho1NTWu04B/LCur1VqvrIRCoausRCKR68jKWVYRERH1ykoqlUIul6NNmzYICwvz2Tu9zFp/GvuyK+44saUhPB4wsosMa6d5ziARTp7qPHnyJF2I54iioiLs3bsXAQEBrl9so0aNQnl5OXblVv82QunOm+BvHaEEgMrvHtntdojF4kY/b7VaXQss/lhWarW6XlnV1ta6TgM6F1nYbLY6Aw0EAoHrupWzrAIDA11lJZFI0KFDB0REREAsFiMqKgrR0dGusoqNjYVYLPbZsnKn54d2xJErygYnt9yJSCjA7KEdWyBV03HyiG/8+PEoLCzE+fPnWUchzWSz2bBv3z4UFxejtLQUp0+fxv79+xGdnI6A0W/AeEvpOawWqH5aDeP187AbdRBGyBE5ZDoCO9T9S9MTRyixYLfbUVlZidLSUlRUVDS4ItBZVidOnEB8fDzMZnOdsnIusnC69cjK39+/XlmFhoYiPDwckZGREIvFkEgkrrJyLrKIjo6mwRMe6PdZnXc/bSnQj495o1M87g9NTn53FRQUIDk5mXUM4gYCgQAPPfRQvY9PXrUfJ0sMdT7msNsgDI2C/ImlEIRHw1BwGoofliF25r8hjJC5HuccoeRJp17ulnP5unORRWVlJSorK+scWTW0fP3WFYE2m63O8nU+n19nRaBIJIJIJHKVVXBwMICbt/lyngZ0lpVzkYVcLqey4jhneXHh0gInv1PLy8sxadIk1jFIC1HqTDhXbqr3g8f3FyFi0FTXfwd17ANhuAym8vw6xXfrCCXnxfYDBw7gn//8JzZv3uz2iT/O5eslJSX1yqqxvVZ3s3xdKBS6VgTeWlbOFYEymcx1GlAikSAqKqpOWclksru61VB5eTliYmLozhoE0/olIC0uAqsP5mN/TiVMJiN4wt8XrDgXkw1LisbsoR099qwKJ4uvuroa/fv3Zx2DtJDGRij9ka22ChZ1Cfyj29X7nHOE0vAYO2bPno3jx4/DarWiuLjYVXw1NTUoLS117bW6taxuXRHo3Gvl3BjsLCuLxdLgXquGlq87VwTGx8fXWb7uvG4lk8kQGxsLuVze6sOilUrlbfdnEd+SFheBtdPSMefVN7H+2BXMfHn+b9uH/JAcE4qJPT1/+xDnrvHpdDqEhoaipqYGISEhrOOQFvDSpnP4voENtbdy2Kyo3Pw2hJExkDw0p+EHXT+Fwv/+o86HBAJBo2XV0PL10NBQ14rAhvZaOZeve/O4r4MHD2LEiBF15jcS31ZaWoqEhARYLBZkZmZ63a3fOHfEd/z4cQiFQio9DtMarbf9vMNhh3LnckAghHjEs40+LlIaC51EAq1WC6vVCn9/fyxevBgPP/ywa/k6HekAVVVVEAgErGMQD+FwOPD444+7/hDauHGj1xUf59b5ZmRk0FYGjgsTNf73msPhgGr3x7DVahA94e/gCRp/7NABfaFUKnH8+HH8+c9/htVqRUpKCrp06YLw8HAqvd+o1WpauEJc1qxZU+dWYhs3boS3nTjkXPFdvHiRhhtz3M0RSg1/66r3roJFdQPSiQvB92v8OsOtI5TS09Oxbds2KJVKjB49ukUyezONRkPFR1xEIhF69uwJHo8HHo8HhUKB/HzPutHsnXDuu7mgoAAdOnRgHYO0oIm94vDhz3n1Pm6troTu/I+AwA/FK//i+rj4oecRkjqszmMdACb2rHvbKl+fRdmY6upqGv1HXGbOnImZM2eCz+cjIyMDUqkU8fHxrGPdE84VX0lJCf3VznFRIQEY0jm63gglYbgU8W/uvOPX83g3l1t7+sozT1FdXU13xSB1FBcXw+FwID093Ssn43hf4jvQaDTo27cv6xikhT0/tCNEwqYtuPDEEUqeTKvVUvGROg4ePAiRSOSVpQdwrPic+6duvaUG4abubSMwb3QyAv3u7Vv45gilZI/dWOuJdDodAgMDWccgHiQzMxMSiYR1jCbj1KnOU6dOQSAQ0LUaH8GlEUqejIqP/FFWVhbatm3LOkaTcar4Tpw44dUbhcm9u3WE0oFcBXgAjA3cj8/TRyh5stra2lafFkM827Vr17z6zBqniu/ChQuQyWR3fiDhFOcIJZXOhC1ni7Fg+VqMGvcIxCGBXjNCyZPp9Xr6uSJ1VFZWolevXqxjNBmnii8vLw+JiYmsYxBGJCEB6Gi5DsX29/H8vMfQp08P1pE4wWAw0JkUUkdtbS0GDRrEOkaTcWpxS3FxMbp27co6BmHEbrdj1qxZAIClS5cyTsMdRqORRgASl8LCQjgcDnTr1o11lCbjVPFVVVWhd+/erGMQRtavX4+ysjIAwJ49e1BSUsI4ETeYTCaEhYWxjkE8xKFDhxAYGOi1WxkADhWf1WqFyWTy6guupOmMRiNefvllGAw3b05rsVjwwQcfME7FDSaTiebfEhdv38oAcKj4zp07Bz6fT3M6fZTNZsPjjz/umiRx33331bnLOGk6i8VCxUdcsrKy0K5d/XtcehPOLG45fvw4goODWccgjAQHB2PNmjVYuXIl8vLykJmZyToSZ1gsFrfflZ54r8LCQgwZMoR1jGbhzBHf+fPnIZVKWccgjKlUKhqv5WZWq5WKj7hUVlZ63f33/ogzxZebm4uEhATWMQhjGo2Gis/NbDYbxGIx6xjEQ3j7VgaAQ8V348YNdOnShXUMwphGo6HxWm5mt9u9fjEDcY9r167B4XAgNTWVdZRm4UzxqVQqrz/8Js2n1WppvJabORwOREdHs45BPMDhw4e9fisDwJHis9vtMBgMXn/4TZpPq9XSIic3slqtAOgmveSmU6dOISoqinWMZuNE8WVnZ4PH49G4MoLa2lqaMuJGSqUSACAUcmYBOGmGy5cve/VdGZw4UXxHjx6l01sEwM3io7mS7qNUKsHj8VjHIB6isLAQKSkprGM0GyeK7+zZs5w4/CbNp9frabO1G6nVaq+/nkPcx9vvyuDEie/o7OxsxMfHs45BPIDRaKTrUW6kUqnoNCdx0ev1nFhLwYniKyoq4sThN2k+o9FIm63dSK1WU/ERAEBBQQEcDgcnto1xoviUSiV69uzJOgbxAGazmTZbu1F1dTUVHwHAna0MAEeKT6/XY+DAgaxjEA9gsVhos7UbaTQa+Pv7s45BPEBmZiZn1lJ4ffFdvXoVDocDycnJrKMQD2C1Wjnzw+kJqquraQQcAXBzK4O335XByeuL78iRI5w5/CbNZ7PZIJPJWMfgjJqaGio+AgC4fv06J67vARwovtOnT9OpLeLicDjoLh1uVFNTQ7NPCYCbaym4sJUB4EDxcWWSAGk+s9kMALS4xY1qa2shEolYxyAeQK/XY/DgwaxjuIXXF19hYSGSkpJYxyAeoLKyEgCN13Kn2tpamn1KcOXKFTgcDs78rvX64qusrKStDATAze8FutbrXlR8BAAOHTqEoKAgzvx8ef27qK2txYABA1jHIB5AoVBAIBCwjsEpBoOBio/gzJkznFot7dXFV1paCrvdjh49erCOQjyAUqmk05xuZjQaERYWxjoGYezy5cucGgvp1cV35MgRBAQE0F/5BMDNuZJ+fn6sY3CCRqNBXl4e9Ho9/P39XQuHiG/iyl0ZnLy6+DIzM2kFH3FRq9W058xNXnnlFaSmpqK8vBz/+c9/IBKJcOnSJdaxCCMKhQLp6emsY7iNVxdfVlYW2rRpwzoG8RBVVVVUfG7y1FNPuf4t7XY7OnXqxJnNy+Te2O12Tm1lALy8+K5evYrOnTuzjkE8RHV1NW22dpMBAwa4run4+/tj7dq1nFnRR+7NlStXAACdOnVinMR9vPo7uaKiAt27d2cdg3iI6upqBAUFsY7BCTweD2+//TYAoHPnzhg2bBjjRIQVrm1lALy8+HQ6HW1lIC41NTW09N6NHnnkEfD5fCxevJh1FMLQmTNnEB0dzTqGW3nt2m+1Wg2bzYY+ffqwjkI8hE6nQ3h4OOsYXk+pM2HLmWLklGvRfsZ7+KU2DpWHCvBYrzhIQugaqq/Jzs7m1FYGwIuL7+jRo/D396d7hREXvV5Pc1ub4cINDVYdzMehPAUAwGS1A9FJ2HVZgV/yVPjw5zwMTYrG7CEd0b1tBOO0pLUUFhZi1KhRrGO4ldcW38mTJxERQT985Hd6vZ6O+JpoQ8Z1LNmdA6PVBoej/ueNVjsA4KfLFTicp8S80cmY1i+hdUMSJpRKJXr37s06hlt5bfH9+uuviI2NZR2DeBCDwUDF1wQ3Sy8bBov9jo91OACDxYYlu7MBgMqP47i4lQHw4uIrKCigfUWkDpPJhMjISNYxvMqFGxos2Z1Tr/TKN74JU2kuePybU5EEoRK0mfUf1+cNFjuW7M5BWlwE0uLozAtX5ebmgsfjcWorA+DFxVdeXo7JkyezjkE8iNlspkk+92jVwXwYrbYGPyd+8FmEdh/Z6NcarTasPpiPtdO4M9GD1HX48GFObhHyuu0M999/P2QyGdRqNY4ePYq1a9fCbr/zKRrCfRaLBRKJhHUMr6HUmXAoT9HgNb274XAAB3IVUOlM7g1GPMbp06c5t5UB8MLiS05Odt1w9KeffsI777wDHo/HOBXxBFarlZM/pM1lMBgwb948ZGVl1fn4ljPFt/06zcGvcGPFEyhf/xqMhRcbfAwPwJazt38e4r24uJUB8MLie/LJJ11jqQIDA7F161YqPgIAsNlskEqlrGN4HJVKhaVLl6J3797o378/du/eDbvdjpxy7c0tCw2IHDYDbZ79FHHPf4WQHg+hcus7sFSV1Xuc0WpHTllNS78FwkhRUREn11J43TW+gQMHuk5tzpkzB/369WOciHgKh8PhM8VnNptRXl6OiooKVFRUQKFQQKFQQK1WQ61WQ6PRQKvVoqamBtXV1bDb7TAYDMjIyMCYMWMgEAiQMH0ZIE1u8PkDYpNc/zuk2wOovXwIhoLT8Et/uN5jtUZLi71PwpZSqeTUXRmcvK74BAIB2rRpg+LiYrz77rus4xAP4bxfnKde47Pb7dBqtSgrK0N5eTkqKyuhUCigUqlcRVVdXY3q6mrU1NSgtrYWBoMBBoMBJpMJZrMZVqsVNputzjVtPp8PoVAIoVAIf39/iEQiiEQiBAcHIzg4GKGhoZBKpa7TnH5+fpBIJHjjjTeQHdEXe3PVd/cGeDwADV8MDBPRPRC5yPnH0tChQ1lHcTuvKb5bxyhF/vnvaOvPx+cnbtAYJQIAruu+7rwpcUNHVUqlEmq1GlVVVVCr1a6jKp1OB71eD4PBAKPRCJPJBIvFAqvVCrvdDsdvK0h4PB4EAgGEQiH8/PwQEBAAkUiEwMBABAcHIyQkBDKZDOHh4YiIiEBkZCQkEgmioqIglUohk8kgl8sRHR19T3ebd5bhRx99hKlTp4LP52PtoQIcLNDUO91pN+pgKs2FqF03gC9AbfZhmG5cgvhPs+o/r5CP5JjQ5v1DE490+fJl8Hg8tG/fnnUUt/P44mtwjJK/HEoAH/2cR2OUCICbxcfj8aDRaFBWVoaKigrXUZWzrJxHVc6yqq2tdZVVc4+qnGUVGRkJsViMqKgoREdHQyaTQSaTISYmBiEhIcz+ffbt24devXrVWZo+sVccPvw5r95jHXYbNIc3wKIuBnh8+EniEP3IfPiJ69/70gFgYs+4loxOGDly5AgntzIAAM/haOpi5pZ3pzFKTjweIBIKaIySF7JYLCgvL3cdWf3xqOrWsrrdUZXN9vtetLs5qgoNDXUdVYnFYtdRlbOs5HI5pFLpPR1VeaNZ609jX3ZF07Y0OBzoEcXDphf+RDcA5qCnn34av/zyC65du8Y6itt57E81jVHyTA6HA1qtFqWlpa6jKqVSWaesGjqqMhqNMBqNsFgssFgsDR5VCQQC+Pn51TmqCgoKQkhICEJCQiCVSl2n/5xlJZVKIZVKkZGRgYULF8JgMDD81/E+zw/tiCNXlDBYGt7Efjt+AmDP8pcgei0fgYGBiIqKQkxMDD7++GP07du3BdKS1pSdnY2EhATWMVqERxZfQ2OUtGd2oPbXX2BWXEdwyhBEjX253tfRGKWGWa1W11FVZWWlq6xUKlWdoyrntSrnwormXKsKDg5GdHQ0Onbs6Dqqcp4CvPValbuOqs6ePUt36miC7m0jMG908l3/kekU6MfHvNEpuBrwFyxatAgGgwE3btxAWVkZTc/hiKKiIowbN451jBbhkcXX0BglYYgE4QMeh+HaWTgs5ka/litjlJwrAMvKyuocVVVVVbmOqm5dAeg8qnJeq7qbo6qAgAAEBgYiKCjIda0qOjoaERERrrJynv6TSqWQy+WIiYlBaKjnLWaoqqqi021N5DxD0pTLCrbe8/Dtt98iJycHABAQEAC1+i5XihKPplKpOHdXBiePK77GxigFJd2807qpPB82i7LRr791jJIkJAB2ux0bNmzAmTNnsGLFihbL/cejKue1qj8uV791BeCtZdXYUZVzYYXz3oPO03/OU4DR0dHo0KFDvWtVzlOAcrkcMpmM89eqqqqqIBKJWMfwWtP6JSAtLgKrD+bjQK4CPPx+KyLg5upNB4BhSdGYPbSj64yKQCDA+vXr0b9/f8jlcnTs2BH9+/fH+PHjsWnTJjoK91LOrQxDhgxhHaVFeNxvwzuNUbobzjFKqfwyzJo1C9euXUNQUFC94nMeVf1xufqtR1W3Xquqra1t0lHVrSsAQ0JCkJiY6FoBGBkZWeeoSiaTITY2FmFhYc3+d/AlGo2Giq+Z0uIisHZaOlQ6E7acLUZOWQ20RgvCRH5IjgnFxJ4Nbx1KT0/H0qVLMWTIEKSnp2Pv3r2YPHkyIiMj8emnn2LKlCkM3g1pjqysLPB4PCQmJrKO0iI8rvhuN0bpbhmtdixe8SnKv1vmOnoyGo0QiUS3PapqbAWgRCJB+/btG9xXdetRlZ8fbeRlRavVcnbpdWuThATgb4M73NPXvPrqq67/PXLkSKhUKsyaNQtTp07F8uXLsXPnTsjlcndHJS3k8OHDCA4OZh2jxXhc8WmNVrc8T2CY2HV6z2KxgMfj4auvvnLtq3IeVdGcT26oqanh9A+qt+Hz+fj000/x6quvYuzYsYiLi8P8+fOxaNEi1tHIXThz5gynB7573JDqMJF7unjcQyOg1WqxZs0a1+H6hAkTMHToUKSkpCA8PJxKj0N0Op1HLrrxdSkpKSgoKMA///lPLFmyBG3btsX58+dZxyJ3kJOTw9mtDIAHFl+yPAwBwvqxHHYbHFYzYLcBDjscVjMc9ob3HjnHKIlEIjz11FMoKChAQUEBXWjnML1eT8XnwV5//XVUVFSgXbt26NmzJ5544glYre45u0Pc78aNG0hNTWUdo8V4XPFN7NXw+KPqY/9F0fuPQJuxBbVZB1D0/iOoPvbfBh/7xzFKXL5IS26qra1FeHg46xjkNsRiMY4dO4atW7dix44dEIvF+OGHH1jHIg3g8lYGwAOv8UWFBGBI5+h6Y5QiBk1FxKCpd/x6Hm4uuabB1b7FaDRS8XmJCRMmoKqqCk8++SQmTJiAAQMGYPv27bTx3UNwfSsD4IFHfMDNMUoiYdOm7NstRmyYNx0DBgzAjBkz8M4772DPnj1uTkg8jclkQmRkJOsY5C4JhUJ88803OHv2LAoLCyGXy/Hee++xjkUAXLx4ETwej5N3XnfyyOJzjlEK9Lu3eIF+fPylawhqiy7jxIkT+PLLL7Fo0SIsW7ashZIST2E2m+mIwQv16NEDN27cwFtvvYW33noLHTp0QG5uLutYPu3IkSOcXyHtkcUH3JwkMW90CgL9BLjT4kseDwj0E2De6BS8+9eRePXVV1176ux2O+bMmdMKiQlLFosFUVFRrGOQJlq8eDGKi4sRERGBLl264JlnnqkzGIK0nrNnz0IqlbKO0aI8tviAm+W3aVY/jOwiQ4CQD9EfVnuKhHwECPkY2UWGTbP6uWYOLl68GBEREfDz80N8fDwmTZqEiRMnuu7STbjHarVS8Xk5uVyOM2fO4Ouvv8Y333wDiUSCn376iXUsn8P1rQyAh9+P71b3OkZp+/btWLhwIU6dOoWff/4ZU6ZMgc1mw5dffomJEycyeAekJfH5fFy8eBFdu3ZlHYW4gdFoxOTJk7F9+3YMGzYMP/zwA9Mb+fqSuLg4PProoy0625g1rym+5rJarXjqqaewfv169OnTBzt37qQjBA7h8XgoLy+HTCZjHYW40YkTJ1yrQD/88EPMnj2bdSTOCwwMxCeffIJp06axjtJiPPpUpzsJhUJ89dVXOH/+PMrKyhATE4OlS5eyjkXcwHkKm8sjlnxV//79UVpaihdeeAEvvPACUlJSOHlHcE9ht9thNBo5vZUB8KHic0pLS0NhYSEWLFiA+fPno3379sjOzmYdizRDRUUFgJunOwn38Pl8vP/++7h27RoEAgE6duyIuXPn0uKXFnD+/HnweDy0bduWdZQW5bO/KRYuXIiSkhKIxWKkpqbSKjIvVlFRQaXnA9q1a4dLly5hzZo1+M9//gOZTIYjR46wjsUpR44c8YlrqT7920Imk+H06dNYv349Nm7ciKioKOzbt491LHKPFAoFBIKmDTwg3mfWrFlQq9Xo1asXhgwZgrFjx8JoNLKOxQm+sJUB8PHic5o6dSrUajUGDx6MkSNHYsSIEdDpdKxjkbukUqk4f4d5UldQUBB+/PFH/Pzzzzhx4gQiIyPx1VdfsY7l9XJzc31irjEV329EIhG+//57HD58GBcuXEBUVBTWrVvHOha5CyqVim4C7KOGDx8OhUKBv/71r5g5cya6d++O0tJS1rG8FtfvyuBExfcHAwcORHl5OZ599lk899xz6Nq1K4qKiljHIrehVqsREEBDyX0Vn8/HmjVrkJOTA4PBgHbt2uGtt95iHcsrqdVqTt+VwYmKrwF8Ph8fffQR8vPzYbPZkJiYiDfeeAM+suXR61RVVUEkErGOQRjr1KkT8vLy8P7772P58uWIjY1FZmYm61hew2azwWg0YtiwYayjtDgqvttITExEdnY2VqxYgY8++oh+kDxUdXU1FR9xeemll6BQKNCpUyf07duXxhXepXPnzoHP5yM2NpZ1lBZHxXcX5syZA4VCgeTkZPpB8kBarRZBQUGsYxAPEh4ejkOHDmHHjh3Yt28fxGIxNm/ezDqWRzt69KhPbGUAqPjuWlhYGA4cOICdO3e6fpC2bNnCOhYBUFNTw/nbqJCmGTNmDFQqFR599FFMnjwZffv2hVKpZB3LI/nKVgaAiu+ejR492vWDNGnSJPTr149+kBjT6XQIDQ1lHYN4KOe4wosXL6KiogIxMTFYsmQJ61gex1e2MgBUfE1Ccz89S21tLRUfuaOuXbvi+vXrWLRoERYtWoSEhARcunSJdSyPcePGDZ+5uwkVXzPQ3E/PoNfrER4ezjoG8RLz5s1DWVkZ5HI50tLSMH36dFitVtaxmFOr1ejTpw/rGK2Cis8NaO4nW0ajkYqP3JOoqChkZGRg06ZN2Lp1KyQSCXbu3Mk6FjNWqxUmkwmDBw9mHaVVUPG5Cc39ZMdkMkEsFrOOQbzQY489BrVajZEjR2LcuHEYMmQINBoN61itzpe2MgBUfG73x7mfDz74IM39bGFms5mKjzSZv78/Nm/ejFOnTiE/Px9SqRQffvgh61itylfuyuBExdcCbp37ef78eZr72cIsFgskEgnrGMTLpaeno6SkBK+99hpee+01dOrUCVeuXGEdq1WcPXsWMpmMdYxWQ8XXgv4497Nbt24097MFWK1WREVFsY5BOGLJkiUoKipCSEgIkpOT8eyzz3L+mr0vbWUAqPha3K1zP61WK839bAE2m81nNt6S1hEbG4tz587h888/x9dff43o6Gj88ssvrGO1mJKSEp/ZygBQ8bUamvvZchwOh0+dpiGtZ/r06VCr1RgwYABGjBiBkSNHora2lnUst1Or1ejbty/rGK2Giq+V0dxP9zKZTABA1/hIixGJRNixYwcOHz6Mc+fOQSKRcOqavXMrw5AhQ1hHaTVUfAw4537eOkCX5n42TUVFBYCbp5QJaUl/vGafmpqKwsJC1rGaLTMzE3w+36fOmtBvC4ZuHaBLcz+bprKykkqPtBrnNfuCggI4HA60b98er776qlcvfjl+/LjPjfyj3xiM0dzP5lEoFBAIBKxjEB+TkJCAy5cvY+XKlVi1ahViYmJw7Ngx1rGaxNe2MgBUfB7DOfdz/vz5NPfzHqhUKgiFQtYxiI+aPXs2lEol0tLSMGjQIIwfPx5Go5F1rHuSm5uL9u3bs47Rqqj4PMzbb79Ncz/vgVKphL+/P+sYa6Cm3gAAER5JREFUxIeFhIRg37592Lt3L44cOQKxWIwNGzawjnXXfG0rA0DF55Fo7ufdU6vVVHzEI4wYMQJKpRLTpk3D9OnT0bNnT5SXl7OOdUdVVVU+tZUBoOLzaM65n4MGDaK5n43QaDQQiUSsYxAC4Obil3Xr1uHy5cvQarWIi4vDwoULWcdqlC9uZQCo+DyeSCTCDz/8QHM/G1FdXU3FRzxOUlIS8vPzsWzZMixduhRxcXE4e/Ys61j1nDx5Enw+H9HR0ayjtCoqPi/h3EP0t7/9jeZ+3kKr1SI4OJh1DEIa9Oqrr6KyshKJiYlIT0/HlClTPOqmt8eOHfO5rQwAFZ9X4fP5WLFiBc39vAUVH/F0EREROHLkCL777jvs2rULkZGR+O6771jHAgCcP38ecrmcdYxWR8XnhWju5+90Op1P3UeMeK/x48dDrVZj/PjxePTRRzFgwACo1WqmmfLy8nxuKwNAxefVaO4noNfrffJUDfFOQqEQGzZswLlz51BcXAyZTIZ//etfzPIUFxejW7duzF6fFSo+L+frcz/1ej3Cw8NZxyDknnTv3h1FRUVYsGAB/v73vzMbWFFVVYU+ffq0+uuyRsXHEb4699NoNCIiIoJ1DEKaZOHChSgtLYVEIkFqaipmzpzZagMrzGYzzGazz21lAKj4OOXWuZ+lpaU+MffTZDIhMjKSdQxCmkwqlSIzMxPffvstNm3aBLFYjD179rT46546dQoCgQBRUVEt/lqehoqPg9LS0lBUVOQTcz9NJhPEYjHrGIQ02+OPP46qqio88MADGDNmDIYNGwatVuv213E4HNDpdDh69KjPXh+n4uOwt99+G8XFxZye+2m1WukmtIQz/P39sXXrVpw4cQI5OTmIjo7GypUr3foa33//PUJDQ7FgwQLo9XpMmjQJJ0+edOtreDoqPo6Ty+U4ffo0vv76a07O/bRarT55qoZwW9++fVFWVoaXXnoJL7/8MpKSknD16lW3PPf9998Pf39/WK1WmM1mbN26FRqNxi3P7S2o+HzEtGnTODn302azQSqVso5BSItYtmwZrl+/joCAAHTq1Alz5sxp9lkbqVSKtLQ0AIBAIMC0adMwcuRId8T1GlR8PoSLcz8dDodPTp4gviMuLg4XL17EunXr8Nlnn0EqleLQoUPNes6ZM2cCAEJDQ7Fq1Sp3xPQqVHw+iCtzP503/KTFLcQXPPXUU1CpVOjTpw+GDRuGUaNGQa/Xuz5/L9uXxo8fDwD473//65OTj6j4fFRDcz9ff/11r1r8UllZCeDmeyHEFwQFBWH37t3Yv38/MjMzIZFI8Nlnn+HEiROQyWQ4evRoo1+r1Jmw9lABXtp0Dq/tKEC7xxfgmqgjVDpTK74Dz8Bz+PKEY+KycuVK/N///R/EYjG2b9+O3r17s450R6dPn0bfvn1hs9lYRyGk1dntdsydOxerV6+GQCCAxWJB+/btkZubC6FQ6HrchRsarDqYj0N5CgCAyfr7H7ciIR8OAEOTojF7SEd0b+sbwyDoT2UCAHjhhRe8bu6nQqGAQCBgHYMQJvh8PlauXIkXX3zRdaujoqIifPTRR67HbMi4jsmfZGBfdgVMVnud0gMA428f++lyBSZ/koENGddb8y0wQ0d8pJ5du3bhiSeegM1mw5dffomJEyeyjtSg9evX429/+1ud6xyE+BLnyD6BQODangAA+/fvR0lgIpbszobBcveXLwL9+Jg3OgXT+iW0UGLPILzzQ4ivcc79nDlzJiZNmoQ+ffpg586dHrdfTqVSwd/fn3UMQpgRiUQ4d+4crl27hrKyMhQUFGD9+vWYOOtVRE1+Fybr78c1Rcvr/gHrsJoRet9oiB981vUxg8WOJbtzkBYXgbQ47p72pCM+clsXL17E2LFjUVZWhnfeeQdvvvkm60guCxcuxNq1a12LXAghN/31s2M4VKBBY7/d7WYDilf+BdLHFkHUrmudz/F4wMguMqydlt4KSdmga3zktjx57qdGo4FIJGIdgxCPotSZcOK6ttHSAwB97nEIgsIR0Da13uccDuBAroLTqz2p+Mhd8cS5n9XV1QgMDGSagRBPs+VM8R0fo/v1FwR3HQ4ej9fg53kAtpy98/N4Kyo+ctc8be5ndXU1goKCmL0+ISxduHABPXv2xL///W9UVVW5Pp5Trq23evNW1upKmG5cQnC3Bxp9jNFqR05ZjVvzehIqPnLPPGXuZ01NDYKDg1v9dQnxBEajEZcvX8brr78OuVyO3r17Y968eSgoKrvt1+ku7UdAXBf4Rdx+1J/WaHFnXI9CqzpJkzjnfh49ehSPPPIIoqKi8PHHH2PWrFmtlkGn09FNaAkn2e12lJaW4tKlS8jNzcXVq1dx48YNlJWVQalUQqPRQKfTwWT6/Trc6dOncfnyZfSa0x7gN15qtZf2I7zfnbcohYn83PJe/n979xrT1nnGAfx/sA3HXMzFGGOuDgZ8I8qURlvTrSpJNCVCmSp1ibqpUyulStayKVK1SBOKFKWKJmXLpCZlQY3WTwxFmppPW9KsW7eWTVqrNE3VNsbEXAIGgsFQE3PxMb7tA/IZFAi54GBy/r9vwME+Rpb+vI/f53nTEYOPHkly7ucbb7yB119/Ha2trbhy5QqqqqpS/txzc3OP5XmI1svMzAxcLhfcbjd6enowODiIO3fuYHx8HN988w2mp6chSZLckK7RaKDVapGXlwe9Xg+j0QibzQaz2Yzq6mocOnQIarUaWq0W586dwyuvvIIL/+7HWx96Vix3SsNuxGYmkW37wT3vU1RnwGZ6cg+pZTsDrZvbt2+jqakJHo8Hx44dw+nTp1f98Hw9WCwW7Nq1C++++27KnoNoLdFoFH19fejq6oLH48Ht27cxNDSEsbExTExMIBgMYm5uDvPz80gkEsjIyIAoisjNzUVBQQEMBgPKyspQXV2NmpoaWK1WOByO+zp1pKSkBHv27EFra6vcZzsxE8b3f/uvFYNv8m9/QCISRvGPfnXPx81SZ+C/v94NfW7Ww/1R0hxXfLRutmzZArfbLc/9bG9vT+ncT0mSWOqklBkfH5dLjX19fRgcHMTo6Cj8fr9capQkCfF4HIIgIDMzE9nZ2dDpdCguLkZpaSl27NiBmpoa1NXVweFwwGKxLJmj+aiGh4eXDXEozs3Cc/UG/MM9tqylQb/vl2s+piAAu6yGJzb0AK74KEWCwSCef/55dHZ24oUXXsDFixfXbcrKe++9h+vXr6O1tRW7d+/Giy++iIMHD7Knj9aU3BDidrvR29uLgYEBDA8PY2xsTC41hkIhRCILGzvUajVEUUReXh6KiopQUlKCsrIymM1m1NbWwm63w263Q6fTbfArW+rLoSn85I+fIhR58AHuWo0Kfz7yNCe3ED2sVMz9PHr0KM6fP494PA6VSoVEIgGv14vy8vJ1uGPabOLxOAYHB+FyueRSo9frhc/nkzeCzM3NIRwOy6XGrKws5OTkoKCgAMXFxTCZTKisrITFYkF9fT0aGhpQVla2qY+86vh0gLM6V8Hgo5SLRqM4dOgQOjo61mXu58DAAOx2OyRJglqtxuHDh9HW1raOd0zpIBAIwOVyobu7Wy41joyMwO/3IxAIyBtBksdSZWZmQqvVQqfTyRtBKioqYDab5VKj1WpV1HzXhfDrhhSN3XOSiyAAolqF4022Jz70AAYfPUbrOfezqakJV69ehSiKGBkZ4Snsm8T8/Dw8Hg+6urrQ09OzpNQ4OTmJu3fvIhQKyacMqFQqudRYUFAglxqrqqpgsVhgt9vhcDig1+s3+JWlr6+Gp9D2cS8+uuWHgIXm9KTkeXy7rAY0N9Y+0eXNxRh89Ni9+eabOHXqFKqqqnDlyhXY7fYHfoxPPvkEzzzzDI4dO4YzZ86k4C7pfiV7zpKlxv7+fni9XrnnLBAIYHZ2FuFweMlGkGSpUa/Xw2QyoaKiQt4I0tDQALPZvKlLjelmciaMSzeG0T06jaAUgU7UwGbKw4HtFU/0RpaVMPhoQ/h8Puzfvx83btzAq6++igsXLiAUCuHo0aM4e/Ys8vLW7iFSq9UYHx/nai9Fkj1n3d3d6O3tlUuNi3vOQqHQij1nRUVFMBqNKC8vlzeC2Gw2OBwOjpmjDcfgow3V0dGBI0eOQBRFPPvss7h8+TKam5vR2tq64vUTM2Fc+nwY3b4gbnztxvatdthKdTj4lPL+a30YsVgMfX19cLlc6OnpkXvOfD6fXGpcqecsJycHhYWFMBgMMJlMcqnRarXC6XTeV88ZUbpg8NGGkyQJ+/btQ2dnJ4CFcWifffYZGhr+f07Yl0NTOP9xLzo9fgBY0pyb/Jyi0WpA83O12FapjM8pFlvcc9bf37+k5ywQCMjjrWKxGARBgEajQXZ2NvLz81FcXAyj0YjKysolG0Hq6urWteeMKF0w+GjDxeNxOBwO3Lp1S/5eVVUVBgYGIAiCYnemSZIEt9uNrq4uuedsZGRE7jkLBoOr9pwVFhbCaDTKE0GSpUan05l2PWdEjxv/naMNF4lEsHXrVmRlZcHn88Hv98Pr9WLv3r346Yk2/O7vPffVi5RIAKFIDL95f+Gg3HQMv3g8jqGhIdy8eVPeCLJ4+PDdu3fljSDJUuPijSAGgwEWiwWNjY2oqalBfX09nE4nKioquBGE6D5xxUdpJ9mQfqqtHf/JfArh6NK36MRffw9p4EvEIxJUOYXQPf1j5G3bu+Saxz19YmpqatlGkOTw4ZV6zpKlxtV6zux2O+rr6zmNhigFGHyUto786fqK8wbn/YPQFJZBUGsQmRyC72ILSg6eRFZprXyNIAB7HUa887MdABbCtL29HSdOnMC1a9dgNBrXfP5IJAKPxwO32y1PBFncc5YcPhyJRJBIJOSes9zcXHkjSLLUaLFY5F2Nj9K8T0SPjqVOSksTM2F0evwrfqaXaahe9JUAAQKigdElwZdIAB/d8mNyJozJO4N4+eWXcfPmTcRiMVy7dg2iKMrDhxf3nC0+5+zbPWf5+fnQ6/UoLy/Hzp075Z4zp9OJLVu2sNRItElwxUdp6Z3OvlXPFAOAyQ/aMPv1P5GIhpFptMD40mlkZGqXXKMWElC5rsLzl+XjzJJnmOl0Onn4cLLnrK6ujj1nRE8wrvgoLXX7gquGHgDo9zaj6Ic/R3ikG5L3awiq5adFRxMC9GYHKisrMTo6ioyMDCQSCbz99tt47bXXUnn7RJTGWJuhtBSUomteI2SoIFY6EZuewPQX7694TcP278Lr9WJoaAgnT56E0WgEixxEysYVH6UlnfgAb814HNHA6CqPs7ASLC0tRUtLC1paWtbj9ohoE+OKj9KSrVSHLPXyt2dsdgqzXZ2Iz4eQiMcQ6v8cs+5OiObvLLtWVGfAZlp75icRKQtXfJSWDjxVgbc+9Cz/gSBg+ourmPygDUjEoc4vQeGew8iu+96ySxMADmyvSP3NEtGmwuCjtFScm4Xn6g3L+vhU2fkofen0mr8vCAtnjHFwNRF9G0udlLZ+0VgLUa16qN8V1So0N9aufSERKQ6Dj9LWtsoCHG+yQat5sLepVpOB4002xZwmTUQPhqVOSmvJQdNKPJ2BiFKDk1toU/hqeAptH/fio1t+CACkFc7j22U1oLmxlis9IronBh9tKpMzYVy6MYzu0WkEpQh0ogY2Ux4ObOcJ7ER0fxh8RESkKNzcQkREisLgIyIiRWHwERGRojD4iIhIURh8RESkKAw+IiJSFAYfEREpCoOPiIgUhcFHRESKwuAjIiJFYfAREZGiMPiIiEhRGHxERKQoDD4iIlIUBh8RESkKg4+IiBSFwUdERIrC4CMiIkVh8BERkaIw+IiISFEYfEREpCj/A/EGFmc4a/MpAAAAAElFTkSuQmCC", - "text/plain": [ - "
" - ] + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
           },
           "metadata": {},
           "output_type": "display_data"
         },
         {
           "data": {
-            "image/png": "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",
+            "text/html": [
+              "
\n",
+              "
\n" + ], "text/plain": [ - "
" + "\n" ] }, "metadata": {}, "output_type": "display_data" - } - ], + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "968020b1388e4883843575d9198af1cd", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "6744eb60dfa04a8598fca3b998ce3077", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "cb8167f00277413eaaa2ad6e0e162fab", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "575205f1a4e64c5d977e69d4939a5605", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "8bf075c6f7834d3fa905b7ddc37cf128", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[2022/08/05 20:35:56 +0000] [61] [INFO] - adbdgl_adapter: Created ArangoDB 'FakeHetero' Graph\n", + "INFO:adbdgl_adapter:Created ArangoDB 'FakeHetero' Graph\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "--------------------\n", + "URL: https://tutorials.arangodb.cloud:8529\n", + "Username: TUT6h05us6483maimfr7o28jq\n", + "Password: TUTis4noysrzjeig2bqpdccaa\n", + "Database: TUTk9nlikuz4zowwxfkusway\n", + "--------------------\n", + "\n", + "View the created graph here: https://tutorials.arangodb.cloud:8529/_db/TUTk9nlikuz4zowwxfkusway/_admin/aardvark/index.html#graph/FakeHetero\n", + "\n", + "View the original graph below:\n", + "\n" + ] + } + ], "source": [ - "# Load the dgl graphs & draw:\n", - "## 1) Lollipop Graph\n", - "dgl_lollipop_graph = remove_self_loop(MiniGCDataset(8, 7, 8)[3][0])\n", - "plt.figure(1)\n", - "nx.draw(dgl_lollipop_graph.to_networkx(), with_labels=True)\n", - "\n", - "## 2) Hypercube Graph\n", - "dgl_hypercube_graph = remove_self_loop(MiniGCDataset(8, 8, 9)[4][0])\n", - "plt.figure(2)\n", - "nx.draw(dgl_hypercube_graph.to_networkx(), with_labels=True)\n", - "\n", - "## 3) Clique Graph\n", - "dgl_clique_graph = remove_self_loop(MiniGCDataset(8, 6, 7)[6][0])\n", - "plt.figure(3)\n", - "nx.draw(dgl_clique_graph.to_networkx(), with_labels=True)\n", - "\n", - "lollipop = \"Lollipop\"\n", - "hypercube = \"Hypercube\"\n", - "clique = \"Clique\"\n", - "\n", - "# Delete the graphs from ArangoDB if they already exist\n", - "db.delete_graph(lollipop, drop_collections=True, ignore_missing=True)\n", - "db.delete_graph(hypercube, drop_collections=True, ignore_missing=True)\n", - "db.delete_graph(clique, drop_collections=True, ignore_missing=True)\n", + "# Create the DGL graph\n", + "hetero_graph = dgl.heterograph({\n", + " (\"user\", \"follows\", \"user\"): (torch.tensor([0, 1]), torch.tensor([1, 2])),\n", + " (\"user\", \"follows\", \"topic\"): (torch.tensor([1, 1]), torch.tensor([1, 2])),\n", + " (\"user\", \"plays\", \"game\"): (torch.tensor([0, 3]), torch.tensor([3, 4])),\n", + "})\n", + "hetero_graph.nodes[\"user\"].data[\"features\"] = torch.tensor([21, 44, 16, 25])\n", + "hetero_graph.nodes[\"user\"].data[\"label\"] = torch.tensor([1, 2, 0, 1])\n", + "hetero_graph.nodes[\"game\"].data[\"features\"] = torch.tensor([[0, 0], [0, 1], [1, 0], [1, 1], [1, 1]])\n", + "hetero_graph.edges[(\"user\", \"plays\", \"game\")].data[\"features\"] = torch.tensor([[6, 1], [1000, 0]])\n", + "\n", + "print(hetero_graph)\n", + "\n", + "name = \"FakeHetero\"\n", + "\n", + "# Define the metagraph\n", + "def label_tensor_to_2_column_dataframe(dgl_tensor):\n", + " \"\"\"\n", + " A user-defined function to create two\n", + " ArangoDB attributes out of the 'user' label tensor\n", + "\n", + " NOTE: user-defined functions must return a Pandas Dataframe\n", + " \"\"\"\n", + " label_map = {0: \"Class A\", 1: \"Class B\", 2: \"Class C\"}\n", + "\n", + " df = pandas.DataFrame(columns=[\"label_num\", \"label_str\"])\n", + " df[\"label_num\"] = dgl_tensor.tolist()\n", + " df[\"label_str\"] = df[\"label_num\"].map(label_map)\n", + "\n", + " return df\n", + "\n", + "\n", + "metagraph = {\n", + " \"nodeTypes\": {\n", + " \"user\": {\n", + " \"features\": \"user_age\", # 1) you can specify a string value for attribute renaming\n", + " \"label\": label_tensor_to_2_column_dataframe, # 2) you can specify a function for user-defined handling, as long as the function returns a Pandas DataFrame\n", + " },\n", + " # 3) You can specify set of strings if you want to preserve the same DGL attribute names for the node/edge type\n", + " \"game\": {\"features\"} # this is equivalent to {\"features\": \"features\"}\n", + " },\n", + " \"edgeTypes\": {\n", + " (\"user\", \"plays\", \"game\"): {\n", + " # 4) you can specify a list of strings for tensor dissasembly (if you know the number of node/edge features in advance)\n", + " \"features\": [\"hours_played\", \"is_satisfied_with_game\"]\n", + " },\n", + " },\n", + "}\n", + "\n", + "# Delete the graph if it already exists\n", + "db.delete_graph(name, drop_collections=True, ignore_missing=True)\n", "\n", "# Create the ArangoDB graphs\n", - "adb_lollipop_graph = adbdgl_adapter.dgl_to_arangodb(lollipop, dgl_lollipop_graph)\n", - "adb_hypercube_graph = adbdgl_adapter.dgl_to_arangodb(hypercube, dgl_hypercube_graph)\n", - "adb_clique_graph = adbdgl_adapter.dgl_to_arangodb(clique, dgl_clique_graph)\n", + "adb_hetero_graph = adbdgl_adapter.dgl_to_arangodb(name, hetero_graph, metagraph, explicit_metagraph=False)\n", + "\n", + "# Create the ArangoDB graph with `explicit_metagraph=True`\n", + "# With `explicit_metagraph=True`, the node & edge types omitted from the metagraph will NOT be converted to ArangoDB.\n", + "# Only 'user', 'game', and ('user', 'plays', 'game') will be brought over (i.e 'topic', ('user', 'follows', 'user'), ... are ignored)\n", + "## adb_hetero_graph = adbdgl_adapter.dgl_to_arangodb(name, hetero_graph, metagraph, explicit_metagraph=True)\n", "\n", "print('\\n--------------------')\n", "print(\"URL: \" + con[\"url\"])\n", @@ -1367,60 +1485,1907 @@ "print(\"Password: \" + con[\"password\"])\n", "print(\"Database: \" + con[\"dbName\"])\n", "print('--------------------\\n')\n", - "print(\"View the created graphs here:\\n\")\n", - "print(f\"1) {con['url']}/_db/{con['dbName']}/_admin/aardvark/index.html#graph/{lollipop}\")\n", - "print(f\"2) {con['url']}/_db/{con['dbName']}/_admin/aardvark/index.html#graph/{hypercube}\")\n", - "print(f\"3) {con['url']}/_db/{con['dbName']}/_admin/aardvark/index.html#graph/{clique}\\n\")\n", - "print(f\"View the original graphs below:\\n\")" + "print(f\"View the created graph here: {con['url']}/_db/{con['dbName']}/_admin/aardvark/index.html#graph/{name}\\n\")\n", + "print(f\"View the original graph below:\\n\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mk6m0hBRkkkT" + }, + "source": [ + "\n", + "#### FakeHeterogeneous Graph with a user-defined ADBDGL Controller" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "KG7kFoOUkkkb" + }, + "source": [ + "Data\n", + "* A fake DGL Heterogeneous graph\n", + "\n", + "API\n", + "* `adbdgl_adapter.adapter.dgl_to_arangodb()`\n", + "\n", + "Notes\n", + "* The `name` parameter is used to name your ArangoDB graph.\n", + "* The `ADBDGL_Controller` is an optional user-defined class for controlling how nodes & edges are handled when transitioning from DGL to ArangoDB. **It is interpreted as the alternative to the `metagraph` parameter.**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 443, + "referenced_widgets": [ + "ea5e9803c5de4d2bbb48782069b9829b", + "3f633be94c7d466ea40571e805a76948", + "96e57d98afce44cd8269204dd19ff6e0", + "da43ef4a8c6a41f9bda153a0cd14c2d7", + "3bc228aa98454dc59a604c8f7ff6b2a0", + "65138d18c9c449d1aaaad387293c5ede", + "3ea99b2a6b4246d3abf628ca743f9f24", + "841ce4f5d391457e858c3c48185e259d", + "987bf80aee4b4b97bfad1699f8384af8", + "4ab3c113235746cab5fde158756ab420", + "09e8c93741bf45acb69ba9e757107564", + "d7d06973b2984eb19fa050409bf62222" + ] + }, + "id": "A-DtrD2Ykkkb", + "outputId": "f2672554-16e4-4b88-e24b-f567ff13bb3f" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Graph(num_nodes={'game': 5, 'topic': 3, 'user': 4},\n", + " num_edges={('user', 'follows', 'topic'): 2, ('user', 'follows', 'user'): 2, ('user', 'plays', 'game'): 2},\n", + " metagraph=[('user', 'topic', 'follows'), ('user', 'user', 'follows'), ('user', 'game', 'plays')])\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[2022/08/05 20:36:18 +0000] [61] [INFO] - adbdgl_adapter: Instantiated ADBDGL_Adapter with database 'TUTk9nlikuz4zowwxfkusway'\n", + "INFO:adbdgl_adapter:Instantiated ADBDGL_Adapter with database 'TUTk9nlikuz4zowwxfkusway'\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "ea5e9803c5de4d2bbb48782069b9829b", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "96e57d98afce44cd8269204dd19ff6e0", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "3bc228aa98454dc59a604c8f7ff6b2a0", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "3ea99b2a6b4246d3abf628ca743f9f24", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "987bf80aee4b4b97bfad1699f8384af8", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "09e8c93741bf45acb69ba9e757107564", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[2022/08/05 20:36:20 +0000] [61] [INFO] - adbdgl_adapter: Created ArangoDB 'FakeHetero' Graph\n", + "INFO:adbdgl_adapter:Created ArangoDB 'FakeHetero' Graph\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "--------------------\n", + "URL: https://tutorials.arangodb.cloud:8529\n", + "Username: TUT6h05us6483maimfr7o28jq\n", + "Password: TUTis4noysrzjeig2bqpdccaa\n", + "Database: TUTk9nlikuz4zowwxfkusway\n", + "--------------------\n", + "\n", + "View the created graph here: https://tutorials.arangodb.cloud:8529/_db/TUTk9nlikuz4zowwxfkusway/_admin/aardvark/index.html#graph/FakeHetero\n", + "\n", + "View the original graph below:\n", + "\n" + ] + } + ], + "source": [ + "# Create the DGL graph\n", + "hetero_graph = dgl.heterograph({\n", + " (\"user\", \"follows\", \"user\"): (torch.tensor([0, 1]), torch.tensor([1, 2])),\n", + " (\"user\", \"follows\", \"topic\"): (torch.tensor([1, 1]), torch.tensor([1, 2])),\n", + " (\"user\", \"plays\", \"game\"): (torch.tensor([0, 3]), torch.tensor([3, 4])),\n", + "})\n", + "hetero_graph.nodes[\"user\"].data[\"features\"] = torch.tensor([21, 44, 16, 25])\n", + "hetero_graph.nodes[\"user\"].data[\"label\"] = torch.tensor([1, 2, 0, 1])\n", + "hetero_graph.nodes[\"game\"].data[\"features\"] = torch.tensor([[0, 0], [0, 1], [1, 0], [1, 1], [1, 1]])\n", + "hetero_graph.edges[(\"user\", \"plays\", \"game\")].data[\"features\"] = torch.tensor([[6, 1], [1000, 0]])\n", + "\n", + "print(hetero_graph)\n", + "\n", + "name = \"FakeHetero\"\n", + "\n", + "# Create a custom ADBDGL_Controller\n", + "class Custom_ADBDGL_Controller(ADBDGL_Controller):\n", + " def _prepare_dgl_node(self, dgl_node: dict, node_type: str) -> dict:\n", + " \"\"\"Optionally modify a DGL node object before it gets inserted into its designated ArangoDB collection.\n", + "\n", + " :param dgl_node: The DGL node object to (optionally) modify.\n", + " :param node_type: The DGL Node Type of the node.\n", + " :return: The DGL Node object\n", + " \"\"\"\n", + " dgl_node[\"foo\"] = \"bar\"\n", + " return dgl_node\n", + "\n", + " def _prepare_dgl_edge(self, dgl_edge: dict, edge_type: tuple) -> dict:\n", + " \"\"\"Optionally modify a DGL edge object before it gets inserted into its designated ArangoDB collection.\n", + "\n", + " :param dgl_edge: The DGL edge object to (optionally) modify.\n", + " :param edge_type: The Edge Type of the DGL edge. Formatted\n", + " as (from_collection, edge_collection, to_collection)\n", + " :return: The DGL Edge object\n", + " \"\"\"\n", + " dgl_edge[\"bar\"] = \"foo\"\n", + " return dgl_edge\n", + "\n", + "# Delete the graph if it already exists\n", + "db.delete_graph(name, drop_collections=True, ignore_missing=True)\n", + "\n", + "# Create the ArangoDB graphs\n", + "adb_g = ADBDGL_Adapter(db, Custom_ADBDGL_Controller()).dgl_to_arangodb(name, hetero_graph)\n", + "\n", + "print('\\n--------------------')\n", + "print(\"URL: \" + con[\"url\"])\n", + "print(\"Username: \" + con[\"username\"])\n", + "print(\"Password: \" + con[\"password\"])\n", + "print(\"Database: \" + con[\"dbName\"])\n", + "print('--------------------\\n')\n", + "print(f\"View the created graph here: {con['url']}/_db/{con['dbName']}/_admin/aardvark/index.html#graph/{name}\\n\")\n", + "print(f\"View the original graph below:\\n\")" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "uByvwf9feG9A" + }, + "source": [ + "# ArangoDB to DGL\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 165, + "referenced_widgets": [ + "c6cffa0a64434e56879ba2a8c9de018a", + "0083494093574c50952dd066502a708d", + "1dea128bde204a8fa53e094e014183fe", + "50f8ff3637ee4fc7af8c811cd5d177be", + "6582a9d3fe044d5380d8e918f3bc5a6d", + "40da9dd52dd6443684b990f74b6cb876", + "80d19dc0d20842c3b5c7313c0ad23d24", + "0478c90ef8234f3a8987dbe9cd3030b2", + "c61e3997250d4f93a8e0494db674892d", + "97e7543f202749c197515a9c5c79adbe", + "88e83ddc1ca1464291e1631b8fced847", + "a9c14a3f339445338119631c8e56ff68" + ] + }, + "id": "rnMe3iMz2K7j", + "outputId": "b1485ec1-64bf-43d5-a5fe-7d6bd5fc2da1" + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "c6cffa0a64434e56879ba2a8c9de018a", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "1dea128bde204a8fa53e094e014183fe", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "6582a9d3fe044d5380d8e918f3bc5a6d", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "80d19dc0d20842c3b5c7313c0ad23d24", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "c61e3997250d4f93a8e0494db674892d", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "88e83ddc1ca1464291e1631b8fced847", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[2022/08/05 20:36:46 +0000] [61] [INFO] - adbdgl_adapter: Created ArangoDB 'FakeHetero' Graph\n", + "INFO:adbdgl_adapter:Created ArangoDB 'FakeHetero' Graph\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# Start from scratch! (with the same DGL graph)\n", + "hetero_graph = dgl.heterograph({\n", + " (\"user\", \"follows\", \"user\"): (torch.tensor([0, 1]), torch.tensor([1, 2])),\n", + " (\"user\", \"follows\", \"topic\"): (torch.tensor([1, 1]), torch.tensor([1, 2])),\n", + " (\"user\", \"plays\", \"game\"): (torch.tensor([0, 3]), torch.tensor([3, 4])),\n", + "})\n", + "hetero_graph.nodes[\"user\"].data[\"features\"] = torch.tensor([21, 44, 16, 25])\n", + "hetero_graph.nodes[\"user\"].data[\"label\"] = torch.tensor([1, 2, 0, 1])\n", + "hetero_graph.nodes[\"game\"].data[\"features\"] = torch.tensor([[0, 0], [0, 1], [1, 0], [1, 1], [1, 1]])\n", + "hetero_graph.edges[(\"user\", \"plays\", \"game\")].data[\"features\"] = torch.tensor([[6, 1], [1000, 0]])\n", + "\n", + "db.delete_graph(\"FakeHetero\", drop_collections=True, ignore_missing=True)\n", + "adbdgl_adapter.dgl_to_arangodb(\"FakeHetero\", hetero_graph)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "ZrEDmtqCVD0W" + }, + "source": [ + "#### Via ArangoDB Graph" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "H8nlvWCryPW0" + }, + "source": [ + "Data\n", + "* A fake DGL Heterogeneous graph\n", + "\n", + "API\n", + "* `adbdgl_adapter.adapter.arangodb_graph_to_dgl()`\n", + "\n", + "Notes\n", + "* The `name` parameter in this case must point to an existing ArangoDB graph in your ArangoDB instance.\n", + "* Due to risk of ambiguity, this method does **not** carry over ArangoDB attributes to DGL." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 184, + "referenced_widgets": [ + "9403e71c2bbe46bd9e6d49d555264554", + "34c4ef0c4aa5454893c0f0fa35902fbd", + "1690574b32cc4b48a8b87520458d5066", + "a9edf4f85a4a4504b155608bb740178a", + "fd2db543279f4a13ab6376b9c23160e0", + "5c310145af4f4c90b659dee771185ab6", + "31a9f782f36d407f8cc42b19679c5c2c", + "9fd8d07a43cd4c06a2d448047ede846c", + "2c2900512b5244d3a0fcaf7409446d0e", + "c5d064af7f4a49dca6716f98d052e951" + ] + }, + "id": "zZ-Hu3lLVHgd", + "outputId": "85729665-feb3-4382-e84b-4286162581c3" + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "9403e71c2bbe46bd9e6d49d555264554", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "1690574b32cc4b48a8b87520458d5066", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "fd2db543279f4a13ab6376b9c23160e0", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "31a9f782f36d407f8cc42b19679c5c2c", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "2c2900512b5244d3a0fcaf7409446d0e", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[2022/08/05 20:37:12 +0000] [61] [INFO] - adbdgl_adapter: Created DGL 'FakeHetero' Graph\n", + "INFO:adbdgl_adapter:Created DGL 'FakeHetero' Graph\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "--------------------\n", + "defaultdict(, {})\n" + ] + } + ], + "source": [ + "# Define graph name\n", + "name = \"FakeHetero\"\n", + "\n", + "# Create the DGL Graph from the ArangoDB graph\n", + "dgl_g = adbdgl_adapter.arangodb_graph_to_dgl(name)\n", + "\n", + "# You can also provide valid Python-Arango AQL query options to the command above, like such:\n", + "# dgl_g = adbdgl_adapter.arangodb_graph_to_dgl(graph_name, ttl=1000, stream=True)\n", + "# See the full parameter list at https://docs.python-arango.com/en/main/specs.html#arango.aql.AQL.execute\n", + "\n", + "# Show graph data\n", + "print('\\n--------------------')\n", + "print(dgl_g)\n", + "print(dgl_g.ndata) # note how this is empty\n", + "print(dgl_g.edata) # note how this is empty" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "RQ4CknYfUEuz" + }, + "source": [ + "#### Via ArangoDB Collections" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "bRcCmqWGy1Kf" + }, + "source": [ + "Data\n", + "* A fake DGL Heterogeneous graph\n", + "\n", + "API\n", + "* `adbdgl_adapter.adapter.arangodb_collections_to_dgl()`\n", + "\n", + "Notes\n", + "* The `name` parameter is purely for documentation purposes in this case.\n", + "* The `vertex_collections` & `edge_collections` parameters must point to existing ArangoDB collections within your ArangoDB instance.\n", + "* Due to risk of ambiguity, this method does **not** carry over ArangoDB attributes to DGL." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 253, + "referenced_widgets": [ + "f01997b9b43d43368d632e26ba9732ad", + "14b29dc1f2b8454fa9acc1d79dcd4870", + "5f5c119141a24cab907ceb2da27e0244", + "46b88027e41a43578ebcc47513dd6911", + "7a43c4b816da4a40b0eed167a85eef22", + "eb376d5cf782424aaccbce31f0d3ede5", + "7a4db2b18c634bef932fb9b1157d4af1", + "b5be8c1e4ab3415c9fffbb61aeb0fff3", + "4e085418ce1b41e1bc24ad6acea92fc4", + "7b5dba3f4d50466eb2071cb13548ef1b" + ] + }, + "id": "i4XOpdRLUNlJ", + "outputId": "c0fa5973-3e46-4227-8b0c-48b4f14736e5" + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "f01997b9b43d43368d632e26ba9732ad", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "5f5c119141a24cab907ceb2da27e0244", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "7a43c4b816da4a40b0eed167a85eef22", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "7a4db2b18c634bef932fb9b1157d4af1", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "4e085418ce1b41e1bc24ad6acea92fc4", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[2022/08/05 20:37:50 +0000] [61] [INFO] - adbdgl_adapter: Created DGL 'FakeHetero' Graph\n", + "INFO:adbdgl_adapter:Created DGL 'FakeHetero' Graph\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "--------------------\n", + "Graph(num_nodes={'game': 5, 'topic': 3, 'user': 4},\n", + " num_edges={('user', 'follows', 'topic'): 2, ('user', 'follows', 'user'): 2, ('user', 'plays', 'game'): 2},\n", + " metagraph=[('user', 'topic', 'follows'), ('user', 'user', 'follows'), ('user', 'game', 'plays')])\n", + "defaultdict(, {})\n", + "defaultdict(, {})\n" + ] + } + ], + "source": [ + "name = \"FakeHetero\"\n", + "\n", + "dgl_g = adbdgl_adapter.arangodb_collections_to_dgl(\n", + " name,\n", + " v_cols={\"user\", \"game\"},\n", + " e_cols={\"plays\", \"follows\"}\n", + ")\n", + "\n", + "# Show graph data (notice that the \"topic\" data is skipped)\n", + "print('\\n--------------------')\n", + "print(dgl_g)\n", + "print(dgl_g.ndata) # note how this is empty\n", + "print(dgl_g.edata) # note how this is empty" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "qEH6OdSB23Ya" + }, + "source": [ + "#### Via ArangoDB-DGL metagraph 1" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "PipFzJ0HzTMA" + }, + "source": [ + "Data\n", + "* A fake DGL Heterogeneous graph\n", + "\n", + "API\n", + "* `adbdgl_adapter.adapter.arangodb_to_dgl()`\n", + "\n", + "Notes\n", + "* The `name` parameter is purely for documentation purposes in this case.\n", + "* The `metagraph` parameter is an object defining vertex & edge collections to import to DGL, along with collection-level specifications to indicate which ArangoDB attributes will become DGL features/labels. It should contain collections & associated document attributes names that exist within your ArangoDB instance." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 409, + "referenced_widgets": [ + "77b31c42e914410aaea93044f1390121", + "8349f1e6b1f34680bacd7de1a1937122", + "38aaa492d75c48f38de60ea0cc5fa93f", + "63845b04ecbc40de8bcc017d754ac907", + "4b7f5f21b98b4c5d8475929bf1f01a65", + "404a19cadaca4b85a957cad231b73cbb", + "bd8b6caa7d2d4df1a99b1870ecc0ae46", + "13d0f7da120b40b993ce3c0b257d5788", + "ea88ab86e9774ed78ea62daa6e338637", + "712770e675424d7eb0c8efd6c34f2012" + ] + }, + "id": "7Kz8lXXq23Yk", + "outputId": "b17433d7-d344-4748-ffe3-f0abca6fb112" + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "77b31c42e914410aaea93044f1390121", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "38aaa492d75c48f38de60ea0cc5fa93f", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "4b7f5f21b98b4c5d8475929bf1f01a65", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "bd8b6caa7d2d4df1a99b1870ecc0ae46", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "ea88ab86e9774ed78ea62daa6e338637", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[2022/08/05 20:38:02 +0000] [61] [INFO] - adbdgl_adapter: Created DGL 'FakeHetero' Graph\n", + "INFO:adbdgl_adapter:Created DGL 'FakeHetero' Graph\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "--------------\n", + "Graph(num_nodes={'game': 5, 'topic': 3, 'user': 4},\n", + " num_edges={('user', 'follows', 'topic'): 2, ('user', 'follows', 'user'): 2, ('user', 'plays', 'game'): 2},\n", + " metagraph=[('user', 'topic', 'follows'), ('user', 'user', 'follows'), ('user', 'game', 'plays')])\n", + "\n", + "--------------\n", + "defaultdict(, {'dgl_game_features': {'game': tensor([[0, 0],\n", + " [0, 1],\n", + " [1, 0],\n", + " [1, 1],\n", + " [1, 1]])}, 'label': {'user': tensor([1, 2, 0, 1])}, 'features': {'user': tensor([21, 44, 16, 25])}})\n", + "--------------\n", + "\n", + "defaultdict(, {'dgl_plays_features': {('user', 'plays', 'game'): tensor([[ 6, 1],\n", + " [1000, 0]])}})\n" + ] + } + ], + "source": [ + "# Define the Metagraph that transfers ArangoDB attributes \"as is\",\n", + "# meaning the data is already formatted to DGL data standards\n", + "metagraph_v1 = {\n", + " \"vertexCollections\": {\n", + " # Move the \"features\" & \"label\" ArangoDB attributes to DGL as \"features\" & \"label\" Tensors\n", + " \"user\": {\"features\", \"label\"}, # equivalent to {\"features\": \"features\", \"label\": \"label\"}\n", + " \"game\": {\"dgl_game_features\": \"features\"},\n", + " \"topic\": {},\n", + " },\n", + " \"edgeCollections\": {\n", + " \"plays\": {\"dgl_plays_features\": \"features\"},\n", + " \"follows\": {}\n", + " },\n", + "}\n", + "\n", + "# Create the DGL graph\n", + "dgl_g = adbdgl_adapter.arangodb_to_dgl(\"FakeHetero\", metagraph_v1)\n", + "\n", + "# Show graph data\n", + "print('\\n--------------')\n", + "print(dgl_g)\n", + "print('\\n--------------')\n", + "print(dgl_g.ndata)\n", + "print('--------------\\n')\n", + "print(dgl_g.edata)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "0806IB4o3WRz" + }, + "source": [ + "#### Via ArangoDB-DGL metagraph 2" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "cnByWtpa3WR7" + }, + "source": [ + "Data\n", + "* [ArangoDB IMDB Movie Dataset](https://www.arangodb.com/docs/stable/arangosearch-example-datasets.html#imdb-movie-dataset)\n", + "\n", + "API\n", + "* `adbddgl_adapter.adapter.arangodb_to_dgl()`\n", + "\n", + "Notes\n", + "* The `name` parameter is purely for documentation purposes in this case.\n", + "* The `metagraph` parameter is an object defining vertex & edge collections to import to DGL, along with collection-level specifications to indicate which ArangoDB attributes will become DGL features/labels. In this example, we rely on user-defined encoders to build DGL-ready tensors (i.e feature matrices) from ArangoDB attributes. See https://pytorch-geometric.readthedocs.io/en/latest/notes/load_csv.html for an example on using encoders." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 499, + "referenced_widgets": [ + "2b13e46a722e4be384fad74e1b3e6461", + "848230df62434c77b5b18f9a43e2d14f", + "59405e2d0c164d5b965680cc9d9cd8f3", + "2a380fe111794c3a951cdafa4a2bf0b3", + "3d081c88cd2945fa9534de722669ada9", + "82f996185e8444ada5e18602e2f8e105" + ] + }, + "id": "cKqLoawE3WR7", + "outputId": "02a8bfed-44ae-4c76-9eea-ba7348738707" + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "2b13e46a722e4be384fad74e1b3e6461", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "59405e2d0c164d5b965680cc9d9cd8f3", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "3d081c88cd2945fa9534de722669ada9", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "[2022/08/05 20:38:44 +0000] [61] [INFO] - adbdgl_adapter: Created DGL 'IMDB' Graph\n", + "INFO:adbdgl_adapter:Created DGL 'IMDB' Graph\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "--------------\n", + "Graph(num_nodes={'Movies': 1682, 'Users': 943},\n", + " num_edges={('Users', 'Ratings', 'Movies'): 65499},\n", + " metagraph=[('Users', 'Movies', 'Ratings')])\n", + "\n", + "--------------\n", + "defaultdict(, {'features': {'Movies': tensor([[0, 0],\n", + " [1, 0],\n", + " [0, 0],\n", + " ...,\n", + " [0, 1],\n", + " [0, 0],\n", + " [0, 1]]), 'Users': tensor([[ 0., 35.],\n", + " [ 1., 53.],\n", + " [ 0., 23.],\n", + " ...,\n", + " [ 0., 20.],\n", + " [ 1., 48.],\n", + " [ 0., 22.]])}, 'label': {'Movies': tensor([1, 0, 0, ..., 0, 1, 0])}})\n", + "--------------\n", + "\n", + "{'weight': tensor([4, 4, 3, ..., 4, 4, 4])}\n" + ] + } + ], + "source": [ + "# Define the Metagraph that transfers attributes via user-defined encoders\n", + "metagraph_v2 = {\n", + " \"vertexCollections\": {\n", + " \"Movies\": {\n", + " \"features\": { # Build a feature matrix from the \"Action\" & \"Drama\" document attributes\n", + " \"Action\": IdentityEncoder(dtype=torch.long),\n", + " \"Drama\": IdentityEncoder(dtype=torch.long),\n", + " },\n", + " \"label\": \"Comedy\",\n", + " },\n", + " \"Users\": {\n", + " \"features\": {\n", + " \"Gender\": CategoricalEncoder(), # CategoricalEncoder(mapping={\"M\": 0, \"F\": 1}),\n", + " \"Age\": IdentityEncoder(dtype=torch.long),\n", + " }\n", + " },\n", + " },\n", + " \"edgeCollections\": {\"Ratings\": {\"weight\": \"Rating\"}},\n", + "}\n", + "\n", + "# Create the DGL Graph\n", + "dgl_g = adbdgl_adapter.arangodb_to_dgl(\"IMDB\", metagraph_v2)\n", + "\n", + "# Show graph data\n", + "print('\\n--------------')\n", + "print(dgl_g)\n", + "print('\\n--------------')\n", + "print(dgl_g.ndata)\n", + "print('--------------\\n')\n", + "print(dgl_g.edata)" ] }, { "cell_type": "markdown", "metadata": { - "id": "CNj1xKhwoJoL" + "id": "d5ijSCcY4bYs" }, "source": [ - "\n", - "#### MiniGCDataset Graphs with attributes" + "#### Via ArangoDB-DGL metagraph 3" ] }, { "cell_type": "markdown", "metadata": { - "id": "CZ1UX9YX1Zzo" + "id": "P1aKzxxZrUXJ" }, "source": [ - "Data source\n", - "* [DGL Mini Graph Classification Dataset](https://docs.dgl.ai/en/0.6.x/api/python/dgl.data.html#mini-graph-classification-dataset)\n", + "Data\n", + "* A fake DGL Heterogeneous graph\n", "\n", - "Package methods used\n", - "* [`adbdgl_adapter.adapter.dgl_to_arangodb()`](https://github.com/arangoml/dgl-adapter/blob/2.0.0/adbdgl_adapter/adapter.py#L215-L311)\n", - "* [`adbdgl_adapter.controller._dgl_feature_to_adb_attribute()`](https://github.com/arangoml/dgl-adapter/blob/2.0.0/adbdgl_adapter/controller.py#L49-L70)\n", + "API\n", + "* `adbdgl_adapter.adapter.arangodb_to_dgl()`\n", "\n", - "Important notes\n", - "* The `name` parameters in this case are simply for naming your ArangoDB graph.\n", - "* We are creating a custom `ADBDGL_Controller` to specify *how* to convert our DGL node/edge features into ArangoDB vertex/edge attributes. View the default `ADBDGL_Controller` [here](https://github.com/arangoml/dgl-adapter/blob/2.0.0/adbdgl_adapter/controller.py#L11)." + "Notes\n", + "* The `name` parameter is purely for documentation purposes in this case.\n", + "* The `metagraph` parameter is an object defining vertex & edge collections to import to DGL, along with collection-level specifications to indicate which ArangoDB attributes will become DGL features/labels. In this example, we rely on user-defined functions to handle ArangoDB attribute to DGL feature conversion." ] }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "metadata": { "colab": { - "base_uri": "https://localhost:8080/" + "base_uri": "https://localhost:8080/", + "height": 377, + "referenced_widgets": [ + "e4b7b35461e848f5819b9f38d67ee652", + "9968f928e28147f7a0956aff8412a608", + "54801c3c74494fe8bf9e2a7fb64bde48", + "903622e283524c7f89635599920c2b14", + "f0d4515c88a44775be59c4e1a0b3c60a", + "9e1eb071f0b24cb6a8d206477b10b831" + ] }, - "id": "jbJsvMMaoJoT", - "outputId": "6dba7563-84b8-4934-a07f-1525ef67bd5e" + "id": "t-lNli3d4bY0", + "outputId": "7bc48392-81a7-4232-aad2-931ff3c8ca48" }, "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "e4b7b35461e848f5819b9f38d67ee652", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "54801c3c74494fe8bf9e2a7fb64bde48", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "f0d4515c88a44775be59c4e1a0b3c60a", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n"
+            ],
+            "text/plain": []
+          },
+          "metadata": {},
+          "output_type": "display_data"
+        },
+        {
+          "data": {
+            "text/html": [
+              "
\n",
+              "
\n" + ], + "text/plain": [ + "\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, { "name": "stderr", "output_type": "stream", "text": [ - "[2022/05/25 17:25:16 +0000] [60] [INFO] - adbdgl_adapter: Instantiated ADBDGL_Adapter with database 'TUT56z6dbtgsoeu5cc6aixs7d'\n", - "[2022/05/25 17:25:17 +0000] [60] [INFO] - adbdgl_adapter: Created ArangoDB 'Lollipop_With_Attributes' Graph\n", - "[2022/05/25 17:25:17 +0000] [60] [INFO] - adbdgl_adapter: Created ArangoDB 'Hypercube_With_Attributes' Graph\n", - "[2022/05/25 17:25:18 +0000] [60] [INFO] - adbdgl_adapter: Created ArangoDB 'Clique_With_Attributes' Graph\n" + "[2022/08/05 20:39:00 +0000] [61] [INFO] - adbdgl_adapter: Created DGL 'FakeHetero' Graph\n", + "INFO:adbdgl_adapter:Created DGL 'FakeHetero' Graph\n" ] }, { @@ -1428,128 +3393,83 @@ "output_type": "stream", "text": [ "\n", - "--------------------\n", - "URL: https://tutorials.arangodb.cloud:8529\n", - "Username: TUTtj3263blez70kmqdi3ts\n", - "Password: TUTf6tursgxqogdo3ww3nplb\n", - "Database: TUT56z6dbtgsoeu5cc6aixs7d\n", - "--------------------\n", + "--------------\n", + "Graph(num_nodes={'game': 5, 'user': 4},\n", + " num_edges={('user', 'plays', 'game'): 2},\n", + " metagraph=[('user', 'game', 'plays')])\n", "\n", - "\\View the created graphs here:\n", + "--------------\n", + "defaultdict(, {'features': {'game': tensor([[0, 0],\n", + " [0, 1],\n", + " [1, 0],\n", + " [1, 1],\n", + " [1, 1]]), 'user': tensor([21, 44, 16, 25])}, 'label': {'user': tensor([1, 2, 0, 1])}})\n", + "--------------\n", "\n", - "1) https://tutorials.arangodb.cloud:8529/_db/TUT56z6dbtgsoeu5cc6aixs7d/_admin/aardvark/index.html#graph/Lollipop_With_Attributes\n", - "2) https://tutorials.arangodb.cloud:8529/_db/TUT56z6dbtgsoeu5cc6aixs7d/_admin/aardvark/index.html#graph/Hypercube_With_Attributes\n", - "3) https://tutorials.arangodb.cloud:8529/_db/TUT56z6dbtgsoeu5cc6aixs7d/_admin/aardvark/index.html#graph/Clique_With_Attributes\n", - "\n" + "{'features': tensor([[ 6, 1],\n", + " [1000, 0]])}\n" ] } ], "source": [ - "# Load the dgl graphs\n", - "dgl_lollipop_graph = remove_self_loop(MiniGCDataset(8, 7, 8)[3][0])\n", - "dgl_hypercube_graph = remove_self_loop(MiniGCDataset(8, 8, 9)[4][0])\n", - "dgl_clique_graph = remove_self_loop(MiniGCDataset(8, 6, 7)[6][0])\n", - "\n", - " # Add DGL Node & Edge Features to each graph\n", - "dgl_lollipop_graph.ndata[\"random_ndata\"] = torch.tensor(\n", - " [[i, i, i] for i in range(0, dgl_lollipop_graph.num_nodes())]\n", - ")\n", - "dgl_lollipop_graph.edata[\"random_edata\"] = torch.rand(dgl_lollipop_graph.num_edges())\n", - "\n", - "dgl_hypercube_graph.ndata[\"random_ndata\"] = torch.rand(dgl_hypercube_graph.num_nodes())\n", - "dgl_hypercube_graph.edata[\"random_edata\"] = torch.tensor(\n", - " [[[i], [i], [i]] for i in range(0, dgl_hypercube_graph.num_edges())]\n", - ")\n", - "\n", - "dgl_clique_graph.ndata['clique_ndata'] = torch.tensor([1,2,3,4,5,6])\n", - "dgl_clique_graph.edata['clique_edata'] = torch.tensor(\n", - " [1 if i % 2 == 0 else 0 for i in range(0, dgl_clique_graph.num_edges())]\n", - ")\n", - "\n", - "# A user-defined Controller class is OPTIONAL when converting DGL features\n", - "# to ArangoDB attributes. NOTE: A custom Controller is NOT needed if you want to\n", - "# keep the numerical-based values of your DGL features.\n", - "class Clique_ADBDGL_Controller(ADBDGL_Controller):\n", - " \"\"\"ArangoDB-DGL controller.\n", - "\n", - " Responsible for controlling how ArangoDB attributes\n", - " are converted into DGL features, and vice-versa.\n", + "# Define the metagraph that transfers attributes via user-defined functions\n", + "def udf_user_features(user_df):\n", + " # process the user_df Pandas DataFrame to return a feature matrix in a tensor\n", + " # user_df[\"features\"] = ...\n", + " return torch.tensor(user_df[\"features\"].to_list())\n", "\n", - " You can derive your own custom ADBDGL_Controller if you want to maintain\n", - " consistency between your ArangoDB attributes & your DGL features.\n", - " \"\"\"\n", - "\n", - " def _dgl_feature_to_adb_attribute(self, key: str, col: str, val: Tensor):\n", - " \"\"\"\n", - " Given a DGL feature key, its assigned value (for an arbitrary node or edge),\n", - " and the collection it belongs to, convert it to a valid ArangoDB attribute\n", - " (e.g string, list, number, ...).\n", - "\n", - " NOTE: No action is needed here if you want to keep the numerical-based values\n", - " of your DGL features.\n", - "\n", - " :param key: The DGL attribute key name\n", - " :type key: str\n", - " :param col: The ArangoDB collection of the (soon-to-be) ArangoDB document.\n", - " :type col: str\n", - " :param val: The assigned attribute value of the DGL node.\n", - " :type val: Tensor\n", - " :return: The feature's representation as an ArangoDB Attribute\n", - " :rtype: Any\n", - " \"\"\"\n", "\n", - " if key == \"clique_ndata\":\n", - " try:\n", - " return [\"Eins\", \"Zwei\", \"Drei\", \"Vier\", \"Fünf\", \"Sechs\"][key-1]\n", - " except:\n", - " return -1\n", + "def udf_game_features(game_df):\n", + " # process the game_df Pandas DataFrame to return a feature matrix in a tensor\n", + " # game_df[\"features\"] = ...\n", + " return torch.tensor(game_df[\"features\"].to_list())\n", "\n", - " if key == \"clique_edata\":\n", - " return bool(val)\n", - "\n", - " return super()._dgl_feature_to_adb_attribute(key, col, val)\n", - "\n", - "# Re-instantiate a new adapter specifically for the Clique Graph Conversion\n", - "clique_adbgl_adapter = ADBDGL_Adapter(db, Clique_ADBDGL_Controller())\n", - "\n", - "# Create the ArangoDB graphs\n", - "lollipop = \"Lollipop_With_Attributes\"\n", - "hypercube = \"Hypercube_With_Attributes\"\n", - "clique = \"Clique_With_Attributes\"\n", "\n", - "db.delete_graph(lollipop, drop_collections=True, ignore_missing=True)\n", - "db.delete_graph(hypercube, drop_collections=True, ignore_missing=True)\n", - "db.delete_graph(clique, drop_collections=True, ignore_missing=True)\n", + "metagraph_v3 = {\n", + " \"vertexCollections\": {\n", + " \"user\": {\n", + " \"features\": udf_user_features, # supports named functions\n", + " \"label\": lambda df: torch.tensor(df[\"label\"].to_list()), # also supports lambda functions\n", + " },\n", + " \"game\": {\"features\": udf_game_features},\n", + " },\n", + " \"edgeCollections\": {\n", + " \"plays\": {\"features\": (lambda df: torch.tensor(df[\"features\"].to_list()))},\n", + " },\n", + "}\n", "\n", - "adb_lollipop_graph = adbdgl_adapter.dgl_to_arangodb(lollipop, dgl_lollipop_graph)\n", - "adb_hypercube_graph = adbdgl_adapter.dgl_to_arangodb(hypercube, dgl_hypercube_graph)\n", - "adb_clique_graph = clique_adbgl_adapter.dgl_to_arangodb(clique, dgl_clique_graph) # Notice the new adapter here!\n", + "# Create the DGL Graph\n", + "dgl_g = adbdgl_adapter.arangodb_to_dgl(\"FakeHetero\", metagraph_v3)\n", "\n", - "print('\\n--------------------')\n", - "print(\"URL: \" + con[\"url\"])\n", - "print(\"Username: \" + con[\"username\"])\n", - "print(\"Password: \" + con[\"password\"])\n", - "print(\"Database: \" + con[\"dbName\"])\n", - "print('--------------------\\n')\n", - "print(\"View the created graphs here:\\n\")\n", - "print(f\"1) {con['url']}/_db/{con['dbName']}/_admin/aardvark/index.html#graph/{lollipop}\")\n", - "print(f\"2) {con['url']}/_db/{con['dbName']}/_admin/aardvark/index.html#graph/{hypercube}\")\n", - "print(f\"3) {con['url']}/_db/{con['dbName']}/_admin/aardvark/index.html#graph/{clique}\\n\")" + "# Show graph data\n", + "print('\\n--------------')\n", + "print(dgl_g)\n", + "print('\\n--------------')\n", + "print(dgl_g.ndata)\n", + "print('--------------\\n')\n", + "print(dgl_g.edata)" ] } ], "metadata": { "colab": { "collapsed_sections": [ - "KS9c-vE5eG89", "ot1oJqn7m78n", "Oc__NAd1eG8-", "7y81WHO8eG8_", "QfE_tKxneG9A", + "bvzJXSHHTi3v", + "UafSB_3JZNwK", + "CNj1xKhwoJoL", + "n08RC_GtkDrC", + "mk6m0hBRkkkT", "uByvwf9feG9A", - "bvzJXSHHTi3v" + "ZrEDmtqCVD0W", + "RQ4CknYfUEuz", + "qEH6OdSB23Ya", + "0806IB4o3WRz", + "d5ijSCcY4bYs" ], - "name": "ArangoDB_DGL_Adapter_v2.ipynb", "provenance": [] }, "kernelspec": { @@ -1568,6 +3488,3723 @@ "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.7.6" + }, + "widgets": { + "application/vnd.jupyter.widget-state+json": { + "0083494093574c50952dd066502a708d": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "0478c90ef8234f3a8987dbe9cd3030b2": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "09d25097c75c4fa8a2c7376f1965afc5": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "09e8c93741bf45acb69ba9e757107564": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_d7d06973b2984eb19fa050409bf62222", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(DGL → ADB): ('user', 'plays', 'game') (2) ▰▰▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;151;196;35m(DGL → ADB): ('user', 'plays', 'game') (2)\u001b[0m \u001b[38;2;153;70;2m▰▰▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "13d0f7da120b40b993ce3c0b257d5788": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "14b29dc1f2b8454fa9acc1d79dcd4870": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "1690574b32cc4b48a8b87520458d5066": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_a9edf4f85a4a4504b155608bb740178a", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(ADB → DGL): topic ▰▱▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;49;155;245m(ADB → DGL): topic\u001b[0m \u001b[38;2;252;253;252m▰▱▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "1dea128bde204a8fa53e094e014183fe": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_50f8ff3637ee4fc7af8c811cd5d177be", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(DGL → ADB): topic (3) ▰▰▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;151;196;35m(DGL → ADB): topic (3)\u001b[0m \u001b[38;2;153;70;2m▰▰▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "21e50aa61c3d4de19b5cc0bbe27d53c9": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "289a6e16c3d640c29d96edf09908bd0f": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_61f3832c906445a3ab7e7ba9b41c0127", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(DGL → ADB): topic (3) ▰▰▰▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;151;196;35m(DGL → ADB): topic (3)\u001b[0m \u001b[38;2;153;70;2m▰▰▰▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "2a380fe111794c3a951cdafa4a2bf0b3": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "2b13e46a722e4be384fad74e1b3e6461": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_848230df62434c77b5b18f9a43e2d14f", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(ADB → DGL): Movies ▰▰▰▰▰▰▰ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;49;155;245m(ADB → DGL): Movies\u001b[0m \u001b[38;2;252;253;252m▰▰▰▰▰▰▰\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "2c2900512b5244d3a0fcaf7409446d0e": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_c5d064af7f4a49dca6716f98d052e951", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(ADB → DGL): plays ▰▱▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;49;155;245m(ADB → DGL): plays\u001b[0m \u001b[38;2;252;253;252m▰▱▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "2d1fc41d509e481cb779603827359184": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_87d9c9de620847f48b4088e8577cd653", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(DGL → ADB): ('Karate_N', 'Karate_E', 'Karate_N') (156) ▰▱▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;151;196;35m(DGL → ADB): ('Karate_N', 'Karate_E', 'Karate_N') (156)\u001b[0m \u001b[38;2;153;70;2m▰▱▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "31a9f782f36d407f8cc42b19679c5c2c": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_9fd8d07a43cd4c06a2d448047ede846c", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(ADB → DGL): follows ▰▱▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;49;155;245m(ADB → DGL): follows\u001b[0m \u001b[38;2;252;253;252m▰▱▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "345a5984959c4e57b7e2715fa8eeef8f": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_99e6613c4187459396eea503453934cb", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(DGL → ADB): game (5) ▰▱▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;151;196;35m(DGL → ADB): game (5)\u001b[0m \u001b[38;2;153;70;2m▰▱▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "34c4ef0c4aa5454893c0f0fa35902fbd": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "38aaa492d75c48f38de60ea0cc5fa93f": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_63845b04ecbc40de8bcc017d754ac907", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(ADB → DGL): game ▰▱▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;49;155;245m(ADB → DGL): game\u001b[0m \u001b[38;2;252;253;252m▰▱▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "3bc228aa98454dc59a604c8f7ff6b2a0": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_65138d18c9c449d1aaaad387293c5ede", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(DGL → ADB): user (4) ▰▰▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;151;196;35m(DGL → ADB): user (4)\u001b[0m \u001b[38;2;153;70;2m▰▰▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "3d081c88cd2945fa9534de722669ada9": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_82f996185e8444ada5e18602e2f8e105", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(ADB → DGL): Ratings ▰▱▱▱▱▱▱ 0:00:06\n
\n", + "text/plain": "\u001b[38;2;49;155;245m(ADB → DGL): Ratings\u001b[0m \u001b[38;2;252;253;252m▰▱▱▱▱▱▱\u001b[0m \u001b[33m0:00:06\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "3ea99b2a6b4246d3abf628ca743f9f24": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_841ce4f5d391457e858c3c48185e259d", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(DGL → ADB): ('user', 'follows', 'topic') (2) ▰▱▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;151;196;35m(DGL → ADB): ('user', 'follows', 'topic') (2)\u001b[0m \u001b[38;2;153;70;2m▰▱▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "3f633be94c7d466ea40571e805a76948": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "3fc8b14d794a46118b328893bd216405": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_c7e222474ff445fe86e4e599848b2ae2", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(DGL → ADB): game (5) ▰▰▰▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;151;196;35m(DGL → ADB): game (5)\u001b[0m \u001b[38;2;153;70;2m▰▰▰▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "404a19cadaca4b85a957cad231b73cbb": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "40da9dd52dd6443684b990f74b6cb876": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "46b88027e41a43578ebcc47513dd6911": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "4ab3c113235746cab5fde158756ab420": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "4b7f5f21b98b4c5d8475929bf1f01a65": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_404a19cadaca4b85a957cad231b73cbb", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(ADB → DGL): topic ▰▱▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;49;155;245m(ADB → DGL): topic\u001b[0m \u001b[38;2;252;253;252m▰▱▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "4e085418ce1b41e1bc24ad6acea92fc4": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_7b5dba3f4d50466eb2071cb13548ef1b", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(ADB → DGL): plays ▰▱▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;49;155;245m(ADB → DGL): plays\u001b[0m \u001b[38;2;252;253;252m▰▱▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "50f8ff3637ee4fc7af8c811cd5d177be": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "54801c3c74494fe8bf9e2a7fb64bde48": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_903622e283524c7f89635599920c2b14", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(ADB → DGL): game ▰▱▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;49;155;245m(ADB → DGL): game\u001b[0m \u001b[38;2;252;253;252m▰▱▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "575205f1a4e64c5d977e69d4939a5605": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_d20843bfa9064d56b37aaea011789a26", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(DGL → ADB): ('user', 'follows', 'user') (2) ▰▱▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;151;196;35m(DGL → ADB): ('user', 'follows', 'user') (2)\u001b[0m \u001b[38;2;153;70;2m▰▱▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "59405e2d0c164d5b965680cc9d9cd8f3": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_2a380fe111794c3a951cdafa4a2bf0b3", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(ADB → DGL): Users ▰▰▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;49;155;245m(ADB → DGL): Users\u001b[0m \u001b[38;2;252;253;252m▰▰▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "5c310145af4f4c90b659dee771185ab6": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "5f5c119141a24cab907ceb2da27e0244": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_46b88027e41a43578ebcc47513dd6911", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(ADB → DGL): topic ▰▱▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;49;155;245m(ADB → DGL): topic\u001b[0m \u001b[38;2;252;253;252m▰▱▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "61d2a0426c324309ab51111933276e3d": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_77c208846c1e4503bc22a5b5504f89ee", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(DGL → ADB): Karate_N (34) ▰▰▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;151;196;35m(DGL → ADB): Karate_N (34)\u001b[0m \u001b[38;2;153;70;2m▰▰▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "61f3832c906445a3ab7e7ba9b41c0127": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "63845b04ecbc40de8bcc017d754ac907": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "65138d18c9c449d1aaaad387293c5ede": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "6582a9d3fe044d5380d8e918f3bc5a6d": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_40da9dd52dd6443684b990f74b6cb876", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(DGL → ADB): user (4) ▰▱▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;151;196;35m(DGL → ADB): user (4)\u001b[0m \u001b[38;2;153;70;2m▰▱▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "6744eb60dfa04a8598fca3b998ce3077": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_09d25097c75c4fa8a2c7376f1965afc5", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(DGL → ADB): user (4) ▰▱▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;151;196;35m(DGL → ADB): user (4)\u001b[0m \u001b[38;2;153;70;2m▰▱▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "712770e675424d7eb0c8efd6c34f2012": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "77b31c42e914410aaea93044f1390121": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_8349f1e6b1f34680bacd7de1a1937122", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(ADB → DGL): user ▰▱▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;49;155;245m(ADB → DGL): user\u001b[0m \u001b[38;2;252;253;252m▰▱▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "77c208846c1e4503bc22a5b5504f89ee": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "7a43c4b816da4a40b0eed167a85eef22": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_eb376d5cf782424aaccbce31f0d3ede5", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(ADB → DGL): game ▰▱▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;49;155;245m(ADB → DGL): game\u001b[0m \u001b[38;2;252;253;252m▰▱▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "7a4db2b18c634bef932fb9b1157d4af1": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_b5be8c1e4ab3415c9fffbb61aeb0fff3", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(ADB → DGL): follows ▰▱▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;49;155;245m(ADB → DGL): follows\u001b[0m \u001b[38;2;252;253;252m▰▱▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "7b5dba3f4d50466eb2071cb13548ef1b": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "80d19dc0d20842c3b5c7313c0ad23d24": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_0478c90ef8234f3a8987dbe9cd3030b2", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(DGL → ADB): ('user', 'follows', 'topic') (2) ▰▰▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;151;196;35m(DGL → ADB): ('user', 'follows', 'topic') (2)\u001b[0m \u001b[38;2;153;70;2m▰▰▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "80e69b3aa98b44e295efe3940c1146c2": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "8128e6d80fcb4a8ca0a72097bb8b6521": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "82f996185e8444ada5e18602e2f8e105": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "8349f1e6b1f34680bacd7de1a1937122": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "841ce4f5d391457e858c3c48185e259d": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "8444e147be8f44aba06ec1f8a880104e": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_80e69b3aa98b44e295efe3940c1146c2", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(DGL → ADB): ('user', 'follows', 'user') (2) ▰▱▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;151;196;35m(DGL → ADB): ('user', 'follows', 'user') (2)\u001b[0m \u001b[38;2;153;70;2m▰▱▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "848230df62434c77b5b18f9a43e2d14f": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "87d9c9de620847f48b4088e8577cd653": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "88e83ddc1ca1464291e1631b8fced847": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_a9c14a3f339445338119631c8e56ff68", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(DGL → ADB): ('user', 'plays', 'game') (2) ▰▱▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;151;196;35m(DGL → ADB): ('user', 'plays', 'game') (2)\u001b[0m \u001b[38;2;153;70;2m▰▱▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "8bf075c6f7834d3fa905b7ddc37cf128": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_b080f26fe35241fb9cca48e97bc9ef0c", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(DGL → ADB): ('user', 'plays', 'game') (2) ▰▱▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;151;196;35m(DGL → ADB): ('user', 'plays', 'game') (2)\u001b[0m \u001b[38;2;153;70;2m▰▱▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "903622e283524c7f89635599920c2b14": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "9403e71c2bbe46bd9e6d49d555264554": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_34c4ef0c4aa5454893c0f0fa35902fbd", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(ADB → DGL): game ▰▰▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;49;155;245m(ADB → DGL): game\u001b[0m \u001b[38;2;252;253;252m▰▰▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "968020b1388e4883843575d9198af1cd": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_f1a08470110e4099af2a3d4cf4d0f956", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(DGL → ADB): topic (3) ▰▰▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;151;196;35m(DGL → ADB): topic (3)\u001b[0m \u001b[38;2;153;70;2m▰▰▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "96e57d98afce44cd8269204dd19ff6e0": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_da43ef4a8c6a41f9bda153a0cd14c2d7", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(DGL → ADB): topic (3) ▰▰▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;151;196;35m(DGL → ADB): topic (3)\u001b[0m \u001b[38;2;153;70;2m▰▰▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "97e7543f202749c197515a9c5c79adbe": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "987bf80aee4b4b97bfad1699f8384af8": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_4ab3c113235746cab5fde158756ab420", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(DGL → ADB): ('user', 'follows', 'user') (2) ▰▱▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;151;196;35m(DGL → ADB): ('user', 'follows', 'user') (2)\u001b[0m \u001b[38;2;153;70;2m▰▱▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "9968f928e28147f7a0956aff8412a608": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "99bbe81a24db49ff9352987fd97649cd": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_21e50aa61c3d4de19b5cc0bbe27d53c9", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(DGL → ADB): user (4) ▰▰▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;151;196;35m(DGL → ADB): user (4)\u001b[0m \u001b[38;2;153;70;2m▰▰▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "99e6613c4187459396eea503453934cb": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "9b2b3abbe2c04af0bc232c9b16bfd90d": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "9e1eb071f0b24cb6a8d206477b10b831": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "9fd8d07a43cd4c06a2d448047ede846c": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "a9c14a3f339445338119631c8e56ff68": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "a9edf4f85a4a4504b155608bb740178a": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "b080f26fe35241fb9cca48e97bc9ef0c": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "b5be8c1e4ab3415c9fffbb61aeb0fff3": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "bd8b6caa7d2d4df1a99b1870ecc0ae46": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_13d0f7da120b40b993ce3c0b257d5788", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(ADB → DGL): plays ▰▱▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;49;155;245m(ADB → DGL): plays\u001b[0m \u001b[38;2;252;253;252m▰▱▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "c5d064af7f4a49dca6716f98d052e951": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "c61e3997250d4f93a8e0494db674892d": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_97e7543f202749c197515a9c5c79adbe", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(DGL → ADB): ('user', 'follows', 'user') (2) ▰▱▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;151;196;35m(DGL → ADB): ('user', 'follows', 'user') (2)\u001b[0m \u001b[38;2;153;70;2m▰▱▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "c6cffa0a64434e56879ba2a8c9de018a": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_0083494093574c50952dd066502a708d", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(DGL → ADB): game (5) ▰▰▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;151;196;35m(DGL → ADB): game (5)\u001b[0m \u001b[38;2;153;70;2m▰▰▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "c7e222474ff445fe86e4e599848b2ae2": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "cb8167f00277413eaaa2ad6e0e162fab": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_8128e6d80fcb4a8ca0a72097bb8b6521", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(DGL → ADB): ('user', 'follows', 'topic') (2) ▰▱▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;151;196;35m(DGL → ADB): ('user', 'follows', 'topic') (2)\u001b[0m \u001b[38;2;153;70;2m▰▱▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "d20843bfa9064d56b37aaea011789a26": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "d7d06973b2984eb19fa050409bf62222": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "da43ef4a8c6a41f9bda153a0cd14c2d7": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "dd2376f84c794b4989f385a5bb147bd8": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "e4b7b35461e848f5819b9f38d67ee652": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_9968f928e28147f7a0956aff8412a608", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(ADB → DGL): user ▰▱▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;49;155;245m(ADB → DGL): user\u001b[0m \u001b[38;2;252;253;252m▰▱▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "ea5e9803c5de4d2bbb48782069b9829b": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_3f633be94c7d466ea40571e805a76948", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(DGL → ADB): game (5) ▰▱▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;151;196;35m(DGL → ADB): game (5)\u001b[0m \u001b[38;2;153;70;2m▰▱▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "ea88ab86e9774ed78ea62daa6e338637": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_712770e675424d7eb0c8efd6c34f2012", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(ADB → DGL): follows ▰▱▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;49;155;245m(ADB → DGL): follows\u001b[0m \u001b[38;2;252;253;252m▰▱▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "eb376d5cf782424aaccbce31f0d3ede5": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "ec7b8b0b853f463fa079dda845891391": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_dd2376f84c794b4989f385a5bb147bd8", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(DGL → ADB): ('user', 'plays', 'game') (2) ▰▱▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;151;196;35m(DGL → ADB): ('user', 'plays', 'game') (2)\u001b[0m \u001b[38;2;153;70;2m▰▱▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "f01997b9b43d43368d632e26ba9732ad": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_14b29dc1f2b8454fa9acc1d79dcd4870", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(ADB → DGL): user ▰▱▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;49;155;245m(ADB → DGL): user\u001b[0m \u001b[38;2;252;253;252m▰▱▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "f0d4515c88a44775be59c4e1a0b3c60a": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_9e1eb071f0b24cb6a8d206477b10b831", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(ADB → DGL): plays ▰▱▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;49;155;245m(ADB → DGL): plays\u001b[0m \u001b[38;2;252;253;252m▰▱▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "f1a08470110e4099af2a3d4cf4d0f956": { + "model_module": "@jupyter-widgets/base", + "model_module_version": "1.2.0", + "model_name": "LayoutModel", + "state": { + "_model_module": "@jupyter-widgets/base", + "_model_module_version": "1.2.0", + "_model_name": "LayoutModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/base", + "_view_module_version": "1.2.0", + "_view_name": "LayoutView", + "align_content": null, + "align_items": null, + "align_self": null, + "border": null, + "bottom": null, + "display": null, + "flex": null, + "flex_flow": null, + "grid_area": null, + "grid_auto_columns": null, + "grid_auto_flow": null, + "grid_auto_rows": null, + "grid_column": null, + "grid_gap": null, + "grid_row": null, + "grid_template_areas": null, + "grid_template_columns": null, + "grid_template_rows": null, + "height": null, + "justify_content": null, + "justify_items": null, + "left": null, + "margin": null, + "max_height": null, + "max_width": null, + "min_height": null, + "min_width": null, + "object_fit": null, + "object_position": null, + "order": null, + "overflow": null, + "overflow_x": null, + "overflow_y": null, + "padding": null, + "right": null, + "top": null, + "visibility": null, + "width": null + } + }, + "f9fdfe6ce44e4e1c8f513f82efca3e0d": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_9b2b3abbe2c04af0bc232c9b16bfd90d", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(DGL → ADB): ('user', 'follows', 'topic') (2) ▰▱▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;151;196;35m(DGL → ADB): ('user', 'follows', 'topic') (2)\u001b[0m \u001b[38;2;153;70;2m▰▱▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + }, + "fd2db543279f4a13ab6376b9c23160e0": { + "model_module": "@jupyter-widgets/output", + "model_module_version": "1.0.0", + "model_name": "OutputModel", + "state": { + "_dom_classes": [], + "_model_module": "@jupyter-widgets/output", + "_model_module_version": "1.0.0", + "_model_name": "OutputModel", + "_view_count": null, + "_view_module": "@jupyter-widgets/output", + "_view_module_version": "1.0.0", + "_view_name": "OutputView", + "layout": "IPY_MODEL_5c310145af4f4c90b659dee771185ab6", + "msg_id": "", + "outputs": [ + { + "data": { + "text/html": "
(ADB → DGL): user ▰▱▱▱▱▱▱ 0:00:00\n
\n", + "text/plain": "\u001b[38;2;49;155;245m(ADB → DGL): user\u001b[0m \u001b[38;2;252;253;252m▰▱▱▱▱▱▱\u001b[0m \u001b[33m0:00:00\u001b[0m\n" + }, + "metadata": {}, + "output_type": "display_data" + } + ] + } + } + } } }, "nbformat": 4, diff --git a/setup.cfg b/setup.cfg index 475af62..a91261b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,7 +3,7 @@ profile = black [flake8] max-line-length = 88 -extend-ignore = E203, E741, W503 +extend-ignore = E203, E741, W503, E731 exclude =.git .idea .*_cache dist venv [mypy] diff --git a/setup.py b/setup.py index 037c9c1..0b56dea 100644 --- a/setup.py +++ b/setup.py @@ -11,24 +11,26 @@ long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/arangoml/dgl-adapter", - keywords=["arangodb", "dgl", "adapter"], + keywords=["arangodb", "dgl", "deep graph library", "adapter"], packages=["adbdgl_adapter"], include_package_data=True, - python_requires=">=3.7", + python_requires=">=3.8", license="Apache Software License", install_requires=[ "requests>=2.27.1", - "dgl>=0.6.1", - "torch>=1.10.2", - "python-arango>=7.4.1", + "rich>=12.5.1", + "pandas>=1.3.5", + "dgl~=1.0", + "torch>=1.12.0", + "python-arango~=7.6", "setuptools>=45", ], extras_require={ "dev": [ - "black", - "flake8>=3.8.0", - "isort>=5.0.0", - "mypy>=0.790", + "black==23.3.0", + "flake8==6.0.0", + "isort==5.12.0", + "mypy==1.4.1", "pytest>=6.0.0", "pytest-cov>=2.0.0", "coveralls>=3.3.1", @@ -41,9 +43,10 @@ "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Topic :: Utilities", "Typing :: Typed", ], diff --git a/tests/conftest.py b/tests/conftest.py index f31c304..0c8acaf 100755 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,17 +2,19 @@ import os import subprocess from pathlib import Path -from typing import Any +from typing import Any, Callable, Dict from arango import ArangoClient from arango.database import StandardDatabase -from dgl import DGLGraph, heterograph, remove_self_loop +from dgl import DGLGraph, DGLHeteroGraph, heterograph, remove_self_loop from dgl.data import KarateClubDataset, MiniGCDataset -from torch import ones, rand, tensor, zeros +from pandas import DataFrame +from torch import Tensor, rand, tensor -from adbdgl_adapter import ADBDGL_Adapter -from adbdgl_adapter.typings import Json +from adbdgl_adapter import ADBDGL_Adapter, ADBDGL_Controller +from adbdgl_adapter.typings import DGLCanonicalEType, Json +con: Json db: StandardDatabase adbdgl_adapter: ADBDGL_Adapter PROJECT_DIR = Path(__file__).parent.parent @@ -26,6 +28,7 @@ def pytest_addoption(parser: Any) -> None: def pytest_configure(config: Any) -> None: + global con con = { "url": config.getoption("url"), "username": config.getoption("username"), @@ -48,37 +51,34 @@ def pytest_configure(config: Any) -> None: global adbdgl_adapter adbdgl_adapter = ADBDGL_Adapter(db, logging_lvl=logging.DEBUG) - if db.has_graph("fraud-detection") is False: - arango_restore(con, "examples/data/fraud_dump") - db.create_graph( - "fraud-detection", - edge_definitions=[ - { - "edge_collection": "accountHolder", - "from_vertex_collections": ["customer"], - "to_vertex_collections": ["account"], - }, - { - "edge_collection": "transaction", - "from_vertex_collections": ["account"], - "to_vertex_collections": ["account"], - }, - ], - ) + +def pytest_exception_interact(node: Any, call: Any, report: Any) -> None: + try: + if report.failed: + params: Dict[str, Any] = node.callspec.params + + graph_name = params.get("name") + adapter = params.get("adapter") + if graph_name and adapter: + db: StandardDatabase = adapter.db + db.delete_graph(graph_name, drop_collections=True, ignore_missing=True) + except AttributeError: + print(node) + print(dir(node)) + print("Could not delete graph") def arango_restore(con: Json, path_to_data: str) -> None: - restore_prefix = "./assets/" if os.getenv("GITHUB_ACTIONS") else "" + restore_prefix = "./tools/" if os.getenv("GITHUB_ACTIONS") else "" protocol = "http+ssl://" if "https://" in con["url"] else "tcp://" url = protocol + con["url"].partition("://")[-1] - # A small hack to work around empty passwords - password = f"--server.password {con['password']}" if con["password"] else "" subprocess.check_call( - f'chmod -R 755 ./assets/arangorestore && {restore_prefix}arangorestore \ + f'chmod -R 755 ./tools/arangorestore && {restore_prefix}arangorestore \ -c none --server.endpoint {url} --server.database {con["dbName"]} \ - --server.username {con["username"]} {password} \ - --input-directory "{PROJECT_DIR}/{path_to_data}"', + --server.username {con["username"]} \ + --server.password "{con["password"]}" \ + --input-directory "{PROJECT_DIR}/{path_to_data}"', cwd=f"{PROJECT_DIR}/tests", shell=True, ) @@ -88,41 +88,90 @@ def get_karate_graph() -> DGLGraph: return KarateClubDataset()[0] -def get_lollipop_graph() -> DGLGraph: - dgl_g = remove_self_loop(MiniGCDataset(8, 7, 8)[3][0]) - dgl_g.ndata["random_ndata"] = tensor( - [[i, i, i] for i in range(0, dgl_g.num_nodes())] - ) - dgl_g.edata["random_edata"] = rand(dgl_g.num_edges()) - return dgl_g - - def get_hypercube_graph() -> DGLGraph: dgl_g = remove_self_loop(MiniGCDataset(8, 8, 9)[4][0]) - dgl_g.ndata["random_ndata"] = rand(dgl_g.num_nodes()) - dgl_g.edata["random_edata"] = tensor( + dgl_g.ndata["node_features"] = rand(dgl_g.num_nodes()) + dgl_g.edata["edge_features"] = tensor( [[[i], [i], [i]] for i in range(0, dgl_g.num_edges())] ) return dgl_g -def get_clique_graph() -> DGLGraph: - dgl_g = remove_self_loop(MiniGCDataset(8, 6, 7)[6][0]) - dgl_g.ndata["random_ndata"] = ones(dgl_g.num_nodes()) - dgl_g.edata["random_edata"] = zeros(dgl_g.num_edges()) +def get_fake_hetero_dataset() -> DGLHeteroGraph: + data_dict = { + ("v0", "e0", "v0"): (tensor([0, 1, 2, 3, 4, 5]), tensor([5, 4, 3, 2, 1, 0])), + ("v0", "e0", "v1"): (tensor([0, 1, 2, 3, 4, 5]), tensor([0, 5, 1, 4, 2, 3])), + ("v0", "e0", "v2"): (tensor([0, 1, 2, 3, 4, 5]), tensor([1, 1, 1, 5, 5, 5])), + ("v1", "e0", "v1"): (tensor([0, 1, 2, 3, 4, 5]), tensor([3, 3, 3, 3, 3, 3])), + ("v1", "e0", "v2"): (tensor([0, 1, 2, 3, 4, 5]), tensor([0, 1, 2, 3, 4, 5])), + ("v2", "e0", "v2"): (tensor([0, 1, 2, 3, 4, 5]), tensor([5, 4, 3, 2, 1, 0])), + } + + dgl_g: DGLHeteroGraph = heterograph(data_dict) + dgl_g.nodes["v0"].data["features"] = rand(6) + dgl_g.nodes["v0"].data["label"] = tensor([1, 3, 2, 1, 3, 2]) + dgl_g.nodes["v1"].data["features"] = rand(6, 1) + dgl_g.nodes["v2"].data["features"] = rand(6, 2) + dgl_g.edata["features"] = {("v0", "e0", "v0"): rand(6, 3)} + return dgl_g -def get_social_graph() -> DGLGraph: +def get_social_graph() -> DGLHeteroGraph: dgl_g = heterograph( { ("user", "follows", "user"): (tensor([0, 1]), tensor([1, 2])), - ("user", "follows", "game"): (tensor([0, 1, 2]), tensor([0, 1, 2])), - ("user", "plays", "game"): (tensor([3, 3]), tensor([1, 2])), + ("user", "follows", "topic"): (tensor([1, 1]), tensor([1, 2])), + ("user", "plays", "game"): (tensor([0, 3]), tensor([3, 4])), } ) - dgl_g.nodes["user"].data["age"] = tensor([21, 16, 38, 64]) - dgl_g.edges["plays"].data["hours_played"] = tensor([3, 5]) + dgl_g.nodes["user"].data["features"] = tensor([21, 44, 16, 25]) + dgl_g.nodes["user"].data["label"] = tensor([1, 2, 0, 1]) + dgl_g.nodes["game"].data["features"] = tensor( + [[0, 0], [0, 1], [1, 0], [1, 1], [1, 1]] + ) + dgl_g.edges[("user", "plays", "game")].data["features"] = tensor( + [[6, 1], [1000, 0]] + ) return dgl_g + + +# For DGL to ArangoDB testing purposes +def udf_users_features_tensor_to_df(t: Tensor, adb_df: DataFrame) -> DataFrame: + adb_df[["age", "gender"]] = t.tolist() + adb_df["gender"] = adb_df["gender"].map({0: "Male", 1: "Female"}) + return adb_df + + +# For ArangoDB to DGL testing purposes +def udf_features_df_to_tensor(df: DataFrame) -> Tensor: + return tensor(df["features"].to_list()) + + +# For ArangoDB to DGL testing purposes +def udf_key_df_to_tensor(key: str) -> Callable[[DataFrame], Tensor]: + def f(df: DataFrame) -> Tensor: + return tensor(df[key].to_list()) + + return f + + +def label_tensor_to_2_column_dataframe(dgl_tensor: Tensor, df: DataFrame) -> DataFrame: + label_map = {0: "Class A", 1: "Class B", 2: "Class C"} + + df["label_num"] = dgl_tensor.tolist() + df["label_str"] = df["label_num"].map(label_map) + + return df + + +class Custom_ADBDGL_Controller(ADBDGL_Controller): + def _prepare_dgl_node(self, dgl_node: Json, node_type: str) -> Json: + dgl_node["foo"] = "bar" + return dgl_node + + def _prepare_dgl_edge(self, dgl_edge: Json, edge_type: DGLCanonicalEType) -> Json: + dgl_edge["bar"] = "foo" + return dgl_edge diff --git a/tests/data/adb/imdb_dump/ENCRYPTION b/tests/data/adb/imdb_dump/ENCRYPTION new file mode 100644 index 0000000..c86c3f3 --- /dev/null +++ b/tests/data/adb/imdb_dump/ENCRYPTION @@ -0,0 +1 @@ +none \ No newline at end of file diff --git a/tests/data/adb/imdb_dump/Movies.structure.json b/tests/data/adb/imdb_dump/Movies.structure.json new file mode 100644 index 0000000..eb9d80c --- /dev/null +++ b/tests/data/adb/imdb_dump/Movies.structure.json @@ -0,0 +1 @@ +{"allInSync":true,"indexes":[],"isReady":true,"parameters":{"cacheEnabled":false,"deleted":false,"distributeShardsLike":"_graphs","globallyUniqueId":"c2730595280/","id":"2730595280","isDisjoint":false,"isSmart":false,"isSmartChild":false,"isSystem":false,"keyOptions":{"allowUserKeys":true,"type":"traditional"},"minReplicationFactor":1,"name":"Movies","numberOfShards":1,"planId":"2730595280","replicationFactor":3,"schema":null,"shardKeys":["_key"],"shardingStrategy":"hash","shards":{"s2730595281":["PRMR-1vqwuhks","PRMR-bvgkeorm","PRMR-zpamyasv"]},"status":3,"type":2,"waitForSync":false,"writeConcern":1},"planVersion":10402} \ No newline at end of file diff --git a/tests/data/adb/imdb_dump/Movies_80662e1f485e79d07ef4973f6b1b9f88.data.json.gz b/tests/data/adb/imdb_dump/Movies_80662e1f485e79d07ef4973f6b1b9f88.data.json.gz new file mode 100644 index 0000000..b838d29 Binary files /dev/null and b/tests/data/adb/imdb_dump/Movies_80662e1f485e79d07ef4973f6b1b9f88.data.json.gz differ diff --git a/tests/data/adb/imdb_dump/Ratings.structure.json b/tests/data/adb/imdb_dump/Ratings.structure.json new file mode 100644 index 0000000..8571f0d --- /dev/null +++ b/tests/data/adb/imdb_dump/Ratings.structure.json @@ -0,0 +1 @@ +{"allInSync":true,"indexes":[],"isReady":true,"parameters":{"cacheEnabled":false,"deleted":false,"distributeShardsLike":"_graphs","globallyUniqueId":"c2728580616/","id":"2728580616","isDisjoint":false,"isSmart":false,"isSmartChild":false,"isSystem":false,"keyOptions":{"allowUserKeys":true,"type":"traditional"},"minReplicationFactor":1,"name":"Ratings","numberOfShards":1,"planId":"2728580616","replicationFactor":3,"schema":null,"shardKeys":["_key"],"shardingStrategy":"hash","shards":{"s2728580617":["PRMR-1vqwuhks","PRMR-bvgkeorm","PRMR-zpamyasv"]},"status":3,"type":3,"waitForSync":false,"writeConcern":1},"planVersion":10408} \ No newline at end of file diff --git a/tests/data/adb/imdb_dump/Ratings_e8dcd33ae274522f351c266f028eed7b.data.json.gz b/tests/data/adb/imdb_dump/Ratings_e8dcd33ae274522f351c266f028eed7b.data.json.gz new file mode 100644 index 0000000..b604626 Binary files /dev/null and b/tests/data/adb/imdb_dump/Ratings_e8dcd33ae274522f351c266f028eed7b.data.json.gz differ diff --git a/tests/data/adb/imdb_dump/Users.structure.json b/tests/data/adb/imdb_dump/Users.structure.json new file mode 100644 index 0000000..e5420b3 --- /dev/null +++ b/tests/data/adb/imdb_dump/Users.structure.json @@ -0,0 +1 @@ +{"allInSync":true,"indexes":[],"isReady":true,"parameters":{"cacheEnabled":false,"deleted":false,"distributeShardsLike":"_graphs","globallyUniqueId":"c2728580582/","id":"2728580582","isDisjoint":false,"isSmart":false,"isSmartChild":false,"isSystem":false,"keyOptions":{"allowUserKeys":true,"type":"traditional"},"minReplicationFactor":1,"name":"Users","numberOfShards":1,"planId":"2728580582","replicationFactor":3,"schema":null,"shardKeys":["_key"],"shardingStrategy":"hash","shards":{"s2728580583":["PRMR-1vqwuhks","PRMR-bvgkeorm","PRMR-zpamyasv"]},"status":3,"type":2,"waitForSync":false,"writeConcern":1},"planVersion":10405} \ No newline at end of file diff --git a/tests/data/adb/imdb_dump/Users_f9aae5fda8d810a29f12d1e61b4ab25f.data.json.gz b/tests/data/adb/imdb_dump/Users_f9aae5fda8d810a29f12d1e61b4ab25f.data.json.gz new file mode 100644 index 0000000..4eb3a4c Binary files /dev/null and b/tests/data/adb/imdb_dump/Users_f9aae5fda8d810a29f12d1e61b4ab25f.data.json.gz differ diff --git a/tests/data/adb/imdb_dump/dump.json b/tests/data/adb/imdb_dump/dump.json new file mode 100644 index 0000000..b2a69d9 --- /dev/null +++ b/tests/data/adb/imdb_dump/dump.json @@ -0,0 +1 @@ +{"database":"TUTdit9ohpgz1ntnbetsjstwi","lastTickAtDumpStart":"2732644865","properties":{"id":"2728554641","name":"TUTdit9ohpgz1ntnbetsjstwi","isSystem":false,"sharding":"","replicationFactor":1,"writeConcern":1,"path":""}} \ No newline at end of file diff --git a/tests/test_adapter.py b/tests/test_adapter.py index 84cbd9f..4d913e4 100644 --- a/tests/test_adapter.py +++ b/tests/test_adapter.py @@ -1,23 +1,39 @@ -from typing import Any, Dict, Set, Union +from collections import defaultdict +from typing import Any, Dict, List, Optional, Set, Union import pytest -from arango.database import StandardDatabase -from arango.graph import Graph as ArangoGraph -from dgl import DGLGraph -from dgl.heterograph import DGLHeteroGraph -from torch import Tensor +from dgl import DGLGraph, DGLHeteroGraph +from dgl.view import EdgeSpace, NodeSpace +from pandas import DataFrame +from torch import Tensor, cat, long, tensor from adbdgl_adapter import ADBDGL_Adapter -from adbdgl_adapter.typings import ArangoMetagraph +from adbdgl_adapter.encoders import CategoricalEncoder, IdentityEncoder +from adbdgl_adapter.exceptions import ADBMetagraphError, DGLMetagraphError +from adbdgl_adapter.typings import ( + ADBMap, + ADBMetagraph, + ADBMetagraphValues, + DGLCanonicalEType, + DGLMetagraph, + DGLMetagraphValues, +) +from adbdgl_adapter.utils import validate_adb_metagraph, validate_dgl_metagraph from .conftest import ( + Custom_ADBDGL_Controller, adbdgl_adapter, + arango_restore, + con, db, - get_clique_graph, + get_fake_hetero_dataset, get_hypercube_graph, get_karate_graph, - get_lollipop_graph, get_social_graph, + label_tensor_to_2_column_dataframe, + udf_features_df_to_tensor, + udf_key_df_to_tensor, + udf_users_features_tensor_to_df, ) @@ -28,210 +44,866 @@ class Bad_ADBDGL_Controller: pass with pytest.raises(TypeError): - ADBDGL_Adapter(bad_db) + ADBDGL_Adapter(bad_db) # type: ignore with pytest.raises(TypeError): ADBDGL_Adapter(db, Bad_ADBDGL_Controller()) # type: ignore @pytest.mark.parametrize( - "adapter, name, metagraph", - [ + "bad_metagraph", + [ # empty metagraph + ({}), + # missing required parent key + ({"vertexCollections": {}}), + # empty sub-metagraph + ({"vertexCollections": {}, "edgeCollections": {}}), + # bad collection name ( - adbdgl_adapter, - "fraud-detection", { "vertexCollections": { - "account": {"Balance", "rank"}, - "customer": {"rank"}, - "Class": {}, + 1: {}, + # other examples include: + # True: {}, + # ('a'): {} }, - "edgeCollections": { - "transaction": { - "transaction_amt", - "sender_bank_id", - "receiver_bank_id", - }, - "accountHolder": {}, - "Relationship": {}, + "edgeCollections": {}, + } + ), + # bad collection metagraph + ( + { + "vertexCollections": { + "vcol_a": None, + # other examples include: + # "vcol_a": 1, + # "vcol_a": 'foo', }, - }, + "edgeCollections": {}, + } + ), + # bad collection metagraph 2 + ( + { + "vertexCollections": { + "vcol_a": {"a", "b", 3}, + # other examples include: + # "vcol_a": 1, + # "vcol_a": 'foo', + }, + "edgeCollections": {}, + } + ), + # bad meta_key + ( + { + "vertexCollections": { + "vcol_a": { + 1: {}, + # other example include: + # True: {}, + # ("x"): {}, + } + }, + "edgeCollections": {}, + } + ), + # bad meta_val + ( + { + "vertexCollections": { + "vcol_a": { + "x": True, + # other example include: + # 'x': ('a'), + # 'x': ['a'], + # 'x': 5 + } + }, + "edgeCollections": {}, + } + ), + # bad meta_val encoder key + ( + { + "vertexCollections": {"vcol_a": {"x": {1: IdentityEncoder()}}}, + "edgeCollections": {}, + } + ), + # bad meta_val encoder value + ( + { + "vertexCollections": { + "vcol_a": { + "x": { + "Action": True, + # other examples include: + # 'Action': {} + # 'Action': (lambda : 1)() + } + } + }, + "edgeCollections": {}, + } ), ], ) -def test_adb_to_dgl( - adapter: ADBDGL_Adapter, name: str, metagraph: ArangoMetagraph -) -> None: - dgl_g = adapter.arangodb_to_dgl(name, metagraph) - assert_dgl_data(db, dgl_g, metagraph) +def test_validate_adb_metagraph(bad_metagraph: Dict[Any, Any]) -> None: + with pytest.raises(ADBMetagraphError): + validate_adb_metagraph(bad_metagraph) + + +@pytest.mark.parametrize( + "bad_metagraph", + [ + # bad node type + ( + { + "nodeTypes": { + ("a", "b", "c"): {}, + # other examples include: + # 1: {}, + # True: {} + } + } + ), + # bad edge type + ( + { + "edgeTypes": { + "b": {}, + # other examples include: + # 1: {}, + # True: {} + } + } + ), + # bad edge type 2 + ( + { + "edgeTypes": { + ("a", "b", 3): {}, + # other examples include: + # 1: {}, + # True: {} + } + } + ), + # bad data type metagraph + ( + { + "nodeTypes": { + "ntype_a": None, + # other examples include: + # "ntype_a": 1, + # "ntype_a": 'foo', + } + } + ), + # bad data type metagraph 2 + ({"nodeTypes": {"ntype_a": {"a", "b", 3}}}), + # bad meta_val + ( + { + "nodeTypes": { + "ntype_a'": { + "x": True, + # other example include: + # 'x': ('a'), + # 'x': (lambda: 1)(), + } + } + } + ), + # bad meta_val list + ( + { + "nodeTypes": { + "ntype_a'": { + "x": ["a", 3], + # other example include: + # 'x': ('a'), + # 'x': (lambda: 1)(), + } + } + } + ), + ], +) +def test_validate_dgl_metagraph(bad_metagraph: Dict[Any, Any]) -> None: + with pytest.raises(DGLMetagraphError): + validate_dgl_metagraph(bad_metagraph) @pytest.mark.parametrize( - "adapter, name, v_cols, e_cols", + "adapter, name, dgl_g, metagraph, \ + explicit_metagraph, overwrite_graph, batch_size, adb_import_kwargs", [ ( adbdgl_adapter, - "fraud-detection", - {"account", "Class", "customer"}, - {"accountHolder", "Relationship", "transaction"}, - ) + "Karate_1", + get_karate_graph(), + {"nodeTypes": {"Karate_1_N": {"label": "node_label"}}}, + False, + False, + 33, + {}, + ), + ( + adbdgl_adapter, + "Karate_2", + get_karate_graph(), + {"nodeTypes": {"Karate_2_N": {}}}, + True, + False, + 1000, + {}, + ), + ( + adbdgl_adapter, + "Social_1", + get_social_graph(), + { + "nodeTypes": { + "user": { + "features": "user_age", + "label": label_tensor_to_2_column_dataframe, + }, + "game": {"features": ["is_multiplayer", "is_free_to_play"]}, + }, + "edgeTypes": { + ("user", "plays", "game"): { + "features": ["hours_played", "is_satisfied_with_game"] + }, + }, + }, + True, + False, + 1, + {}, + ), + ( + adbdgl_adapter, + "Social_2", + get_social_graph(), + { + "edgeTypes": { + ("user", "plays", "game"): { + "features": ["hours_played", "is_satisfied_with_game"] + }, + }, + }, + True, + False, + 1000, + {}, + ), + ( + adbdgl_adapter, + "Social_3", + get_social_graph(), + {}, + False, + False, + None, + {}, + ), + ( + adbdgl_adapter, + "FakeHeterogeneous_1", + get_fake_hetero_dataset(), + { + "nodeTypes": { + "v0": {"features": "adb_node_features", "label": "adb_node_label"} + }, + "edgeTypes": {("v0", "e0", "v0"): {"features": "adb_edge_features"}}, + }, + True, + False, + None, + {}, + ), + ( + adbdgl_adapter, + "FakeHeterogeneous_2", + get_fake_hetero_dataset(), + {}, + False, + False, + None, + {}, + ), + ( + adbdgl_adapter, + "FakeHeterogeneous_3", + get_fake_hetero_dataset(), + { + "nodeTypes": {"v0": {"features", "label"}}, + "edgeTypes": {("v0", "e0", "v0"): {"features"}}, + }, + True, + True, + None, + {}, + ), ], ) -def test_adb_collections_to_dgl( - adapter: ADBDGL_Adapter, name: str, v_cols: Set[str], e_cols: Set[str] +def test_dgl_to_adb( + adapter: ADBDGL_Adapter, + name: str, + dgl_g: Union[DGLGraph, DGLHeteroGraph], + metagraph: DGLMetagraph, + explicit_metagraph: bool, + overwrite_graph: bool, + batch_size: Optional[int], + adb_import_kwargs: Any, ) -> None: - dgl_g = adapter.arangodb_collections_to_dgl( + db.delete_graph(name, drop_collections=True, ignore_missing=True) + adapter.dgl_to_arangodb( name, - v_cols, - e_cols, - ) - assert_dgl_data( - db, dgl_g, - metagraph={ - "vertexCollections": {col: set() for col in v_cols}, - "edgeCollections": {col: set() for col in e_cols}, - }, + metagraph, + explicit_metagraph, + overwrite_graph, + batch_size, + **adb_import_kwargs ) + assert_dgl_to_adb(name, dgl_g, metagraph, explicit_metagraph) + db.delete_graph(name, drop_collections=True) -@pytest.mark.parametrize( - "adapter, name", - [(adbdgl_adapter, "fraud-detection")], -) -def test_adb_graph_to_dgl(adapter: ADBDGL_Adapter, name: str) -> None: - arango_graph = db.graph(name) - v_cols = arango_graph.vertex_collections() - e_cols = {col["edge_collection"] for col in arango_graph.edge_definitions()} - - dgl_g: DGLGraph = adapter.arangodb_graph_to_dgl(name) - assert_dgl_data( - db, - dgl_g, - metagraph={ - "vertexCollections": {col: set() for col in v_cols}, - "edgeCollections": {col: set() for col in e_cols}, - }, - ) +def test_dgl_to_adb_with_controller() -> None: + name = "Karate_3" + data = get_karate_graph() + db.delete_graph(name, drop_collections=True, ignore_missing=True) + + ADBDGL_Adapter(db, Custom_ADBDGL_Controller()).dgl_to_arangodb(name, data) + + for doc in db.collection(name + "_N"): # type: ignore + assert "foo" in doc + assert doc["foo"] == "bar" + + for edge in db.collection(name + "_E"): # type: ignore + assert "bar" in edge + assert edge["bar"] == "foo" + + db.delete_graph(name, drop_collections=True) @pytest.mark.parametrize( - "adapter, name, dgl_g, overwrite_graph, import_options", + "adapter, name, metagraph, dgl_g_old, batch_size", [ ( adbdgl_adapter, - "Clique", - get_clique_graph(), - False, - {"batch_size": 3, "on_duplicate": "replace"}, + "Karate", + { + "vertexCollections": { + "Karate_N": {"karate_label": "label"}, + }, + "edgeCollections": { + "Karate_E": {}, + }, + }, + get_karate_graph(), + 1, ), - (adbdgl_adapter, "Lollipop", get_lollipop_graph(), False, {"overwrite": True}), ( adbdgl_adapter, - "Hypercube", - get_hypercube_graph(), - False, - {"batch_size": 1000, "on_duplicate": "replace"}, + "Karate_2", + { + "vertexCollections": { + "Karate_2_N": {"karate_label": "label"}, + }, + "edgeCollections": { + "Karate_2_E": {}, + }, + }, + get_karate_graph(), + 33, ), ( adbdgl_adapter, "Hypercube", + { + "vertexCollections": { + "Hypercube_N": {"node_features": "node_features"}, + }, + "edgeCollections": { + "Hypercube_E": {"edge_features": "edge_features"}, + }, + }, get_hypercube_graph(), - False, - {"overwrite": True}, + 1000, ), - (adbdgl_adapter, "Karate", get_karate_graph(), False, {"overwrite": True}), ( adbdgl_adapter, "Social", + { + "vertexCollections": { + "user": {"node_features": "features", "label": "label"}, + "game": {"node_features": "features"}, + "topic": {}, + }, + "edgeCollections": { + "plays": {"edge_features": "features"}, + "follows": {}, + }, + }, get_social_graph(), - True, - {"on_duplicate": "replace"}, + 1, + ), + ( + adbdgl_adapter, + "Heterogeneous", + { + "vertexCollections": { + "v0": {"features": "features", "label": "label"}, + "v1": {"features": "features"}, + "v2": {"features": "features"}, + }, + "edgeCollections": { + "e0": {}, + }, + }, + get_fake_hetero_dataset(), + 1000, + ), + ( + adbdgl_adapter, + "HeterogeneousSimpleMetagraph", + { + "vertexCollections": { + "v0": {"features", "label"}, + "v1": {"features"}, + "v2": {"features"}, + }, + "edgeCollections": { + "e0": {}, + }, + }, + get_fake_hetero_dataset(), + None, + ), + ( + adbdgl_adapter, + "HeterogeneousOverComplicatedMetagraph", + { + "vertexCollections": { + "v0": {"features": {"features": None}, "label": {"label": None}}, + "v1": {"features": "features"}, + "v2": {"features": {"features": None}}, + }, + "edgeCollections": { + "e0": {}, + }, + }, + get_fake_hetero_dataset(), + None, + ), + ( + adbdgl_adapter, + "HeterogeneousUserDefinedFunctions", + { + "vertexCollections": { + "v0": { + "features": (lambda df: tensor(df["features"].to_list())), + "label": (lambda df: tensor(df["label"].to_list())), + }, + "v1": {"features": udf_features_df_to_tensor}, + "v2": {"features": udf_key_df_to_tensor("features")}, + }, + "edgeCollections": { + "e0": {}, + }, + }, + get_fake_hetero_dataset(), + None, ), ], ) -def test_dgl_to_adb( +def test_adb_to_dgl( adapter: ADBDGL_Adapter, name: str, - dgl_g: Union[DGLGraph, DGLHeteroGraph], - overwrite_graph: bool, - import_options: Any, + metagraph: ADBMetagraph, + dgl_g_old: Optional[Union[DGLGraph, DGLHeteroGraph]], + batch_size: Optional[None], +) -> None: + if dgl_g_old: + db.delete_graph(name, drop_collections=True, ignore_missing=True) + adapter.dgl_to_arangodb(name, dgl_g_old) + + dgl_g_new = adapter.arangodb_to_dgl(name, metagraph, batch_size=batch_size) + assert_adb_to_dgl(dgl_g_new, metagraph) + + if dgl_g_old: + db.delete_graph(name, drop_collections=True) + + +def test_adb_partial_to_dgl() -> None: + dgl_g = get_social_graph() + + name = "Social" + db.delete_graph(name, drop_collections=True, ignore_missing=True) + adbdgl_adapter.dgl_to_arangodb(name, dgl_g) + + metagraph: ADBMetagraph + + # Case 1: Partial edge collection import turns the graph homogeneous + metagraph = { + "vertexCollections": { + "user": {"features": "features", "label": "label"}, + }, + "edgeCollections": { + "follows": {}, + }, + } + + dgl_g_new = adbdgl_adapter.arangodb_to_dgl( + "HeterogeneousTurnedHomogeneous", metagraph + ) + + assert dgl_g_new.is_homogeneous + assert ( + dgl_g.ndata["features"]["user"].tolist() == dgl_g_new.ndata["features"].tolist() + ) + assert dgl_g.ndata["label"]["user"].tolist() == dgl_g_new.ndata["label"].tolist() + + # Grab the nodes from the Heterogeneous graph + from_nodes, to_nodes = dgl_g.edges(etype=("user", "follows", "user")) + # Grab the same nodes from the Homogeneous graph + from_nodes_new, to_nodes_new = dgl_g_new.edges(etype=None) + + assert from_nodes.tolist() == from_nodes_new.tolist() + assert to_nodes.tolist() == to_nodes_new.tolist() + + # Case 2: Partial edge collection import keeps the graph heterogeneous + metagraph = { + "vertexCollections": { + "user": {"features": "features", "label": "label"}, + "game": {"features": "features"}, + }, + "edgeCollections": {"follows": {}, "plays": {"features": "features"}}, + } + + dgl_g_new = adbdgl_adapter.arangodb_to_dgl( + "HeterogeneousWithOneLessNodeType", metagraph + ) + + assert type(dgl_g_new) is DGLHeteroGraph + assert set(dgl_g_new.ntypes) == {"user", "game"} + for n_type in dgl_g_new.ntypes: + for k, v in dgl_g_new.nodes[n_type].data.items(): + assert v.tolist() == dgl_g.nodes[n_type].data[k].tolist() + + for e_type in dgl_g_new.canonical_etypes: + for k, v in dgl_g_new.edges[e_type].data.items(): + assert v.tolist() == dgl_g.edges[e_type].data[k].tolist() + + db.delete_graph(name, drop_collections=True) + + +@pytest.mark.parametrize( + "adapter, name, v_cols, e_cols, dgl_g_old", + [ + ( + adbdgl_adapter, + "SocialGraph", + {"user", "game"}, + {"plays", "follows"}, + get_social_graph(), + ) + ], +) +def test_adb_collections_to_dgl( + adapter: ADBDGL_Adapter, + name: str, + v_cols: Set[str], + e_cols: Set[str], + dgl_g_old: Union[DGLGraph, DGLHeteroGraph], ) -> None: - adb_g = adapter.dgl_to_arangodb(name, dgl_g, overwrite_graph, **import_options) - assert_arangodb_data(name, dgl_g, adb_g) + if dgl_g_old: + db.delete_graph(name, drop_collections=True, ignore_missing=True) + adapter.dgl_to_arangodb(name, dgl_g_old) + + dgl_g_new = adapter.arangodb_collections_to_dgl( + name, + v_cols, + e_cols, + ) + + assert_adb_to_dgl( + dgl_g_new, + metagraph={ + "vertexCollections": {col: {} for col in v_cols}, + "edgeCollections": {col: {} for col in e_cols}, + }, + ) + + if dgl_g_old: + db.delete_graph(name, drop_collections=True) + + +@pytest.mark.parametrize( + "adapter, name, dgl_g_old", + [ + (adbdgl_adapter, "Heterogeneous", get_fake_hetero_dataset()), + ], +) +def test_adb_graph_to_dgl( + adapter: ADBDGL_Adapter, name: str, dgl_g_old: Union[DGLGraph, DGLHeteroGraph] +) -> None: + if dgl_g_old: + db.delete_graph(name, drop_collections=True, ignore_missing=True) + adapter.dgl_to_arangodb(name, dgl_g_old) + + dgl_g_new = adapter.arangodb_graph_to_dgl(name) + + graph = db.graph(name) + v_cols: Set[str] = graph.vertex_collections() # type: ignore + edge_definitions: List[Dict[str, Any]] = graph.edge_definitions() # type: ignore + e_cols: Set[str] = {c["edge_collection"] for c in edge_definitions} + + assert_adb_to_dgl( + dgl_g_new, + metagraph={ + "vertexCollections": {col: {} for col in v_cols}, + "edgeCollections": {col: {} for col in e_cols}, + }, + ) + + if dgl_g_old: + db.delete_graph(name, drop_collections=True) -def assert_dgl_data( - db: StandardDatabase, dgl_g: DGLGraph, metagraph: ArangoMetagraph +def test_full_cycle_imdb() -> None: + name = "imdb" + db.delete_graph(name, drop_collections=True, ignore_missing=True) + arango_restore(con, "tests/data/adb/imdb_dump") + db.create_graph( + name, + edge_definitions=[ + { + "edge_collection": "Ratings", + "from_vertex_collections": ["Users"], + "to_vertex_collections": ["Movies"], + }, + ], + ) + + adb_to_dgl_metagraph: ADBMetagraph = { + "vertexCollections": { + "Movies": { + "label": "Comedy", + "features": { + "Action": IdentityEncoder(dtype=long), + "Drama": IdentityEncoder(dtype=long), + # etc.... + }, + }, + "Users": { + "features": { + "Age": IdentityEncoder(dtype=long), + "Gender": CategoricalEncoder(), + } + }, + }, + "edgeCollections": {"Ratings": {"weight": "Rating"}}, + } + + dgl_g = adbdgl_adapter.arangodb_to_dgl(name, adb_to_dgl_metagraph) + assert_adb_to_dgl(dgl_g, adb_to_dgl_metagraph) + + dgl_to_adb_metagraph: DGLMetagraph = { + "nodeTypes": { + "Movies": { + "label": "comedy", + "features": ["action", "drama"], + }, + "Users": {"features": udf_users_features_tensor_to_df}, + }, + "edgeTypes": {("Users", "Ratings", "Movies"): {"weight": "rating"}}, + } + adbdgl_adapter.dgl_to_arangodb(name, dgl_g, dgl_to_adb_metagraph, overwrite=True) + assert_dgl_to_adb(name, dgl_g, dgl_to_adb_metagraph) + + db.delete_graph(name, drop_collections=True) + + +def assert_adb_to_dgl( + dgl_g: Union[DGLGraph, DGLHeteroGraph], metagraph: ADBMetagraph ) -> None: - has_one_ntype = len(metagraph["vertexCollections"]) == 1 - has_one_etype = len(metagraph["edgeCollections"]) == 1 - - for col, atribs in metagraph["vertexCollections"].items(): - num_nodes = dgl_g.num_nodes(col) - assert num_nodes == db.collection(col).count() - - for atrib in atribs: - assert atrib in dgl_g.ndata - if has_one_ntype: - assert len(dgl_g.ndata[atrib]) == num_nodes - else: - assert col in dgl_g.ndata[atrib] - assert len(dgl_g.ndata[atrib][col]) == num_nodes - - for col, atribs in metagraph["edgeCollections"].items(): - num_edges = dgl_g.num_edges(col) - assert num_edges == db.collection(col).count() - - canon_etype = dgl_g.to_canonical_etype(col) - for atrib in atribs: - assert atrib in dgl_g.edata - if has_one_etype: - assert len(dgl_g.edata[atrib]) == num_edges - else: - assert canon_etype in dgl_g.edata[atrib] - assert len(dgl_g.edata[atrib][canon_etype]) == num_edges - - -def assert_arangodb_data( + has_one_ntype = len(dgl_g.ntypes) == 1 + has_one_etype = len(dgl_g.canonical_etypes) == 1 + + # Maps ArangoDB Vertex _keys to DGL Node ids + adb_map: ADBMap = defaultdict(dict) + + for v_col, meta in metagraph["vertexCollections"].items(): + n_key = None if has_one_ntype else v_col + collection = db.collection(v_col) + assert collection.count() == dgl_g.num_nodes(n_key) + + df = DataFrame(collection.all()) + adb_map[v_col] = {adb_id: dgl_id for dgl_id, adb_id in enumerate(df["_key"])} + + assert_adb_to_dgl_meta(meta, df, dgl_g.nodes[n_key].data) + + et_df: DataFrame + v_cols: List[str] = list(metagraph["vertexCollections"].keys()) + for e_col, meta in metagraph["edgeCollections"].items(): + collection = db.collection(e_col) + assert collection.count() <= dgl_g.num_edges(None) + + df = DataFrame(collection.all()) + df[["from_col", "from_key"]] = df["_from"].str.split(pat="/", n=1, expand=True) + df[["to_col", "to_key"]] = df["_to"].str.split(pat="/", n=1, expand=True) + + for (from_col, to_col), count in ( + df[["from_col", "to_col"]].value_counts().items() + ): + edge_type = (from_col, e_col, to_col) + if from_col not in v_cols or to_col not in v_cols: + continue + + e_key = None if has_one_etype else edge_type + assert count == dgl_g.num_edges(e_key) + + et_df = df[(df["from_col"] == from_col) & (df["to_col"] == to_col)] + from_nodes = et_df["from_key"].map(adb_map[from_col]).tolist() + to_nodes = et_df["to_key"].map(adb_map[to_col]).tolist() + + assert from_nodes == dgl_g.edges(etype=e_key)[0].tolist() + assert to_nodes == dgl_g.edges(etype=e_key)[1].tolist() + + assert_adb_to_dgl_meta(meta, et_df, dgl_g.edges[e_key].data) + + +def assert_adb_to_dgl_meta( + meta: Union[Set[str], Dict[str, ADBMetagraphValues]], + df: DataFrame, + dgl_data: Union[NodeSpace, EdgeSpace], +) -> None: + valid_meta: Dict[str, ADBMetagraphValues] + valid_meta = meta if type(meta) is dict else {m: m for m in meta} + + for k, v in valid_meta.items(): + assert k in dgl_data + assert type(dgl_data[k]) is Tensor + + t = dgl_data[k].tolist() + if type(v) is str: + data = df[v].tolist() + assert len(data) == len(t) + assert data == t + + if type(v) is dict: + data = [] + for attr, encoder in v.items(): + if encoder is None: + data.append(tensor(df[attr].to_list())) + if callable(encoder): + data.append(encoder(df[attr])) + + cat_data = cat(data, dim=-1).tolist() + assert len(cat_data) == len(t) + assert cat_data == t + + if callable(v): + data = v(df).tolist() + assert len(data) == len(t) + assert data == t + + +def assert_dgl_to_adb( name: str, dgl_g: Union[DGLGraph, DGLHeteroGraph], - adb_g: ArangoGraph, + metagraph: DGLMetagraph, + explicit_metagraph: bool = False, ) -> None: - is_default_type = dgl_g.canonical_etypes == adbdgl_adapter.DEFAULT_CANONICAL_ETYPE - - node: Tensor - for ntype in dgl_g.ntypes: - adb_v_col = f"{name}_N" if is_default_type else ntype - attributes = dgl_g.node_attr_schemes(ntype).keys() - col = adb_g.vertex_collection(adb_v_col) - - for node in dgl_g.nodes(ntype): - vertex = col.get(str(node.item())) - assert vertex - for atrib in attributes: - assert atrib in vertex - - from_node: Tensor - to_node: Tensor - for c_etype in dgl_g.canonical_etypes: - dgl_from_col, dgl_e_col, dgl_to_col = c_etype - attributes = dgl_g.edge_attr_schemes(c_etype).keys() - - adb_e_col = f"{name}_E" if is_default_type else dgl_e_col - adb_from_col = f"{name}_N" if is_default_type else dgl_from_col - adb_to_col = f"{name}_N" if is_default_type else dgl_to_col - - col = adb_g.edge_collection(adb_e_col) - - from_nodes, to_nodes = dgl_g.edges(etype=c_etype) - for from_node, to_node in zip(from_nodes, to_nodes): - edge = col.find( - { - "_from": f"{adb_from_col}/{str(from_node.item())}", - "_to": f"{adb_to_col}/{str(to_node.item())}", - } - ).next() - assert edge - for atrib in attributes: - assert atrib in edge + has_one_ntype = len(dgl_g.ntypes) == 1 + has_one_etype = len(dgl_g.canonical_etypes) == 1 + has_default_canonical_etypes = dgl_g.canonical_etypes == [("_N", "_E", "_N")] + + node_types: List[str] + edge_types: List[DGLCanonicalEType] + explicit_metagraph = metagraph != {} and explicit_metagraph + if explicit_metagraph: + node_types = metagraph.get("nodeTypes", {}).keys() # type: ignore + edge_types = metagraph.get("edgeTypes", {}).keys() # type: ignore + + elif has_default_canonical_etypes: + n_type = name + "_N" + node_types = [n_type] + edge_types = [(n_type, name + "_E", n_type)] + + else: + node_types = dgl_g.ntypes + edge_types = dgl_g.canonical_etypes + + n_meta = metagraph.get("nodeTypes", {}) + for n_type in node_types: + n_key = None if has_one_ntype else n_type + collection = db.collection(n_type) + assert collection.count() == dgl_g.num_nodes(n_key) + + df = DataFrame(collection.all()) + meta = n_meta.get(n_type, {}) + assert_dgl_to_adb_meta(df, meta, dgl_g.nodes[n_key].data, explicit_metagraph) + + e_meta = metagraph.get("edgeTypes", {}) + for e_type in edge_types: + e_key = None if has_one_etype else e_type + from_col, e_col, to_col = e_type + collection = db.collection(e_col) + + df = DataFrame(collection.all()) + df[["from_col", "from_key"]] = df["_from"].str.split(pat="/", n=1, expand=True) + df[["to_col", "to_key"]] = df["_to"].str.split(pat="/", n=1, expand=True) + + et_df = df[(df["from_col"] == from_col) & (df["to_col"] == to_col)] + assert len(et_df) == dgl_g.num_edges(e_key) + + from_nodes = dgl_g.edges(etype=e_key)[0].tolist() + to_nodes = dgl_g.edges(etype=e_key)[1].tolist() + + assert from_nodes == et_df["from_key"].astype(int).tolist() + assert to_nodes == et_df["to_key"].astype(int).tolist() + + meta = e_meta.get(e_type, {}) + assert_dgl_to_adb_meta(et_df, meta, dgl_g.edges[e_key].data, explicit_metagraph) + + +def assert_dgl_to_adb_meta( + df: DataFrame, + meta: Union[Set[str], Dict[Any, DGLMetagraphValues]], + dgl_data: Union[NodeSpace, EdgeSpace], + explicit_metagraph: bool, +) -> None: + valid_meta: Dict[Any, DGLMetagraphValues] + valid_meta = meta if type(meta) is dict else {m: m for m in meta} + + if explicit_metagraph: + dgl_keys = set(valid_meta.keys()) + else: + dgl_keys = dgl_data.keys() + + for k in dgl_keys: + data = dgl_data[k] + meta_val = valid_meta.get(k, str(k)) + + assert len(data) == len(df) + + if type(data) is Tensor: + if type(meta_val) is str: + assert meta_val in df + assert df[meta_val].tolist() == data.tolist() + + if type(meta_val) is list: + assert all([e in df for e in meta_val]) + assert df[meta_val].values.tolist() == data.tolist() + + if callable(meta_val): + udf_df = meta_val(data, DataFrame(index=range(len(data)))) + assert all([column in df for column in udf_df.columns]) + for column in udf_df.columns: + assert df[column].tolist() == udf_df[column].tolist() diff --git a/tests/assets/arangorestore b/tests/tools/arangorestore similarity index 100% rename from tests/assets/arangorestore rename to tests/tools/arangorestore