diff --git a/.environment.yml b/.environment.yml index 9897be9400..b8cf75c44e 100644 --- a/.environment.yml +++ b/.environment.yml @@ -4,69 +4,113 @@ channels: - conda-forge - defaults dependencies: - - ca-certificates=2021.10.8 - - certifi=2021.10.8 + - ca-certificates>=2021.10.8 + - certifi>=2021.10.8 - ipykernel - - libcxx=12.0.0 + - libcxx>=12.0.0 - libffi>=3.3 - - ncurses=6.3 + - ncurses>=6.4 - make=4.3 - - openssl=1.1.1q - - pip=21.2.4 - - python=3.9.13 - - python_abi=3.9 - - readline=8.1.2 - - libzlib=1.2.13 + - openssl>=1.1.1q + - pip>=21.2.4 + - python=3.10.12 + - python_abi=3.10 + - readline>=8.1.2 + - libzlib>=1.2.13 - rust=1.67.1 - setuptools=61.2.0 - sqlite>=3.38.3 - tk>=8.6.11 - - tzdata=2022a - - wheel=0.37.1 - - xz=5.2.5 + - tzdata>=2022a + - wheel>=0.37.1 + - xz>=5.2.5 - zlib>=1.2.12 - pip: - - alabaster==0.7.12 - - babel==2.10.1 - - charset-normalizer==2.0.12 - - docutils==0.17.1 - - idna==3.3 - - imagesize==1.3.0 - - importlib-metadata==4.11.3 - - jupyter==1.0.0 - - markdown-it-py==2.1.0 - - markupsafe==2.1.1 + - accessible-pygments==0.0.4 + - alabaster==0.7.13 + - appnope==0.1.3 + - asttokens==2.2.1 + - attrs==23.1.0 + - autodocsumm==0.2.11 + - Babel==2.12.1 + - backcall==0.2.0 + - beautifulsoup4==4.12.2 + - bleach==6.0.0 + - certifi==2023.5.7 + - charset-normalizer==3.1.0 + - contourpy==1.1.0 + - cycler==0.11.0 + - decorator==5.1.1 + - defusedxml==0.7.1 + - docutils==0.19 + - executing==1.2.0 + - fastjsonschema==2.17.1 + - fonttools==4.40.0 + - idna==3.4 + - imagesize==1.4.1 + - ipython==8.14.0 + - jedi==0.18.2 + - Jinja2==3.1.2 + - jsonpickle==3.0.1 + - jsonschema==4.17.3 + - jupyter_client==8.3.0 + - jupyter_core==5.3.1 + - jupyterlab-pygments==0.2.2 + - kiwisolver==1.4.4 + - MarkupSafe==2.1.3 - matplotlib==3.7.1 - - maturin==0.14.17 - - mdit-py-plugins==0.3.0 - - mdurl==0.1.1 - - myst-parser==0.17.2 + - matplotlib-inline==0.1.6 + - maturin==1.1.0 + - mistune==3.0.1 + - nbclient==0.8.0 + - nbconvert==7.6.0 + - nbformat==5.9.0 + - nbsphinx==0.9.2 + - networkx==3.1 + - numpy==1.25.0 - numpydoc==1.5.0 - - packaging==21.3 - - pandas==2.0.2 - - patchelf==0.17.2.1 - - poetry==1.2.0 - - pygments==2.12.0 - - pyparsing==3.0.8 - - pytz==2022.1 - - pyyaml==6.0 + - packaging==23.1 + - pandas==2.0.3 + - pandocfilters==1.5.0 + - parso==0.8.3 + - pexpect==4.8.0 + - pickleshare==0.7.5 + - Pillow==10.0.0 + - platformdirs==3.8.0 + - prompt-toolkit==3.0.39 + - ptyprocess==0.7.0 + - pure-eval==0.2.2 - pydata-sphinx-theme==0.13.3 + - Pygments==2.15.1 + - pyparsing==3.1.0 + - pyrsistent==0.19.3 + - python-dateutil==2.8.2 + - pytz==2023.3 - pyvis==0.3.2 - - requests==2.27.1 + - pyzmq==25.1.0 + - requests==2.31.0 + - six==1.16.0 - snowballstemmer==2.2.0 - - nbsphinx>=0.8.7 - - sphinx==4.5.0 - - sphinx-rtd-theme==1.0.0 - - sphinxcontrib-applehelp==1.0.2 - - sphinx_copybutton==0.5.2 + - soupsieve==2.4.1 + - Sphinx==6.2.1 + - sphinx-copybutton==0.5.2 + - sphinx-toggleprompt==0.4.0 - sphinx_design==0.4.1 + - sphinxcontrib-applehelp==1.0.4 - sphinxcontrib-devhelp==1.0.2 - - sphinxcontrib-htmlhelp==2.0.0 - - sphinx_toggleprompt==0.4.0 - - sphinx-tabs==3.4.0 + - sphinxcontrib-htmlhelp==2.0.1 - sphinxcontrib-jsmath==1.0.1 - sphinxcontrib-qthelp==1.0.3 - sphinxcontrib-serializinghtml==1.1.5 - - typing-extensions==4.2.0 - - urllib3==1.26.9 - - zipp==3.8.0 + - stack-data==0.6.2 + - tinycss2==1.2.1 + - tomli==2.0.1 + - tornado==6.3.2 + - traitlets==5.9.0 + - typing_extensions==4.7.1 + - tzdata==2023.3 + - urllib3==2.0.3 + - wcwidth==0.2.6 + - webencodings==0.5.1 + + diff --git a/.fleet/settings.json b/.fleet/settings.json new file mode 100644 index 0000000000..a7858d188f --- /dev/null +++ b/.fleet/settings.json @@ -0,0 +1,3 @@ +{ + "editor.guides": [] +} \ No newline at end of file diff --git a/.github/workflows/_release_python.yml b/.github/workflows/_release_python.yml index 0b2154a9a0..0fbc291f82 100644 --- a/.github/workflows/_release_python.yml +++ b/.github/workflows/_release_python.yml @@ -18,8 +18,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - target: [x86_64, aarch64, armv7] - python: ['3.9', '3.10'] + target: [x86_64, aarch64] steps: - name: "Check if user has write access" uses: "lannonbr/repo-permission-check-action@2.0.0" @@ -43,16 +42,18 @@ jobs: ref: ${{ inputs.base }} - uses: actions/setup-python@v4 with: - python-version: ${{ matrix.python }} - - name: Setup QEMU - uses: docker/setup-qemu-action@v1 + python-version: | + 3.8 + 3.9 + 3.10 + 3.11 - name: Build wheels uses: PyO3/maturin-action@v1 with: working-directory: ./python command: build target: ${{ matrix.target }} - args: --release --out dist --find-interpreter + args: --release --out dist -i python3.7 -i python3.8 -i python3.9 -i python3.10 -i python3.11 manylinux: 2014 before-script-linux: | if [[ -f /etc/os-release ]]; then @@ -101,7 +102,6 @@ jobs: strategy: matrix: target: [x64] - python: ['3.9', '3.10'] steps: - name: "Check if user has write access" uses: "lannonbr/repo-permission-check-action@2.0.0" @@ -114,7 +114,11 @@ jobs: ref: ${{ inputs.base }} - uses: actions/setup-python@v4 with: - python-version: ${{ matrix.python }} + python-version: | + 3.8 + 3.9 + 3.10 + 3.11 architecture: ${{ matrix.target }} - name: Build wheels uses: PyO3/maturin-action@v1 @@ -122,7 +126,7 @@ jobs: working-directory: ./python command: build target: ${{ matrix.target }} - args: --release --out dist --find-interpreter + args: --release --out dist -i python3.7 -i python3.8 -i python3.9 -i python3.10 -i python3.11 - name: Upload wheels to gh artifact uses: actions/upload-artifact@v3 with: @@ -133,7 +137,6 @@ jobs: strategy: matrix: target: [x86_64, aarch64] - python: ['3.9', '3.10'] steps: - name: "Check if user has write access" uses: "lannonbr/repo-permission-check-action@2.0.0" @@ -157,7 +160,11 @@ jobs: ref: ${{ inputs.base }} - uses: actions/setup-python@v4 with: - python-version: ${{ matrix.python }} + python-version: | + 3.8 + 3.9 + 3.10 + 3.11 - name: Build wheels uses: PyO3/maturin-action@v1 with: diff --git a/.github/workflows/_release_rust.yml b/.github/workflows/_release_rust.yml index 925fb09d54..48d1fd59fa 100644 --- a/.github/workflows/_release_rust.yml +++ b/.github/workflows/_release_rust.yml @@ -61,27 +61,9 @@ jobs: with: command: publish args: --token ${{ secrets.CRATES_TOKEN }} --package raphtory --allow-dirty - - name: "Publish raphtory-io to crates.io" - if: ${{ !inputs.dry_run }} - uses: actions-rs/cargo@v1 - with: - command: publish - args: --token ${{ secrets.CRATES_TOKEN }} --package raphtory-io --allow-dirty - name: "Publish raphtory-graphql to crates.io" if: ${{ !inputs.dry_run }} uses: actions-rs/cargo@v1 with: command: publish args: --token ${{ secrets.CRATES_TOKEN }} --package raphtory-graphql --allow-dirty - - name: "Publish py-raphtory to crates.io" - if: ${{ !inputs.dry_run }} - uses: actions-rs/cargo@v1 - with: - command: publish - args: --token ${{ secrets.CRATES_TOKEN }} --package py-raphtory --allow-dirty - - name: "Publish raphtory-pymodule to crates.io" - if: ${{ !inputs.dry_run }} - uses: actions-rs/cargo@v1 - with: - command: publish - args: --token ${{ secrets.CRATES_TOKEN }} --package raphtory-pymodule --allow-dirty \ No newline at end of file diff --git a/.github/workflows/binder_auto_build.yml b/.github/workflows/binder_auto_build.yml index 1b5d2f8ddb..c91bffcdd3 100644 --- a/.github/workflows/binder_auto_build.yml +++ b/.github/workflows/binder_auto_build.yml @@ -11,6 +11,6 @@ jobs: steps: - uses: s-weigand/trigger-mybinder-build@v1 with: - target-repo: raphtory/raphtory + target-repo: pometry/raphtory service-name: gh use-default-build-servers: true diff --git a/.github/workflows/code_coverage.yml b/.github/workflows/code_coverage.yml index 59195e9bca..e41a7e8d65 100644 --- a/.github/workflows/code_coverage.yml +++ b/.github/workflows/code_coverage.yml @@ -51,7 +51,7 @@ jobs: with: command: clean - name: Run tests (rust) - run: cargo test -p raphtory + run: cargo test -p raphtory --features "io python" env: CARGO_INCREMENTAL: '0' RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off -Cpanic=abort -Zpanic_abort_tests' @@ -66,7 +66,7 @@ jobs: name: Run rust tests (rust-grcov) and collect coverage uses: actions-rs/grcov@v0.1.5 - name: Run python tests and collect coverage - run: pytest --cov=./ --cov-report=xml + run: cd python/tests && pytest --cov=./ --cov-report=xml - name: Codecov uses: codecov/codecov-action@v3.1.1 with: diff --git a/.github/workflows/rust_format_check.yml b/.github/workflows/rust_format_check.yml new file mode 100644 index 0000000000..6bf9353ffc --- /dev/null +++ b/.github/workflows/rust_format_check.yml @@ -0,0 +1,31 @@ +# this workflow checks out the code, and installs nightly build of cargo and runs a cargo +nightly fmt --all -- --check and fails workflow if any code is not formatted +# + +name: Rust format check +on: + workflow_call: + inputs: + fail_if_not_formatted: + type: boolean + default: true + required: false + description: "Fail the workflow if any code is not formatted" + +jobs: + rust-format-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Setup Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + override: true + components: rustfmt + - name: Run rust format check + run: | + if [ ${{ inputs.fail_if_not_formatted }} == true ]; then + cargo +nightly fmt --all -- --check + else + cargo +nightly fmt --all -- --check || true + fi \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9432888ca0..a7417ee399 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,9 +3,6 @@ on: push: branches: - master - pull_request: - branches: - - master concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} @@ -13,6 +10,9 @@ concurrency: jobs: + rust-format-check: + name: Rust format check + uses: ./.github/workflows/rust_format_check.yml call-test-rust-workflow-in-local-repo: name: Run Rust tests uses: ./.github/workflows/test_rust_workflow.yml @@ -20,6 +20,8 @@ jobs: call-test-python-workflow-in-local-repo: name: Run Python tests uses: ./.github/workflows/test_python_workflow.yml + with: + test_python_lower: true secrets: inherit call-benchmark-workflow-in-local-repo: name: Run benchmarks diff --git a/.github/workflows/test_during_pr.yml b/.github/workflows/test_during_pr.yml new file mode 100644 index 0000000000..2169067a6f --- /dev/null +++ b/.github/workflows/test_during_pr.yml @@ -0,0 +1,37 @@ +name: Run tests during PR +on: + pull_request: + branches: + - master + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + + +jobs: + rust-format-check: + name: Rust format check + uses: ./.github/workflows/rust_format_check.yml + call-test-rust-workflow-in-local-repo: + name: Run Rust tests + uses: ./.github/workflows/test_rust_workflow.yml + secrets: inherit + needs: rust-format-check + call-test-python-workflow-in-local-repo: + name: Run Python tests + uses: ./.github/workflows/test_python_workflow.yml + with: + test_python_lower: false + secrets: inherit + needs: rust-format-check + call-benchmark-workflow-in-local-repo: + name: Run benchmarks + uses: ./.github/workflows/benchmark.yml + secrets: inherit + needs: rust-format-check + call-code-coverage: + name: Code Coverage + uses: ./.github/workflows/code_coverage.yml + needs: rust-format-check + diff --git a/.github/workflows/test_python_workflow.yml b/.github/workflows/test_python_workflow.yml index b4c95656ff..faa50cd0f4 100644 --- a/.github/workflows/test_python_workflow.yml +++ b/.github/workflows/test_python_workflow.yml @@ -6,14 +6,31 @@ on: type: boolean default: false required: false + test_python_lower: + type: boolean + default: false + required: false # DO NOT CHANGE NAME OF WORKFLOW, USED IN OTHER WORKFLOWS KEEP "Rust Tests" jobs: + select-strategy: + runs-on: ubuntu-latest + outputs: + python-versions: ${{ steps.set-matrix.outputs.python-versions }} + steps: + - id: set-matrix + run: | + if [ ${{ inputs.test_python_lower }} == true ]; then + echo "python-versions=[\"3.8\",\"3.11\"]" >> $GITHUB_OUTPUT + else + echo "python-versions=[\"3.8\"]" >> $GITHUB_OUTPUT + fi python-test: if: ${{ !inputs.skip_tests }} name: Python Tests + needs: select-strategy strategy: matrix: - python: ['3.9', '3.10'] + python: ${{ fromJson(needs.select-strategy.outputs.python-versions) }} os: [macos-latest, ubuntu-latest, windows-latest] runs-on: '${{ matrix.os }}' steps: @@ -55,6 +72,7 @@ jobs: run: | python -m pip install -q pytest networkx numpy seaborn pandas nbmake pytest-xdist matplotlib pyvis python -m pip install target/wheels/raphtory-*.whl + python -m pip install -e examples/custom_python_extension - name: Install Python dependencies (Windows) if: "contains(matrix.os, 'Windows')" run: | @@ -63,6 +81,10 @@ jobs: Get-ChildItem -Path $folder_path -Recurse -Include *.whl | ForEach-Object { python -m pip install "$($_.FullName)" } + python -m pip install -e examples/custom_python_extension - name: Run Python tests run: | cd python/tests && pytest --nbmake --nbmake-timeout=1200 . + - name: Run Python extension tests + run: | + cd examples/custom_python_extension/test && pytest . diff --git a/.github/workflows/test_rust_workflow.yml b/.github/workflows/test_rust_workflow.yml index 928b8ef762..e13ee211e5 100644 --- a/.github/workflows/test_rust_workflow.yml +++ b/.github/workflows/test_rust_workflow.yml @@ -30,8 +30,6 @@ jobs: ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ - target/debug - target/release key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} restore-keys: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - uses: actions-rs/toolchain@v1 @@ -47,7 +45,7 @@ jobs: RUSTFLAGS: -Awarnings with: command: test - args: --all --no-default-features + args: --all --no-default-features --features "raphtory/io raphtory/python" doc-test: if: ${{ !inputs.skip_tests }} name: "Doc tests" @@ -68,8 +66,6 @@ jobs: ~/.cargo/registry/index/ ~/.cargo/registry/cache/ ~/.cargo/git/db/ - target/debug - target/release key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} restore-keys: ${{ runner.os }}-cargo- - uses: actions-rs/toolchain@v1 diff --git a/.gitignore b/.gitignore index 3838651bdf..82b18e8c99 100644 --- a/.gitignore +++ b/.gitignore @@ -5,9 +5,11 @@ massif.* *.svg # this is for raphtory **/.env/ -**/data/* +comparison-benchmark/python/data/* .vscode docs/source/_rustdoc/* +docs/nx.html +docs/graph.html docs/build/* .DS_Store docs/logs/* @@ -22,3 +24,12 @@ examples/py/enron/emails.csv /docs/lib/ /docs/build/ docs/source/ +.env + +# Byte-compiled / optimized / DLL files +__pycache__/ +.pytest_cache/ +*.py[cod] + +# C extensions +*.so \ No newline at end of file diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000000..4c54ff0d8f --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,2 @@ +#will enable when it is stable +imports_granularity = "Crate" \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 3d6745c3d9..ddd7631781 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,15 +18,44 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aes" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" +dependencies = [ + "cfg-if 1.0.0", + "cipher", + "cpufeatures", +] + +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if 1.0.0", + "getrandom 0.2.10", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04" +checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -42,17 +71,101 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" +[[package]] +name = "anstream" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" + +[[package]] +name = "anstyle-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys 0.48.0", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +dependencies = [ + "anstyle", + "windows-sys 0.48.0", +] + +[[package]] +name = "arc-swap" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" + +[[package]] +name = "arrow2" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15ae0428d69ab31d7b2adad22a752d6f11fef2e901d2262d0cad4f5cb08b7093" +dependencies = [ + "ahash", + "bytemuck", + "chrono", + "dyn-clone", + "either", + "ethnum", + "foreign_vec", + "getrandom 0.2.10", + "hash_hasher", + "num-traits", + "rustc_version", + "simdutf8", +] + [[package]] name = "ascii_utils" version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71938f30533e4d95a6d17aa530939da3842c2ab6f4f84b9dae68447e4129f74a" +[[package]] +name = "async-convert" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d416feee97712e43152cd42874de162b8f9b77295b1c85e5d92725cc8310bae" +dependencies = [ + "async-trait", +] + [[package]] name = "async-graphql" -version = "5.0.8" +version = "5.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ae09afb01514b3dbd6328547b2b11fcbcb0205d9c5e6f2e17e60cb166a82d7f" +checksum = "b35ef8f9be23ee30fe1eb1cf175c689bc33517c6c6d0fd0669dade611e5ced7f" dependencies = [ "async-graphql-derive", "async-graphql-parser", @@ -66,7 +179,7 @@ dependencies = [ "futures-util", "handlebars", "http", - "indexmap", + "indexmap 1.9.3", "mime", "multer", "num-traits", @@ -83,13 +196,13 @@ dependencies = [ [[package]] name = "async-graphql-derive" -version = "5.0.8" +version = "5.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60ae62851dd3ff9a7550aee75e848e8834b75285b458753e98dd71d0733ad3f2" +checksum = "1a0f6ceed3640b4825424da70a5107e79d48d9b2bc6318dfc666b2fc4777f8c4" dependencies = [ "Inflector", "async-graphql-parser", - "darling", + "darling 0.14.4", "proc-macro-crate", "proc-macro2", "quote", @@ -99,9 +212,9 @@ dependencies = [ [[package]] name = "async-graphql-parser" -version = "5.0.8" +version = "5.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e6ee332acd99d2c50c3443beae46e9ed784c205eead9a668b7b5118b4a60a8b" +checksum = "ecc308cd3bc611ee86c9cf19182d2b5ee583da40761970e41207f088be3db18f" dependencies = [ "async-graphql-value", "pest", @@ -111,9 +224,9 @@ dependencies = [ [[package]] name = "async-graphql-poem" -version = "5.0.8" +version = "5.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c714cf530b5b4d5dec0d177be059a76bcd6823824564f74254ce24a9fccee73b" +checksum = "68f818938d4e47dcc40bc383e9ddec373e9aab1db29e5ad9706b29621afe3b3f" dependencies = [ "async-graphql", "futures-util", @@ -124,14 +237,37 @@ dependencies = [ [[package]] name = "async-graphql-value" -version = "5.0.8" +version = "5.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "122da50452383410545b9428b579f4cda5616feb6aa0aff0003500c53fcff7b7" +checksum = "d461325bfb04058070712296601dfe5e5bd6cdff84780a0a8c569ffb15c87eb3" dependencies = [ "bytes", - "indexmap", + "indexmap 1.9.3", + "serde", + "serde_json", +] + +[[package]] +name = "async-openai" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7614373e1d24d44f7b57df2125c5253c7afbf927574f2695371e5589a0dd4937" +dependencies = [ + "async-convert", + "backoff", + "base64 0.21.2", + "derive_builder", + "futures", + "rand 0.8.5", + "reqwest", + "reqwest-eventsource", "serde", "serde_json", + "thiserror", + "tokio", + "tokio-stream", + "tokio-util", + "tracing", ] [[package]] @@ -153,18 +289,18 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.28", ] [[package]] name = "async-trait" -version = "0.1.68" +version = "0.1.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842" +checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.28", ] [[package]] @@ -184,6 +320,20 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backoff" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" +dependencies = [ + "futures-core", + "getrandom 0.2.10", + "instant", + "pin-project-lite", + "rand 0.8.5", + "tokio", +] + [[package]] name = "base64" version = "0.13.1" @@ -192,9 +342,15 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.0" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" +checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" [[package]] name = "bincode" @@ -211,6 +367,21 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" + +[[package]] +name = "bitpacking" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c7d2ac73c167c06af4a5f37e6e59d84148d57ccbe4480b76f0273eefea82d7" +dependencies = [ + "crunchy", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -222,15 +393,29 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.12.1" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b1ce199063694f33ffb7dd4e0ee620741495c32833cde5aa08f02a0bf96f0c8" +checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" [[package]] name = "bytemuck" version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17febce684fd15d89027105661fec94afb475cb995fbc59d2865198446ba2eea" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdde5c9cd29ebd706ce1b35600920a33550e402fc998a2e53ad3b42c3c47a192" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.28", +] [[package]] name = "byteorder" @@ -279,6 +464,15 @@ name = "cc" version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +dependencies = [ + "jobserver", +] + +[[package]] +name = "census" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fafee10a5dd1cffcb5cc560e0d0df8803d7355a2b12272e3557dee57314cb6e" [[package]] name = "cfg-if" @@ -294,25 +488,25 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.24" +version = "0.4.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" +checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" dependencies = [ + "android-tzdata", "iana-time-zone", "js-sys", - "num-integer", "num-traits", "serde", - "time", + "time 0.1.45", "wasm-bindgen", "winapi", ] [[package]] name = "ciborium" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c137568cc60b904a7724001b35ce2630fd00d5d84805fbb608ab89509d788f" +checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926" dependencies = [ "ciborium-io", "ciborium-ll", @@ -321,32 +515,77 @@ dependencies = [ [[package]] name = "ciborium-io" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346de753af073cc87b52b2083a506b38ac176a44cfb05497b622e27be899b369" +checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656" [[package]] name = "ciborium-ll" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213030a2b5a4e0c0892b6652260cf6ccac84827b83a85a534e178e3906c4cf1b" +checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b" dependencies = [ "ciborium-io", "half", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" -version = "3.2.23" +version = "3.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" dependencies = [ - "bitflags", - "clap_lex", - "indexmap", + "bitflags 1.3.2", + "clap_lex 0.2.4", + "indexmap 1.9.3", "textwrap", ] +[[package]] +name = "clap" +version = "4.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1640e5cc7fb47dbb8338fd471b105e7ed6c3cb2aeb00c2e067127ffd3764a05d" +dependencies = [ + "clap_builder", + "clap_derive", + "once_cell", +] + +[[package]] +name = "clap_builder" +version = "4.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98c59138d527eeaf9b53f35a77fcc1fad9d883116070c63d5de1c7dc7b00c72b" +dependencies = [ + "anstream", + "anstyle", + "clap_lex 0.5.0", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8cd2b2a819ad6eec39e8f1d6b53001af1e5469f8c177579cdaeb313115b825f" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.28", +] + [[package]] name = "clap_lex" version = "0.2.4" @@ -357,13 +596,24 @@ dependencies = [ ] [[package]] -name = "codespan-reporting" -version = "0.11.1" +name = "clap_lex" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" + +[[package]] +name = "colorchoice" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "combine" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" dependencies = [ - "termcolor", - "unicode-width", + "memchr", ] [[package]] @@ -376,6 +626,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + [[package]] name = "core-foundation" version = "0.9.3" @@ -394,9 +650,9 @@ checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" [[package]] name = "cpufeatures" -version = "0.2.7" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" +checksum = "03e69e28e9f7f77debdedbaafa2866e1de9ba56df55a8bd7cfc724c25a09987c" dependencies = [ "libc", ] @@ -420,7 +676,7 @@ dependencies = [ "atty", "cast", "ciborium", - "clap", + "clap 3.2.25", "criterion-plot", "itertools", "lazy_static", @@ -469,9 +725,9 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.14" +version = "0.9.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" dependencies = [ "autocfg", "cfg-if 1.0.0", @@ -482,13 +738,19 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.15" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c063cd8cc95f5c377ed0d4b49a4b21f632396ff690e8470c29b3359b346984b" +checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294" dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -501,9 +763,9 @@ dependencies = [ [[package]] name = "csv" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b015497079b9a9d69c02ad25de6c0a6edef051ea6360a327d0bd05802ef64ad" +checksum = "626ae34994d3d8d668f4269922248239db4ae42d538b14c398b74a52208e8086" dependencies = [ "csv-core", "itoa", @@ -531,81 +793,60 @@ dependencies = [ ] [[package]] -name = "ctor" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd4056f63fce3b82d852c3da92b08ea59959890813a7f4ce9c0ff85b10cf301b" +name = "custom_python_extension" +version = "0.1.1" dependencies = [ - "quote", - "syn 2.0.15", + "pyo3", + "pyo3-build-config", + "raphtory", ] [[package]] -name = "cxx" -version = "1.0.94" +name = "darling" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f61f1b6389c3fe1c316bf8a4dccc90a38208354b330925bce1f74a6c4756eb93" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" dependencies = [ - "cc", - "cxxbridge-flags", - "cxxbridge-macro", - "link-cplusplus", + "darling_core 0.14.4", + "darling_macro 0.14.4", ] [[package]] -name = "cxx-build" -version = "1.0.94" +name = "darling" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12cee708e8962df2aeb38f594aae5d827c022b6460ac71a7a3e2c3c2aae5a07b" +checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" dependencies = [ - "cc", - "codespan-reporting", - "once_cell", - "proc-macro2", - "quote", - "scratch", - "syn 2.0.15", + "darling_core 0.20.3", + "darling_macro 0.20.3", ] [[package]] -name = "cxxbridge-flags" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7944172ae7e4068c533afbb984114a56c46e9ccddda550499caa222902c7f7bb" - -[[package]] -name = "cxxbridge-macro" -version = "1.0.94" +name = "darling_core" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" dependencies = [ + "fnv", + "ident_case", "proc-macro2", "quote", - "syn 2.0.15", -] - -[[package]] -name = "darling" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" -dependencies = [ - "darling_core", - "darling_macro", + "strsim", + "syn 1.0.109", ] [[package]] name = "darling_core" -version = "0.14.4" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621" dependencies = [ "fnv", "ident_case", "proc-macro2", "quote", "strsim", - "syn 1.0.109", + "syn 2.0.28", ] [[package]] @@ -614,11 +855,22 @@ version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" dependencies = [ - "darling_core", + "darling_core 0.14.4", "quote", "syn 1.0.109", ] +[[package]] +name = "darling_macro" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" +dependencies = [ + "darling_core 0.20.3", + "quote", + "syn 2.0.28", +] + [[package]] name = "dashmap" version = "5.4.0" @@ -626,10 +878,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" dependencies = [ "cfg-if 1.0.0", - "hashbrown", + "hashbrown 0.12.3", "lock_api", "once_cell", "parking_lot_core", + "serde", ] [[package]] @@ -651,6 +904,37 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eaa37046cc0f6c3cc6090fbdbf73ef0b8ef4cfcc37f6befc0020f63e8cf121e1" +[[package]] +name = "derive_builder" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d67778784b508018359cbc8696edb3db78160bab2c2a28ba7f56ef6932997f8" +dependencies = [ + "derive_builder_macro", +] + +[[package]] +name = "derive_builder_core" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f" +dependencies = [ + "darling 0.14.4", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_builder_macro" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebcda35c7a396850a55ffeac740804b40ffec779b98fffbb1738f4033f0ee79e" +dependencies = [ + "derive_builder_core", + "syn 1.0.109", +] + [[package]] name = "diff" version = "0.1.13" @@ -659,19 +943,20 @@ checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" [[package]] name = "digest" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] name = "display-error-chain" -version = "0.1.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e1a8646b2c125eeb9a84ef0faa6d2d102ea0d5da60b824ade2743263117b848" +checksum = "f77af9e75578c1ab34f5f04545a8b05be0c36fbd7a9bb3cf2d2a971e435fdbb9" [[package]] name = "dotenv" @@ -679,6 +964,18 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" +[[package]] +name = "downcast-rs" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" + +[[package]] +name = "dyn-clone" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30" + [[package]] name = "dynamic-graphql" version = "0.7.3" @@ -697,7 +994,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c547074a568bfe79c9858a4be0ba42abb18b21f9ddfee44b3c83b96a24d7ef7" dependencies = [ "Inflector", - "darling", + "darling 0.14.4", "proc-macro-crate", "proc-macro2", "quote", @@ -720,6 +1017,18 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "enum_dispatch" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f36e95862220b211a6e2aa5eca09b4fa391b13cd52ceb8035a24bf65a79de2" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "env_logger" version = "0.7.1" @@ -740,6 +1049,12 @@ dependencies = [ "regex", ] +[[package]] +name = "equivalent" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88bffebc5d80432c9b140ee17875ff173a8ab62faad5b257da912bd2f6c1c0a1" + [[package]] name = "errno" version = "0.3.1" @@ -761,20 +1076,47 @@ dependencies = [ "libc", ] +[[package]] +name = "ethnum" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0198b9d0078e0f30dedc7acbb21c974e838fc8fae3ee170128658a98cb2c1c04" + +[[package]] +name = "eventsource-stream" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74fef4569247a5f429d9156b9d0a2599914385dd189c539334c625d8099d90ab" +dependencies = [ + "futures-core", + "nom", + "pin-project-lite", +] + [[package]] name = "examples" -version = "0.4.0" +version = "0.5.7" dependencies = [ "chrono", "itertools", "rand 0.8.5", "raphtory", - "raphtory-io", "rayon", "regex", "serde", ] +[[package]] +name = "fail" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5e43d0f78a42ad591453aedb1d7ae631ce7ee445c7643691055a9ed8d3b01c" +dependencies = [ + "log", + "once_cell", + "rand 0.8.5", +] + [[package]] name = "fast_chemail" version = "0.9.6" @@ -784,6 +1126,12 @@ dependencies = [ "ascii_utils", ] +[[package]] +name = "fastdivide" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25c7df09945d65ea8d70b3321547ed414bbc540aad5bac6883d021b970f35b04" + [[package]] name = "fastrand" version = "1.9.0" @@ -793,11 +1141,23 @@ dependencies = [ "instant", ] +[[package]] +name = "filetime" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall 0.2.16", + "windows-sys 0.48.0", +] + [[package]] name = "flate2" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" +checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" dependencies = [ "crc32fast", "miniz_oxide", @@ -813,7 +1173,7 @@ dependencies = [ "futures-sink", "nanorand", "pin-project", - "spin", + "spin 0.9.8", ] [[package]] @@ -837,15 +1197,31 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "foreign_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee1b05cbd864bcaecbd3455d6d967862d446e4ebfc3c2e5e5b9841e53cba6673" + [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" dependencies = [ "percent-encoding", ] +[[package]] +name = "fs4" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eeb4ed9e12f43b7fa0baae3f9cdda28352770132ef2e09a23760c29cae8bd47" +dependencies = [ + "rustix 0.38.2", + "windows-sys 0.48.0", +] + [[package]] name = "fuchsia-cprng" version = "0.1.1" @@ -909,7 +1285,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.28", ] [[package]] @@ -924,6 +1300,12 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +[[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" + [[package]] name = "futures-util" version = "0.3.28" @@ -972,6 +1354,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "generator" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc16584ff22b460a382b7feec54b23d2908d858152e5739a120b949293bd74e" +dependencies = [ + "cc", + "libc", + "log", + "rustversion", + "windows", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -995,9 +1390,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" +checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -1014,14 +1409,14 @@ checksum = "e77ac7b51b8e6313251737fcef4b1c01a2ea102bde68415b62c0ee9268fec357" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.28", ] [[package]] name = "h2" -version = "0.3.18" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17f8a914c2987b688368b5138aa05321db91f4090cf26118185672ad588bce21" +checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" dependencies = [ "bytes", "fnv", @@ -1029,7 +1424,7 @@ dependencies = [ "futures-sink", "futures-util", "http", - "indexmap", + "indexmap 1.9.3", "slab", "tokio", "tokio-util", @@ -1056,12 +1451,33 @@ dependencies = [ "thiserror", ] +[[package]] +name = "hash_hasher" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74721d007512d0cb3338cd20f0654ac913920061a4c4d0d8708edb3f2a698c0c" + [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" + [[package]] name = "headers" version = "0.3.8" @@ -1069,7 +1485,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3e372db8e5c0d213e0cd0b9be18be2aca3d44cf2fe30a9d46a65581cd454584" dependencies = [ "base64 0.13.1", - "bitflags", + "bitflags 1.3.2", "bytes", "headers-core", "http", @@ -1087,6 +1503,12 @@ dependencies = [ "http", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -1111,6 +1533,27 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "htmlescape" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" + [[package]] name = "http" version = "0.2.9" @@ -1147,9 +1590,9 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" [[package]] name = "hyper" -version = "0.14.26" +version = "0.14.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab302d72a6f11a3b910431ff93aae7e773078c769f0a3ef15fb9ec692ed147d4" +checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468" dependencies = [ "bytes", "futures-channel", @@ -1169,6 +1612,20 @@ dependencies = [ "want", ] +[[package]] +name = "hyper-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d78e1e73ec14cf7375674f74d7dde185c8206fd9dea6fb6295e8a98098aaa97" +dependencies = [ + "futures-util", + "http", + "hyper", + "rustls", + "tokio", + "tokio-rustls", +] + [[package]] name = "hyper-tls" version = "0.5.0" @@ -1184,9 +1641,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.56" +version = "0.1.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -1198,12 +1655,11 @@ dependencies = [ [[package]] name = "iana-time-zone-haiku" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" dependencies = [ - "cxx", - "cxx-build", + "cc", ] [[package]] @@ -1214,9 +1670,9 @@ checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" [[package]] name = "idna" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" dependencies = [ "unicode-bidi", "unicode-normalization", @@ -1229,7 +1685,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" +dependencies = [ + "equivalent", + "hashbrown 0.14.0", "serde", ] @@ -1239,6 +1706,15 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "instant" version = "0.1.12" @@ -1246,6 +1722,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", ] [[package]] @@ -1256,19 +1735,18 @@ checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02" [[package]] name = "inventory" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7741301a6d6a9b28ce77c0fb77a4eb116b6bc8f3bef09923f7743d059c4157d3" +checksum = "e0539b5de9241582ce6bd6b0ba7399313560151e58c9aaf8b74b711b1bdce644" dependencies = [ - "ctor 0.2.0", "ghost", ] [[package]] name = "io-lifetimes" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ "hermit-abi 0.3.1", "libc", @@ -1277,9 +1755,20 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.7.2" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12b6ee2129af8d4fb011108c73d99a1b83a85977f23b82460c0ae2e25bb4b57f" +checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" + +[[package]] +name = "is-terminal" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +dependencies = [ + "hermit-abi 0.3.1", + "rustix 0.38.2", + "windows-sys 0.48.0", +] [[package]] name = "itertools" @@ -1296,14 +1785,25 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" +[[package]] +name = "jobserver" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +dependencies = [ + "libc", +] + [[package]] name = "js-raphtory" -version = "0.4.0" +version = "0.5.7" dependencies = [ "chrono", "console_error_panic_hook", "js-sys", "raphtory", + "serde", + "serde-wasm-bindgen", "wasm-bindgen", "wasm-bindgen-test", "wee_alloc", @@ -1311,51 +1811,64 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.63" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f37a4a5928311ac501dee68b3c7613a1037d0edb30c8e5427bd832d55d1b790" +checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a" dependencies = [ "wasm-bindgen", ] +[[package]] +name = "kdam" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eec124c5ef865373afd03f7900161495339c59cef395a6dc45d025bcd6499b0b" +dependencies = [ + "terminal_size", + "windows-sys 0.48.0", +] + [[package]] name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "levenshtein_automata" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2cdeb66e45e9f36bfad5bbdb4d2384e70936afbee843c6f6543f0c551ebb25" + [[package]] name = "libc" -version = "0.2.142" +version = "0.2.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" [[package]] name = "libm" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb" +checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" [[package]] -name = "link-cplusplus" -version = "1.0.8" +name = "linux-raw-sys" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" -dependencies = [ - "cc", -] +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "linux-raw-sys" -version = "0.3.7" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ece97ea872ece730aed82664c424eb4c8291e1ff2480247ccf7409044bc6479f" +checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" [[package]] name = "lock_api" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" dependencies = [ "autocfg", "scopeguard", @@ -1364,13 +1877,39 @@ dependencies = [ [[package]] name = "log" -version = "0.4.17" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" + +[[package]] +name = "loom" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +checksum = "ff50ecb28bb86013e935fb6683ab1f6d3a20016f123c76fd4c27470076ac30f5" dependencies = [ "cfg-if 1.0.0", + "generator", + "pin-utils", + "scoped-tls", + "tracing", + "tracing-subscriber", ] +[[package]] +name = "lru" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "718e8fae447df0c7e1ba7f5189829e63fd536945c8988d61444c19039f16b670" +dependencies = [ + "hashbrown 0.13.2", +] + +[[package]] +name = "lz4_flex" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b8c72594ac26bfd34f2d99dfced2edfaddfe8a476e3ff2ca0eb293d925c4f83" + [[package]] name = "matchers" version = "0.1.0" @@ -1380,17 +1919,36 @@ dependencies = [ "regex-automata", ] +[[package]] +name = "measure_time" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56220900f1a0923789ecd6bf25fbae8af3b2f1ff3e9e297fc9b6b8674dd4d852" +dependencies = [ + "instant", + "log", +] + [[package]] name = "memchr" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "memmap2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d28bba84adfe6646737845bc5ebbfa2c08424eb1c37e94a1fd2a82adb56a872" +dependencies = [ + "libc", +] + [[package]] name = "memoffset" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" dependencies = [ "autocfg", ] @@ -1407,25 +1965,40 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" -version = "0.6.2" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7" dependencies = [ "adler", ] [[package]] name = "mio" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b9d9a46eff5b4ff64b45a9e316a6d1e0bc719ef429cbec4dc630684212bfdf9" +checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", - "log", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] @@ -1442,17 +2015,23 @@ dependencies = [ "log", "memchr", "mime", - "spin", + "spin 0.9.8", "version_check", ] +[[package]] +name = "murmurhash32" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9380db4c04d219ac5c51d14996bbf2c2e9a15229771b53f8671eb6c83cf44df" + [[package]] name = "nanorand" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3" dependencies = [ - "getrandom 0.2.9", + "getrandom 0.2.10", ] [[package]] @@ -1475,9 +2054,9 @@ dependencies = [ [[package]] name = "neo4rs" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243f7625e3622d7d51e419be402a64502c202bdc33a40b9fb792aa0868490c2c" +checksum = "b7d3e9f28d52b1cde4bbbdd52f1e3c257f9050e34207880f3f1724179648244a" dependencies = [ "async-trait", "bytes", @@ -1486,8 +2065,12 @@ dependencies = [ "futures", "log", "neo4rs-macros", + "pin-project-lite", "thiserror", "tokio", + "tokio-rustls", + "url", + "webpki-roots", ] [[package]] @@ -1500,6 +2083,16 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1599,9 +2192,18 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.17.2" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "oneshot" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9670a07f94779e00908f3e686eab508878ebb390ba6e604d3a284c00e8d0487b" +checksum = "fc22d22931513428ea6cc089e942d38600e3d00976eef8c86de6b8a3aadec6eb" +dependencies = [ + "loom", +] [[package]] name = "oorandom" @@ -1611,11 +2213,11 @@ checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" [[package]] name = "openssl" -version = "0.10.52" +version = "0.10.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01b8574602df80f7b85fdfc5392fa884a4e3b3f4f35402c070ab34c3d3f78d56" +checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" dependencies = [ - "bitflags", + "bitflags 1.3.2", "cfg-if 1.0.0", "foreign-types", "libc", @@ -1632,7 +2234,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.28", ] [[package]] @@ -1643,18 +2245,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "111.25.3+1.1.1t" +version = "111.26.0+1.1.1u" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "924757a6a226bf60da5f7dd0311a34d2b52283dd82ddeb103208ddc66362f80c" +checksum = "efc62c9f12b22b8f5208c23a7200a442b2e5999f8bdf80233852122b5a4f6f37" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.87" +version = "0.9.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e17f59264b2809d77ae94f0e1ebabc434773f370d6ca667bd223ea10e06cc7e" +checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" dependencies = [ "cc", "libc", @@ -1708,7 +2310,7 @@ dependencies = [ "fnv", "futures-channel", "futures-util", - "indexmap", + "indexmap 1.9.3", "js-sys", "once_cell", "pin-project-lite", @@ -1746,11 +2348,20 @@ dependencies = [ "num-traits", ] +[[package]] +name = "ordered-float" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc2dbde8f8a79f2102cc474ceb0ad68e3b80b85289ea62389b60e66777e4213" +dependencies = [ + "num-traits", +] + [[package]] name = "os_str_bytes" -version = "6.5.0" +version = "6.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceedf44fb00f2d1984b0bc98102627ce622e083e49a5bacdb3e514fa4238e267" +checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac" [[package]] name = "output_vt100" @@ -1767,6 +2378,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "ownedbytes" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c718e498b20704d5fb5d51d07f414a22f61c19254c1708e117b93fd76860739c" +dependencies = [ + "stable_deref_trait", +] + [[package]] name = "parking_lot" version = "0.12.1" @@ -1779,28 +2399,51 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall 0.2.16", + "redox_syscall 0.3.5", "smallvec", - "windows-sys 0.45.0", + "windows-targets", +] + +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest", + "hmac", + "password-hash", + "sha2", ] [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pest" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e68e84bfb01f0507134eac1e9b410a12ba379d064eab48c50ba4ce329a527b70" +checksum = "f73935e4d55e2abf7f130186537b19e7a4abc886a0252380b59248af473a3fc9" dependencies = [ "thiserror", "ucd-trie", @@ -1808,9 +2451,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b79d4c71c865a25a4322296122e3924d30bc8ee0834c8bfc8b95f7f054afbfb" +checksum = "aef623c9bbfa0eedf5a0efba11a5ee83209c326653ca31ff019bec3a95bfff2b" dependencies = [ "pest", "pest_generator", @@ -1818,22 +2461,22 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c435bf1076437b851ebc8edc3a18442796b30f1728ffea6262d59bbe28b077e" +checksum = "b3e8cba4ec22bada7fc55ffe51e2deb6a0e0db2d0b7ab0b103acc80d2510c190" dependencies = [ "pest", "pest_meta", "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.28", ] [[package]] name = "pest_meta" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "745a452f8eb71e39ffd8ee32b3c5f51d03845f99786fa9b68db6ff509c505411" +checksum = "a01f71cb40bd8bb94232df14b946909e14660e33fc05db3e50ae2a82d7ea0ca0" dependencies = [ "once_cell", "pest", @@ -1842,22 +2485,22 @@ dependencies = [ [[package]] name = "pin-project" -version = "1.0.12" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad29a609b6bcd67fee905812e544992d216af9d755757c05ed2d0e15a74c6ecc" +checksum = "c95a7476719eab1e366eaf73d0260af3021184f18177925b07f54b30089ceead" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.0.12" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "069bdb1e05adc7a8990dce9cc75370895fbe4e3d58b9b73bf1aee56359344a55" +checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.28", ] [[package]] @@ -1874,15 +2517,15 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.26" +version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" +checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" [[package]] name = "plotters" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2538b639e642295546c50fcd545198c9d64ee2a38620a628724a3b266d5fbf97" +checksum = "d2c224ba00d7cadd4d5c660deaf2098e5e80e07846537c51f9cfa4be50c1fd45" dependencies = [ "num-traits", "plotters-backend", @@ -1893,27 +2536,27 @@ dependencies = [ [[package]] name = "plotters-backend" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "193228616381fecdc1224c62e96946dfbc73ff4384fba576e052ff8c1bea8142" +checksum = "9e76628b4d3a7581389a35d5b6e2139607ad7c75b17aed325f210aa91f4a9609" [[package]] name = "plotters-svg" -version = "0.3.3" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a81d2759aae1dae668f783c308bc5c8ebd191ff4184aaa1b37f65a6ae5a56f" +checksum = "38f6d39893cca0701371e3c27294f09797214b86f1fb951b89ade8ec04e2abab" dependencies = [ "plotters-backend", ] [[package]] name = "poem" -version = "1.3.55" +version = "1.3.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0608069d4999c3c02d49dff261663f2e73a8f7b00b7cd364fb5e93e419dafa1" +checksum = "0a56df40b79ebdccf7986b337f9b0e51ac55cd5e9d21fb20b6aa7c7d49741854" dependencies = [ "async-trait", - "base64 0.21.0", + "base64 0.21.2", "bytes", "futures-util", "headers", @@ -1939,9 +2582,9 @@ dependencies = [ [[package]] name = "poem-derive" -version = "1.3.55" +version = "1.3.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b839bad877aa933dd00901abd127a44496130e3def48e079d60e43f2c8a33cc" +checksum = "1701f977a2d650a03df42c053686ea0efdb83554f34c7b026b89383c0a1b7846" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -1961,7 +2604,7 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a25e9bcb20aa780fd0bb16b72403a9064d6b3f22f026946029acb941a50af755" dependencies = [ - "ctor 0.1.26", + "ctor", "diff", "output_vt100", "yansi", @@ -2011,43 +2654,18 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.56" +version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb" dependencies = [ "unicode-ident", ] -[[package]] -name = "py-raphtory" -version = "0.4.0" -dependencies = [ - "bincode", - "chrono", - "csv", - "display-error-chain", - "flate2", - "flume", - "futures", - "itertools", - "num", - "parking_lot", - "pyo3", - "raphtory", - "raphtory-io", - "rayon", - "regex", - "replace_with", - "rustc-hash", - "serde", - "tokio", -] - [[package]] name = "pyo3" -version = "0.18.3" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b1ac5b3731ba34fdaa9785f8d74d17448cd18f30cf19e0c7e7b1fdb5272109" +checksum = "e681a6cfdc4adcc93b4d3cf993749a4552018ee0a9b65fc0ccfad74352c72a38" dependencies = [ "cfg-if 1.0.0", "chrono", @@ -2062,11 +2680,24 @@ dependencies = [ "unindent", ] +[[package]] +name = "pyo3-asyncio" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2cc34c1f907ca090d7add03dc523acdd91f3a4dab12286604951e2f5152edad" +dependencies = [ + "futures", + "once_cell", + "pin-project-lite", + "pyo3", + "tokio", +] + [[package]] name = "pyo3-build-config" -version = "0.18.3" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cb946f5ac61bb61a5014924910d936ebd2b23b705f7a4a3c40b05c720b079a3" +checksum = "076c73d0bc438f7a4ef6fdd0c3bb4732149136abd952b110ac93e4edb13a6ba5" dependencies = [ "once_cell", "target-lexicon", @@ -2074,9 +2705,9 @@ dependencies = [ [[package]] name = "pyo3-ffi" -version = "0.18.3" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd4d7c5337821916ea2a1d21d1092e8443cf34879e53a0ac653fbb98f44ff65c" +checksum = "e53cee42e77ebe256066ba8aa77eff722b3bb91f3419177cf4cd0f304d3284d9" dependencies = [ "libc", "pyo3-build-config", @@ -2084,9 +2715,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.18.3" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d39c55dab3fc5a4b25bbd1ac10a2da452c4aca13bb450f22818a002e29648d" +checksum = "dfeb4c99597e136528c6dd7d5e3de5434d1ceaf487436a3f03b2d56b6fc9efd1" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -2096,9 +2727,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.18.3" +version = "0.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97daff08a4c48320587b5224cc98d609e3c27b6d437315bd40b605c98eeb5918" +checksum = "947dc12175c254889edc0c02e399476c2f652b4b9ebd123aa655c224de259536" dependencies = [ "proc-macro2", "quote", @@ -2141,9 +2772,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.26" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +checksum = "5907a1b7c277254a8b15170f6e7c97cfa60ee7872a3217663bb81151e48184bb" dependencies = [ "proc-macro2", ] @@ -2235,7 +2866,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.9", + "getrandom 0.2.10", ] [[package]] @@ -2259,19 +2890,34 @@ dependencies = [ [[package]] name = "raphtory" -version = "0.4.0" +version = "0.5.7" dependencies = [ + "arrow2", + "async-trait", "bincode", + "bzip2", "chrono", "csv", + "dashmap", + "display-error-chain", + "enum_dispatch", + "flate2", "flume", "futures", + "futures-util", "genawaiter", "itertools", + "kdam", + "lock_api", + "neo4rs", + "num", "num-traits", "once_cell", + "ordered-float 3.7.0", "parking_lot", "pretty_assertions", + "pyo3", + "pyo3-asyncio", "quickcheck 1.0.3", "quickcheck_macros", "rand 0.8.5", @@ -2279,35 +2925,46 @@ dependencies = [ "rayon", "regex", "replace_with", + "reqwest", "roaring", "rustc-hash", "serde", + "serde_json", + "serde_with", "sorted_vector_map", + "tantivy", "tempdir", + "tempfile", "thiserror", + "tokio", "twox-hash", "uuid", + "zip", ] [[package]] name = "raphtory-benchmark" -version = "0.4.0" +version = "0.5.7" dependencies = [ "criterion", "rand 0.8.5", "raphtory", - "raphtory-io", "rayon", "sorted_vector_map", ] [[package]] name = "raphtory-graphql" -version = "0.4.0" +version = "0.5.7" dependencies = [ "async-graphql", "async-graphql-poem", + "async-openai", "async-stream", + "base64 0.21.2", + "bincode", + "chrono", + "clap 4.3.11", "dotenv", "dynamic-graphql", "futures-util", @@ -2315,44 +2972,46 @@ dependencies = [ "once_cell", "opentelemetry", "opentelemetry-jaeger", + "ordered-float 3.7.0", + "parking_lot", "poem", "raphtory", "serde", "serde_json", + "tempfile", + "thiserror", "tokio", "tracing", "tracing-opentelemetry", "tracing-subscriber", + "uuid", + "walkdir", ] [[package]] -name = "raphtory-io" -version = "0.4.0" +name = "raphtory-pymodule" +version = "0.5.7" dependencies = [ - "bzip2", - "chrono", - "csv", - "flate2", - "itertools", - "neo4rs", + "openssl", + "pyo3", + "pyo3-asyncio", + "pyo3-build-config", "raphtory", - "rayon", - "regex", - "reqwest", - "serde", - "serde_json", - "tokio", - "zip", + "raphtory-graphql", ] [[package]] -name = "raphtory-pymodule" -version = "0.4.0" +name = "raphtory-rust-benchmark" +version = "0.5.7" dependencies = [ - "openssl", - "py-raphtory", - "pyo3", - "pyo3-build-config", + "chrono", + "clap 4.3.11", + "csv", + "flate2", + "ordered-float 3.7.0", + "raphtory", + "serde", + "tar", ] [[package]] @@ -2392,7 +3051,7 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] @@ -2401,18 +3060,18 @@ version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] name = "regex" -version = "1.8.1" +version = "1.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" +checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.7.1", + "regex-syntax 0.7.2", ] [[package]] @@ -2432,9 +3091,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" +checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78" [[package]] name = "remove_dir_all" @@ -2453,11 +3112,11 @@ checksum = "e3a8614ee435691de62bcffcf4a66d91b3594bf1428a5722e79103249a095690" [[package]] name = "reqwest" -version = "0.11.17" +version = "0.11.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13293b639a097af28fc8a90f22add145a9c954e49d77da06263d58cf44d5fb91" +checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" dependencies = [ - "base64 0.21.0", + "base64 0.21.2", "bytes", "encoding_rs", "futures-core", @@ -2466,28 +3125,52 @@ dependencies = [ "http", "http-body", "hyper", + "hyper-rustls", "hyper-tls", "ipnet", "js-sys", "log", "mime", + "mime_guess", "native-tls", "once_cell", "percent-encoding", "pin-project-lite", + "rustls", + "rustls-native-certs", + "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", "tokio", "tokio-native-tls", + "tokio-rustls", + "tokio-util", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-streams", "web-sys", "winreg", ] +[[package]] +name = "reqwest-eventsource" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f03f570355882dd8d15acc3a313841e6e90eddbc76a93c748fd82cc13ba9f51" +dependencies = [ + "eventsource-stream", + "futures-core", + "futures-timer", + "mime", + "nom", + "pin-project-lite", + "reqwest", + "thiserror", +] + [[package]] name = "retain_mut" version = "0.1.7" @@ -2503,6 +3186,21 @@ dependencies = [ "uncased", ] +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted", + "web-sys", + "winapi", +] + [[package]] name = "roaring" version = "0.10.1" @@ -2515,26 +3213,107 @@ dependencies = [ "serde", ] +[[package]] +name = "rust-stemmers" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e46a2036019fdb888131db7a4c847a1063a7493f971ed94ea82c67eada63ca54" +dependencies = [ + "serde", + "serde_derive", +] + [[package]] name = "rustc-hash" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "rustix" -version = "0.37.19" +version = "0.37.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acf8729d8542766f1b2cf77eb034d52f40d375bb8b615d0b147089946e16613d" +checksum = "b96e891d04aa506a6d1f318d2771bcb1c7dfda84e126660ace067c9b474bb2c0" dependencies = [ - "bitflags", + "bitflags 1.3.2", "errno", "io-lifetimes", "libc", - "linux-raw-sys", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustix" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aabcb0461ebd01d6b79945797c27f8529082226cb630a9865a71870ff63532a4" +dependencies = [ + "bitflags 2.3.3", + "errno", + "libc", + "linux-raw-sys 0.4.3", "windows-sys 0.48.0", ] +[[package]] +name = "rustls" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e32ca28af694bc1bbf399c33a516dbdf1c90090b8ab23c2bc24f834aa2247f5f" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3987094b1d07b653b7dfdc3f70ce9a1da9c51ac18c1b06b662e4f9a0e9f4b2" +dependencies = [ + "base64 0.21.2", +] + +[[package]] +name = "rustls-webpki" +version = "0.100.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6207cd5ed3d8dca7816f8f3725513a34609c0c765bf652b8c3cb4cfd87db46b" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc31bd9b61a32c31f9650d18add92aa83a49ba979c143eefd27fe7177b05bd5f" + [[package]] name = "ryu" version = "1.0.13" @@ -2572,18 +3351,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] -name = "scratch" -version = "1.0.5" +name = "sct" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] [[package]] name = "security-framework" -version = "2.8.2" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254" +checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" dependencies = [ - "bitflags", + "bitflags 1.3.2", "core-foundation", "core-foundation-sys", "libc", @@ -2592,39 +3375,56 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4" +checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" dependencies = [ "core-foundation-sys", "libc", ] +[[package]] +name = "semver" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" + [[package]] name = "serde" -version = "1.0.160" +version = "1.0.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" +checksum = "9e8c8cf938e98f769bc164923b06dce91cea1751522f46f8466461af04c9027d" dependencies = [ "serde_derive", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_derive" -version = "1.0.160" +version = "1.0.164" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" +checksum = "d9735b638ccc51c28bf6914d90a2e9725b377144fc612c49a611fddd1b631d68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.28", ] [[package]] name = "serde_json" -version = "1.0.96" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +checksum = "46266871c240a00b8f503b877622fe33430b3c7d963bdc0f2adc511e54a1eae3" dependencies = [ "itoa", "ryu", @@ -2637,10 +3437,39 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ - "form_urlencoded", - "itoa", - "ryu", - "serde", + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ca3b16a3d82c4088f343b7480a93550b3eabe1a358569c2dfe38bbcead07237" +dependencies = [ + "base64 0.21.2", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.0.0", + "serde", + "serde_json", + "serde_with_macros", + "time 0.3.22", +] + +[[package]] +name = "serde_with_macros" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e6be15c453eb305019bfa438b1593c731f36a289a7853f7707ee29e870b3b3c" +dependencies = [ + "darling 0.20.3", + "proc-macro2", + "quote", + "syn 2.0.28", ] [[package]] @@ -2656,9 +3485,9 @@ dependencies = [ [[package]] name = "sha2" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +checksum = "479fb9d862239e610720565ca91403019f2f00410f1864c5aa7479b950a76ed8" dependencies = [ "cfg-if 1.0.0", "cpufeatures", @@ -2683,6 +3512,21 @@ dependencies = [ "libc", ] +[[package]] +name = "simdutf8" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" + +[[package]] +name = "sketches-ddsketch" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a406c1882ed7f29cd5e248c9848a80e7cb6ae0fea82346d2746f2f941c07e1" +dependencies = [ + "serde", +] + [[package]] name = "slab" version = "0.4.8" @@ -2717,6 +3561,12 @@ dependencies = [ "quickcheck 0.9.2", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "spin" version = "0.9.8" @@ -2726,6 +3576,12 @@ dependencies = [ "lock_api", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "static_assertions" version = "1.1.0" @@ -2738,6 +3594,12 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + [[package]] name = "syn" version = "1.0.109" @@ -2751,9 +3613,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.15" +version = "2.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" dependencies = [ "proc-macro2", "quote", @@ -2771,11 +3633,165 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "tantivy" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aec540e9cebc88f523f67f596dee213e491f0c55961de013566f267a0c31f5e9" +dependencies = [ + "aho-corasick", + "arc-swap", + "async-trait", + "base64 0.21.2", + "bitpacking", + "byteorder", + "census", + "crc32fast", + "crossbeam-channel", + "downcast-rs", + "fail", + "fastdivide", + "fs4", + "htmlescape", + "itertools", + "levenshtein_automata", + "log", + "lru", + "lz4_flex", + "measure_time", + "memmap2", + "murmurhash32", + "num_cpus", + "once_cell", + "oneshot", + "rayon", + "regex", + "rust-stemmers", + "rustc-hash", + "serde", + "serde_json", + "sketches-ddsketch", + "smallvec", + "tantivy-bitpacker", + "tantivy-columnar", + "tantivy-common", + "tantivy-fst", + "tantivy-query-grammar", + "tantivy-stacker", + "tantivy-tokenizer-api", + "tempfile", + "thiserror", + "time 0.3.22", + "uuid", + "winapi", +] + +[[package]] +name = "tantivy-bitpacker" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16099e96f0ede682084469b80d6909dc170aa2b11d2a45538b5b36b2a90090b9" +dependencies = [ + "bitpacking", +] + +[[package]] +name = "tantivy-columnar" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e32b024b26eab93eb8648faf08004356bf9d47376557ee4409f4b210163656" +dependencies = [ + "fastdivide", + "fnv", + "itertools", + "serde", + "tantivy-bitpacker", + "tantivy-common", + "tantivy-sstable", + "tantivy-stacker", +] + +[[package]] +name = "tantivy-common" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7d12fdd6ec0f7e0962f129c03c696a85ec567734950cbb2b89af4a293ce342f" +dependencies = [ + "async-trait", + "byteorder", + "ownedbytes", + "serde", + "time 0.3.22", +] + +[[package]] +name = "tantivy-fst" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc3c506b1a8443a3a65352df6382a1fb6a7afe1a02e871cee0d25e2c3d5f3944" +dependencies = [ + "byteorder", + "regex-syntax 0.6.29", + "utf8-ranges", +] + +[[package]] +name = "tantivy-query-grammar" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "106d8f78ad1da4f0fdd526a0760c326c0573510d4dedabeb1962d35a35879797" +dependencies = [ + "combine", + "once_cell", + "regex", +] + +[[package]] +name = "tantivy-sstable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eda34243d3ee64bd8f9ba74a3b0d05f4d07beff7767a727212e9b5a19c13dde7" +dependencies = [ + "tantivy-common", + "tantivy-fst", + "zstd 0.12.3+zstd.1.5.2", +] + +[[package]] +name = "tantivy-stacker" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b9e9470301b026ad3b95f79a791a2a3ee81f3ab16fbe412a9dd81ff834acf5" +dependencies = [ + "murmurhash32", + "tantivy-common", +] + +[[package]] +name = "tantivy-tokenizer-api" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64186801b6e06b3a1c4275e23b517835ff4ecbb707318b838dc9de457c062200" +dependencies = [ + "serde", +] + +[[package]] +name = "tar" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b55807c0344e1e6c04d7c965f5289c39a8d94ae23ed5c0b57aabac549f871c6" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "target-lexicon" -version = "0.12.7" +version = "0.12.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd1ba337640d60c3e96bc6f0638a939b9c9a7f2c316a1598c279828b3d1dc8c5" +checksum = "1b1c7f239eb94671427157bd93b3694320f3668d4e1eff08c7285366fd777fac" [[package]] name = "tempdir" @@ -2789,24 +3805,26 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.5.0" +version = "3.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" +checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" dependencies = [ + "autocfg", "cfg-if 1.0.0", "fastrand", "redox_syscall 0.3.5", - "rustix", - "windows-sys 0.45.0", + "rustix 0.37.20", + "windows-sys 0.48.0", ] [[package]] -name = "termcolor" -version = "1.2.0" +name = "terminal_size" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6" +checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237" dependencies = [ - "winapi-util", + "rustix 0.37.20", + "windows-sys 0.48.0", ] [[package]] @@ -2817,22 +3835,22 @@ checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" [[package]] name = "thiserror" -version = "1.0.40" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +checksum = "d9207952ae1a003f42d3d5e892dac3c6ba42aa6ac0c79a6a91a2b5cb4253e75c" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.40" +version = "1.0.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +checksum = "f1728216d3244de4f14f14f8c15c79be1a7c67867d28d69b719690e2a19fb445" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.28", ] [[package]] @@ -2863,7 +3881,7 @@ dependencies = [ "byteorder", "integer-encoding", "log", - "ordered-float", + "ordered-float 1.1.1", "threadpool", ] @@ -2878,6 +3896,33 @@ dependencies = [ "winapi", ] +[[package]] +name = "time" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea9e1b3cf1243ae005d9e74085d4d542f3125458f3a81af210d901dcd7411efd" +dependencies = [ + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" + +[[package]] +name = "time-macros" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372950940a5f07bf38dbe211d7283c9e6d7327df53794992d293e534c733d09b" +dependencies = [ + "time-core", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -2905,9 +3950,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.27.0" +version = "1.28.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0de47a4eecbe11f498978a9b29d792f0d2692d1dd003650c24c76510e3bc001" +checksum = "94d7b1cfd2aa4011f2de74c2c4c63665e27a71006b0a192dcd2710272e73dfa2" dependencies = [ "autocfg", "bytes", @@ -2919,18 +3964,18 @@ dependencies = [ "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.45.0", + "windows-sys 0.48.0", ] [[package]] name = "tokio-macros" -version = "2.0.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61a573bdc87985e9d6ddeed1b3d864e8a302c847e40d647746df2f1de209d1ce" +checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.28", ] [[package]] @@ -2943,6 +3988,16 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.14" @@ -2983,17 +4038,17 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" [[package]] name = "toml_edit" -version = "0.19.8" +version = "0.19.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" +checksum = "266f016b7f039eec8a1a80dfe6156b633d208b9fccca5e4db1d6775b0c4e34a7" dependencies = [ - "indexmap", + "indexmap 2.0.0", "toml_datetime", "winnow", ] @@ -3018,13 +4073,13 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.24" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" +checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.28", ] [[package]] @@ -3137,6 +4192,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.13" @@ -3145,9 +4209,9 @@ checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" [[package]] name = "unicode-ident" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" [[package]] name = "unicode-normalization" @@ -3158,23 +4222,23 @@ dependencies = [ "tinyvec", ] -[[package]] -name = "unicode-width" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" - [[package]] name = "unindent" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1766d682d402817b5ac4490b3c3002d91dfa0d22812f341609f97b08757359c" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "url" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" dependencies = [ "form_urlencoded", "idna", @@ -3187,13 +4251,26 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8-ranges" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "uuid" -version = "1.3.1" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b55a3fef2a1e3b3a00ce878640918820d3c51081576ac657d23af9fc7928fdb" +checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d" dependencies = [ - "getrandom 0.2.9", + "getrandom 0.2.10", + "serde", ] [[package]] @@ -3226,11 +4303,10 @@ dependencies = [ [[package]] name = "want" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ - "log", "try-lock", ] @@ -3254,9 +4330,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.86" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bba0e8cb82ba49ff4e229459ff22a191bbe9a1cb3a341610c9c33efc27ddf73" +checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen-macro", @@ -3264,24 +4340,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.86" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b04bc93f9d6bdee709f6bd2118f57dd6679cf1176a1af464fca3ab0d66d8fb" +checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.28", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.36" +version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d1985d03709c53167ce907ff394f5316aa22cb4e12761295c5dc57dacb6297e" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -3291,9 +4367,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.86" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14d6b024f1a526bb0234f52840389927257beb670610081360e5a03c5df9c258" +checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3301,28 +4377,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.86" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8" +checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.15", + "syn 2.0.28", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.86" +version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93" +checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" [[package]] name = "wasm-bindgen-test" -version = "0.3.36" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e636f3a428ff62b3742ebc3c70e254dfe12b8c2b469d688ea59cdd4abcf502" +checksum = "6e6e302a7ea94f83a6d09e78e7dc7d9ca7b186bc2829c24a22d0753efd680671" dependencies = [ "console_error_panic_hook", "js-sys", @@ -3334,24 +4410,46 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.36" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f18c1fad2f7c4958e7bcce014fa212f59a65d5e3721d0f77e6c0b27ede936ba3" +checksum = "ecb993dd8c836930ed130e020e77d9b2e65dd0fbab1b67c790b0f5d80b11a575" dependencies = [ "proc-macro2", "quote", ] +[[package]] +name = "wasm-streams" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bbae3363c08332cadccd13b67db371814cd214c2524020932f0804b8cf7c078" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "web-sys" -version = "0.3.61" +version = "0.3.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" +checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b" dependencies = [ "js-sys", "wasm-bindgen", ] +[[package]] +name = "webpki-roots" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b03058f88386e5ff5310d9111d53f48b17d732b401aeb83a8d5190f2ac459338" +dependencies = [ + "rustls-webpki", +] + [[package]] name = "wee_alloc" version = "0.4.5" @@ -3401,7 +4499,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-targets 0.48.0", + "windows-targets", ] [[package]] @@ -3419,37 +4517,13 @@ dependencies = [ "windows_x86_64_msvc 0.42.2", ] -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.0", -] - -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", + "windows-targets", ] [[package]] @@ -3553,9 +4627,9 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" [[package]] name = "winnow" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61de7bac303dc551fe038e2b3cef0f571087a47571ea6e79a87692ac99b99699" +checksum = "ca0ace3845f0d96209f0375e6d367e3eb87eb65d27d445bdc9f1843a26f39448" dependencies = [ "memchr", ] @@ -3569,6 +4643,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "xattr" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d1526bbe5aaeb5eb06885f4d987bcdfa5e23187055de9b83fe00156a821fabc" +dependencies = [ + "libc", +] + [[package]] name = "yansi" version = "0.5.1" @@ -3577,14 +4660,69 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" [[package]] name = "zip" -version = "0.5.13" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ab48844d61251bb3835145c521d88aa4031d7139e8485990f60ca911fa0815" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" dependencies = [ + "aes", "byteorder", "bzip2", + "constant_time_eq", "crc32fast", + "crossbeam-utils", "flate2", - "thiserror", - "time", + "hmac", + "pbkdf2", + "sha1", + "time 0.3.22", + "zstd 0.11.2+zstd.1.5.2", +] + +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe 5.0.2+zstd.1.5.2", +] + +[[package]] +name = "zstd" +version = "0.12.3+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76eea132fb024e0e13fd9c2f5d5d595d8a967aa72382ac2f9d39fcc95afd0806" +dependencies = [ + "zstd-safe 6.0.5+zstd.1.5.4", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-safe" +version = "6.0.5+zstd.1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56d9e60b4b1758206c238a10165fbcae3ca37b01744e394c463463f6529d23b" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.8+zstd.1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c" +dependencies = [ + "cc", + "libc", + "pkg-config", ] diff --git a/Cargo.toml b/Cargo.toml index 1ce676d890..5b07049519 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,18 +1,18 @@ [workspace] members = [ "raphtory", - "raphtory-io", "raphtory-benchmark", "examples/rust", + "examples/custom_python_extension", "python", - "py-raphtory", "js-raphtory", "raphtory-graphql", + "comparison-benchmark/rust/raphtory-rust-benchmark" ] default-members = ["raphtory"] [workspace.package] -version = "0.4.0" +version = "0.5.7" documentation = "https://raphtory.readthedocs.io/en/latest/" repository = "https://github.com/Raphtory/raphtory/" license = "GPL-3.0" diff --git a/Makefile b/Makefile index c8f17ad30d..0f48dd1771 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,8 @@ RUST_READTHEDOCS_DOCS_TARGET=docs/source/_rustdoc +rust-fmt: + cargo +nightly fmt + rust-build: cargo build -q @@ -25,4 +28,4 @@ install-python: cd python && maturin build && pip install ../target/wheels/*.whl run-graphql: - cargo run --release -p raphtory-graphql \ No newline at end of file + cargo run --release -p raphtory-graphql diff --git a/README.md b/README.md index 932c2e4715..989cb5696a 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,10 @@ PyPI - + +PyPI Downloads + + Launch Notebook @@ -33,7 +36,7 @@   Pometry   -🧙🏻‍ Tutorial +🧙🏻‍ Tutorial   🐛 Report a Bug   @@ -42,10 +45,10 @@
-Raphtory is an in-memory graph tool written in Rust with friendly Python APIs on top. It is blazingly fast, scales to hundreds of millions of edges +Raphtory is an in-memory vectorised graph database written in Rust with friendly Python APIs on top. It is blazingly fast, scales to hundreds of millions of edges on your laptop, and can be dropped into your existing pipelines with a simple `pip install raphtory`. -It supports time traveling, multilayer modelling, and advanced analytics beyond simple querying like community evolution, dynamic scoring, and mining temporal motifs. +It supports time traveling, full-text search, multilayer modelling, and advanced analytics beyond simple querying like automatic risk detection, dynamic scoring, and temporal motifs. If you wish to contribute, check out the open [list of issues](https://github.com/Pometry/Raphtory/issues), [bounty board](https://github.com/Raphtory/Raphtory/discussions/categories/bounty-board) or hit us up directly on [slack](https://join.slack.com/t/raphtory/shared_invite/zt-xbebws9j-VgPIFRleJFJBwmpf81tvxA). Successful contributions will be reward with swizzling swag! @@ -54,6 +57,7 @@ If you wish to contribute, check out the open [list of issues](https://github.co ```python from raphtory import Graph +from raphtory import algorithms as algo import pandas as pd # Create a new graph @@ -82,12 +86,16 @@ print(pd.DataFrame(results[1:], columns=results[0])) # Grab an edge, explore the history of its 'weight' cb_edge = graph.edge("Bob","Charlie") -weight_history = cb_edge.property_history("weight") +weight_history = cb_edge.properties.temporal.get("weight").items() print("The edge between Bob and Charlie has the following weight history:", weight_history) # Compare this weight between time 2 and time 3 weight_change = cb_edge.at(2)["weight"] - cb_edge.at(3)["weight"] print("The weight of the edge between Bob and Charlie has changed by",weight_change,"pts") + +# Run pagerank and ask for the top ranked node +top_node = algo.pagerank(graph).top_k(1) +print("The most important node in the graph is",top_node[0][0],"with a score of",top_node[0][1]) ``` ```a @@ -107,8 +115,113 @@ Graph(number_of_edges=2, number_of_vertices=3, earliest_time=1, latest_time=3) The edge between Bob and Charlie has the following weight history: [(2, 5.0), (3, -15.0)] The weight of the edge between Bob and Charlie has changed by 20.0 pts + +The top node in the graph is Charlie with a score of 0.4744116163405977 ``` +## GraphQL + +### Create/Load a graph + +Save a raphtory graph and set the `GRAPH_DIRECTORY` environment variable to point to the directory containing the graph. + +
+ + +Alternatively you can run the code below to generate a graph. + + +```bash +mkdir -p /tmp/graphs +mkdir -p examples/rust/src/bin/lotr/data/ +tail -n +2 resource/lotr.csv > examples/rust/src/bin/lotr/data/lotr.csv + +cd examples/rust && cargo run --bin lotr -r + +cp examples/rust/src/bin/lotr/data/graphdb.bincode /tmp/graphs/lotr.bincode +``` + +
+ + +### Run the GraphQL server + +The code below will run GraphQL with a UI at `localhost:1736` + +GraphlQL will look for graph files in `/tmp/graphs` or in the path set in the `GRAPH_DIRECTORY` Environment variable. + +```bash +cd raphtory-graphql && cargo run -r +``` + +
+ℹ️Warning: Server must have the same version + environment +The GraphQL server must be running in the same environment (i.e. debug or release) and same Raphtory version as the generated graph, otherwise it will throw errors due to incompatible graph metadata across versions. +
+ +
+Following will be output upon a successful launch + +```bash +warning: `raphtory` (lib) generated 17 warnings (run `cargo fix --lib -p raphtory` to apply 13 suggestions) + Finished release [optimized] target(s) in 0.91s + Running `Raphtory/target/release/raphtory-graphql` +loading graph from /tmp/graphs/lotr.bincode +Playground: http://localhost:1736 + 2023-08-11T14:36:52.444203Z INFO poem::server: listening, addr: socket://0.0.0.0:1736 + at /Users/pometry/.cargo/registry/src/github.com-1ecc6299db9ec823/poem-1.3.56/src/server.rs:109 + + 2023-08-11T14:36:52.444257Z INFO poem::server: server started + at /Users/pometry/.cargo/registry/src/github.com-1ecc6299db9ec823/poem-1.3.56/src/server.rs:111 +``` +
+ + +### Execute a query + +Go to the Playground at `http://localhost:1736` and execute the following commands: + +Query: +```bash + query GetNodes($graphName: String!) { + graph(name: $graphName) { + nodes { + name + } + } + } +``` + +Query Variables: +```bash +{ + "graphName": "lotr.bincode" +} +``` + +Expected Result: +```bash +{ + "data": { + "graph": { + "nodes": [ + { + "name": "Gandalf" + }, + { + "name": "Elrond" + }, + { + "name": "Frodo" + }, + { + "name": "Bilbo" + }, + ... +``` + + + ## Installing Raphtory diff --git a/benchmark/benchmark_base.py b/comparison-benchmark/python/benchmark_base.py similarity index 69% rename from benchmark/benchmark_base.py rename to comparison-benchmark/python/benchmark_base.py index a3d53c9e6a..b8ad8ae793 100755 --- a/benchmark/benchmark_base.py +++ b/comparison-benchmark/python/benchmark_base.py @@ -2,25 +2,38 @@ ### This class is used by the benchmarking scripts to benchmark the graph tools import time from abc import ABC, abstractmethod -import docker + +try: + import docker +except ImportError as e: + print("IMPORT ERROR, docker not found...") import os import multiprocessing class BenchmarkBase(ABC): - - def start_docker(self, image_name, container_folder, exec_commands, envs={}, ports={}, image_path=None, wait=0, start_cmd=None): + def start_docker( + self, + image_name, + container_folder, + exec_commands, + envs={}, + ports={}, + image_path=None, + wait=0, + start_cmd=None, + ): if envs is None: envs = {} - print('Creating Docker client...') + print("Creating Docker client...") self.docker = docker.from_env() - print('Pulling Docker image...') + print("Pulling Docker image...") self.docker.images.pull(image_name) - print('Defining volumes...') + print("Defining volumes...") local_folder = os.path.abspath(os.getcwd()) - volumes = {local_folder: {'bind': container_folder, 'mode': 'z'}} + volumes = {local_folder: {"bind": container_folder, "mode": "z"}} if image_path: image, build_logs = self.docker.images.build( @@ -28,18 +41,18 @@ def start_docker(self, image_name, container_folder, exec_commands, envs={}, por ) image_name = image.id - print('Running Docker container & benchmark...') + print("Running Docker container & benchmark...") if start_cmd is None: self.container = self.docker.containers.run( image_name, volumes=volumes, detach=True, - stdin_open = True, + stdin_open=True, tty=True, environment=envs, ports=ports, - mem_limit='4g', + mem_limit="4g", ) else: self.container = self.docker.containers.run( @@ -47,21 +60,21 @@ def start_docker(self, image_name, container_folder, exec_commands, envs={}, por command=start_cmd, volumes=volumes, detach=True, - stdin_open = True, + stdin_open=True, tty=True, environment=envs, ports=ports, - mem_limit='4g', + mem_limit="4g", ) time.sleep(wait) try: for cmd in exec_commands: - print(f'Running command {cmd}...') + print(f"Running command {cmd}...") _, stream = self.container.exec_run(cmd, stream=True) for data in stream: - print(data.decode(), end='') + print(data.decode(), end="") print() del stream # print(exec_command) @@ -74,16 +87,20 @@ def start_docker(self, image_name, container_folder, exec_commands, envs={}, por print("Completed command...") except Exception as e: print(e) - print('Error running command') + print("Error running command") self.container.stop() self.container.remove() - return 1, 'Error running command' + return 1, "Error running command" - print('Benchmark completed, retrieving results...') - file_path = '/tmp/bench-*.csv' - file_contents = self.container.exec_run(['/bin/bash', '-c', f'cat {file_path}']).output.decode('utf-8').strip() + print("Benchmark completed, retrieving results...") + file_path = "/tmp/bench-*.csv" + file_contents = ( + self.container.exec_run(["/bin/bash", "-c", f"cat {file_path}"]) + .output.decode("utf-8") + .strip() + ) - print('Removing container...') + print("Removing container...") self.container.stop() self.container.remove() diff --git a/benchmark/benchmark_driver.py b/comparison-benchmark/python/benchmark_driver.py similarity index 65% rename from benchmark/benchmark_driver.py rename to comparison-benchmark/python/benchmark_driver.py index a7ed78ccf2..cf00467771 100755 --- a/benchmark/benchmark_driver.py +++ b/comparison-benchmark/python/benchmark_driver.py @@ -8,14 +8,22 @@ import os from io import StringIO -fns = ['setup', 'degree', 'out_neighbours', 'page_rank', 'connected_components'] +fns = ["setup", "degree", "out_neighbours", "page_rank", "connected_components"] def process_arguments(): - parser = argparse.ArgumentParser(description='benchmark args') - parser.add_argument('--docker', action=argparse.BooleanOptionalAction, - help='Launch with docker containers, --no-docker to run locally', default=True) - parser.add_argument('-b', '--bench', type=str, help=""" + parser = argparse.ArgumentParser(description="benchmark args") + parser.add_argument( + "--docker", + action=argparse.BooleanOptionalAction, + help="Launch with docker containers, --no-docker to run locally", + default=True, + ) + parser.add_argument( + "-b", + "--bench", + type=str, + help=""" Run specific benchmark, default: Goes to Menu (if docker runs all), all: Run All @@ -28,7 +36,9 @@ def process_arguments(): mem: Run Memgraph Benchmark cozo: Run CozoDB Benchmark exit: Exit - """, default='menu') + """, + default="menu", + ) return parser.parse_args() @@ -51,26 +61,26 @@ def display_menu(): def setup(): return { - 'all': 'ALL', - 'download': 'DOWNLOAD', - 'r': RaphtoryBench, - 'gt': GraphToolBench, - 'k': KuzuBench, - 'nx': NetworkXBench, - 'neo': Neo4jBench, - 'mem': MemgraphBench, - 'cozo': CozoDBBench + "all": "ALL", + "download": "DOWNLOAD", + "r": RaphtoryBench, + "gt": GraphToolBench, + "k": KuzuBench, + "nx": NetworkXBench, + "neo": Neo4jBench, + "mem": MemgraphBench, + "cozo": CozoDBBench, } def run_benchmark(choice, docker=False): benchmarks_to_run = [] - if choice.lower() == 'all': + if choice.lower() == "all": for key in setup().keys(): - if key == 'menu' or key == 'all' or key == 'download': + if key == "menu" or key == "all" or key == "download": continue benchmarks_to_run.append(key) - elif choice == 'download' or choice == 'menu': + elif choice == "download" or choice == "menu": return elif choice in setup().keys(): benchmarks_to_run.append(choice) @@ -79,6 +89,8 @@ def run_benchmark(choice, docker=False): print("Running benchmarks: " + str(benchmarks_to_run)) for key in benchmarks_to_run: driver = setup()[key] + print(key) + print(setup()) if docker: print("** Running dockerized benchmark " + str(key) + "...") print("Starting docker container...") @@ -88,23 +100,25 @@ def run_benchmark(choice, docker=False): results[driver.name()] = logs else: print("** Running for " + driver.name() + "...") - times = '' - fn_header = '' + times = "" + fn_header = "" for fn in fns: print("** Running " + fn + "...") start_time = time.time() getattr(driver, fn)() end_time = time.time() print(fn + " time: " + str(end_time - start_time)) - fn_header += fn + ',' - if driver.name() == 'Neo4j' and fn == 'setup': + fn_header += fn + "," + if driver.name() == "Neo4j" and fn == "setup": # take away 15 seconds for the sleep time when restarting the database end_time = end_time - 50 - times += str(end_time - start_time) + ',' + times += str(end_time - start_time) + "," fn_header = fn_header[:-1] times = times[:-1] - results[driver.name()] = fn_header + '\n' + times - pd.DataFrame([times.split(',')], columns=fns).to_csv('/tmp/bench-'+driver.name()+'-'+str(time.time())+'.csv') + results[driver.name()] = fn_header + "\n" + times + pd.DataFrame([times.split(",")], columns=fns).to_csv( + "/tmp/bench-" + driver.name() + "-" + str(time.time()) + ".csv" + ) return results @@ -116,10 +130,10 @@ def print_table(data): _data[key] = pd.read_csv(StringIO(value)) merged_df = pd.concat([df.assign(key=key) for key, df in _data.items()]) col = merged_df.pop("key") - if 'Unnamed: 0' in merged_df.columns: - merged_df.drop('Unnamed: 0', axis=1, inplace=True) - merged_df.insert(0, 'System', col) - print(merged_df.to_string(index=False, justify='left')) + if "Unnamed: 0" in merged_df.columns: + merged_df.drop("Unnamed: 0", axis=1, inplace=True) + merged_df.insert(0, "System", col) + print(merged_df.to_string(index=False, justify="left")) def dl_file(url, path): @@ -127,7 +141,7 @@ def dl_file(url, path): print("Downloading " + url + "...") r = requests.get(url, stream=True) if r.status_code == 200: - with open(path, 'wb') as f: + with open(path, "wb") as f: f.write(r.raw.read()) else: print("Error downloading data") @@ -146,38 +160,38 @@ def download_data(): # Download the data print("Downloading data...") urls = { - 'simple-profiles.csv.gz': 'https://raw.githubusercontent.com/Raphtory/Data/main/simple-profiles.csv.gz', - 'simple-relationships.csv.gz': 'https://media.githubusercontent.com/media/Raphtory/Data/main/simple-relationships.csv.gz' + "simple-profiles.csv.gz": "https://osf.io/download/w2xns/", + "simple-relationships.csv.gz": "https://osf.io/download/nbq6h/", } # make the data directory - create_directory('data') + create_directory("data") for name, url in urls.items(): - dl_file(url, 'data/' + name) + dl_file(url, "data/" + name) # Unzip the files for name in urls.keys(): print("Unzipping " + name + "...") - with gzip.open('data/' + name, 'rb') as f_in: - with open('data/' + name[:-3], 'wb') as f_out: + with gzip.open("data/" + name, "rb") as f_in: + with open("data/" + name[:-3], "wb") as f_out: shutil.copyfileobj(f_in, f_out) print("Done unzipping " + name + "...") - os.remove('data/' + name) + os.remove("data/" + name) # return the file paths - return 'data/simple-profiles.csv', 'data/simple-relationships.csv' + return "data/simple-profiles.csv", "data/simple-relationships.csv" def main(docker, choice): print("Welcome to the Raphtory Benchmarking Tool") results = {} try: - if choice == 'menu': + if choice == "menu": choice = display_menu() - if choice == 'download' or choice == 1: + if choice == "download" or choice == 1: download_data() - elif choice == 'exit' or choice not in setup(): + elif choice == "exit" or choice not in setup(): print(str(choice) + ". Exiting...") else: results = run_benchmark(choice, docker) diff --git a/benchmark/benchmark_imports.py b/comparison-benchmark/python/benchmark_imports.py similarity index 98% rename from benchmark/benchmark_imports.py rename to comparison-benchmark/python/benchmark_imports.py index 8191457e3b..843052c688 100755 --- a/benchmark/benchmark_imports.py +++ b/comparison-benchmark/python/benchmark_imports.py @@ -8,42 +8,49 @@ try: from raphtory_bench import RaphtoryBench + RaphtoryBench = RaphtoryBench() except ImportError as e: pass try: from kuzu_bench import KuzuBench + KuzuBench = KuzuBench() except ImportError: pass try: from networkx_bench import NetworkXBench + NetworkXBench = NetworkXBench() except ImportError as e: pass try: from neo4j_bench import Neo4jBench + Neo4jBench = Neo4jBench() except ImportError: pass try: from graphtool_bench import GraphToolBench + GraphToolBench = GraphToolBench() except ImportError as e: pass try: from memgraph_bench import MemgraphBench + MemgraphBench = MemgraphBench() except ImportError as e: pass try: from cozo_bench import CozoDBBench + CozoDBBench = CozoDBBench() except ImportError: - pass \ No newline at end of file + pass diff --git a/benchmark/cozo_bench.py b/comparison-benchmark/python/cozo_bench.py similarity index 70% rename from benchmark/cozo_bench.py rename to comparison-benchmark/python/cozo_bench.py index c83ae9e19e..19d968d2ff 100755 --- a/benchmark/cozo_bench.py +++ b/comparison-benchmark/python/cozo_bench.py @@ -7,15 +7,16 @@ class CozoDBBench(BenchmarkBase): - def start_docker(self, **kwargs): - image_name = 'python:3.10-bullseye' - container_folder = '/app/data' + image_name = "python:3.10-bullseye" + container_folder = "/app/data" exec_commands = [ - 'pip install requests docker pycozo[embedded,pandas]', - '/bin/bash -c "cd /app/data;python benchmark_driver.py --no-docker --bench cozo"' + "pip install requests docker pycozo[embedded,pandas]", + '/bin/bash -c "cd /app/data;python benchmark_driver.py --no-docker --bench cozo"', ] - code, contents = super().start_docker(image_name, container_folder, exec_commands) + code, contents = super().start_docker( + image_name, container_folder, exec_commands + ) return code, contents def shutdown(self): @@ -33,7 +34,8 @@ def setup(self): self.client = Client() self.client.run("{:create user { code: Int }}") self.client.run("{:create friend { fr: Int, to: Int }}") - self.client.run(""" + self.client.run( + """ res[user] <~ CsvReader(types: ['Int'], url: 'file://./data/simple-profiles.csv', @@ -45,8 +47,10 @@ def setup(self): :replace user { code: Int } - """) - self.client.run(""" + """ + ) + self.client.run( + """ res[] <~ CsvReader(types: ['Int', 'Int'], url: 'file://./data/simple-relationships.csv', @@ -56,10 +60,13 @@ def setup(self): res[fr, to] :replace friend { fr: Int, to: Int } - """) + """ + ) def degree(self): - return self.client.run("?[user_id, total_degree, out_degree, in_degree] <~ DegreeCentrality(*friend[])") + return self.client.run( + "?[user_id, total_degree, out_degree, in_degree] <~ DegreeCentrality(*friend[])" + ) def out_neighbours(self): return self.degree() @@ -68,4 +75,6 @@ def page_rank(self): return self.client.run("?[user_id, page_rank] <~ PageRank(*friend[])") def connected_components(self): - return self.client.run("?[user_id, component] <~ ConnectedComponents(*friend[])") + return self.client.run( + "?[user_id, component] <~ ConnectedComponents(*friend[])" + ) diff --git a/benchmark/graphtool_bench.py b/comparison-benchmark/python/graphtool_bench.py similarity index 69% rename from benchmark/graphtool_bench.py rename to comparison-benchmark/python/graphtool_bench.py index de0b008a93..86e4ac7a8e 100755 --- a/benchmark/graphtool_bench.py +++ b/comparison-benchmark/python/graphtool_bench.py @@ -12,14 +12,16 @@ class GraphToolBench(BenchmarkBase): def start_docker(self, **kwargs): - image_name = 'tiagopeixoto/graph-tool:latest' - container_folder = '/app/data' + image_name = "tiagopeixoto/graph-tool:latest" + container_folder = "/app/data" exec_commands = [ - 'python -m ensurepip --upgrade', - 'python -m pip install requests tqdm docker pandas', - '/bin/bash -c "cd /app/data;python benchmark_driver.py --no-docker --bench gt"' + "python -m ensurepip --upgrade", + "python -m pip install requests tqdm docker pandas", + '/bin/bash -c "cd /app/data;python benchmark_driver.py --no-docker --bench gt"', ] - code, contents = super().start_docker(image_name, container_folder, exec_commands) + code, contents = super().start_docker( + image_name, container_folder, exec_commands + ) return code, contents def shutdown(self): @@ -34,13 +36,13 @@ def name(self): def setup(self): self.graph = gt.Graph() # with gzip.open(relationships_file, 'rt') as f: - with open(simple_relationship_file, 'r') as f: - reader = csv.reader(f, delimiter='\t') + with open(simple_relationship_file, "r") as f: + reader = csv.reader(f, delimiter="\t") for row in reader: # , total=30622564): self.graph.add_edge(int(row[0]), int(row[1])) def degree(self): - self.graph.degree_property_map('total').get_array() + self.graph.degree_property_map("total").get_array() def out_neighbours(self): [len(list(v.out_neighbours())) for v in self.graph.vertices()] diff --git a/benchmark/kuzu_bench.py b/comparison-benchmark/python/kuzu_bench.py similarity index 67% rename from benchmark/kuzu_bench.py rename to comparison-benchmark/python/kuzu_bench.py index cd42e435e4..45596b1468 100755 --- a/benchmark/kuzu_bench.py +++ b/comparison-benchmark/python/kuzu_bench.py @@ -1,4 +1,5 @@ from benchmark_base import BenchmarkBase + # Dont fail if not installed try: import kuzu @@ -7,15 +8,16 @@ class KuzuBench(BenchmarkBase): - def start_docker(self): - image_name = 'python:3.10-bullseye' - container_folder = '/app/data' + image_name = "python:3.10-bullseye" + container_folder = "/app/data" exec_commands = [ - 'pip install requests tqdm docker kuzu pandas numpy scipy', - '/bin/bash -c "cd /app/data;python benchmark_driver.py --no-docker --bench k"' + "pip install requests tqdm docker kuzu pandas numpy scipy", + '/bin/bash -c "cd /app/data;python benchmark_driver.py --no-docker --bench k"', ] - code, contents = super().start_docker(image_name, container_folder, exec_commands) + code, contents = super().start_docker( + image_name, container_folder, exec_commands + ) return code, contents def shutdown(self): @@ -33,7 +35,7 @@ def run_query(self, query): return res def setup(self): - self.db = kuzu.Database('/tmp/testdb') + self.db = kuzu.Database("/tmp/testdb") self.conn = kuzu.Connection(self.db) self.run_query("CREATE NODE TABLE User(id INT64, PRIMARY KEY (id))") self.run_query("CREATE REL TABLE Follows(FROM User TO User)") @@ -41,14 +43,15 @@ def setup(self): self.run_query('COPY Follows FROM "data/simple-relationships.csv" (DELIM="\t")') def degree(self): - res = self.run_query('MATCH (a:User)-[f:Follows]->(b:User) RETURN a.id,COUNT(f)') + res = self.run_query( + "MATCH (a:User)-[f:Follows]->(b:User) RETURN a.id,COUNT(f)" + ) df = res.get_as_df() def out_neighbours(self): - self.conn.set_query_timeout(300000) # 300 seconds - res = self.run_query('MATCH (u:User)-[:Follows]->(n)' - 'RETURN COUNT(n.id)') - # 'RETURN u.id, COLLECT(n.id) AS out_neighbours') + self.conn.set_query_timeout(300000) # 300 seconds + res = self.run_query("MATCH (u:User)-[:Follows]->(n)" "RETURN COUNT(n.id)") + # 'RETURN u.id, COLLECT(n.id) AS out_neighbours') df = res.get_as_df() def page_rank(self): diff --git a/benchmark/memgraph_bench.py b/comparison-benchmark/python/memgraph_bench.py similarity index 77% rename from benchmark/memgraph_bench.py rename to comparison-benchmark/python/memgraph_bench.py index 6a90d25bc2..d065d452fa 100755 --- a/benchmark/memgraph_bench.py +++ b/comparison-benchmark/python/memgraph_bench.py @@ -12,13 +12,13 @@ class MemgraphBench(BenchmarkBase): def start_docker(self, **kwargs): - image_name = 'memgraph/memgraph-platform:latest' - container_folder = '/app/data' + image_name = "memgraph/memgraph-platform:latest" + container_folder = "/app/data" exec_commands = [ '/bin/bash -c "apt update && apt install -y libssl-dev"', '/bin/bash -c "python3 -m pip install gqlalchemy requests tqdm docker pandas"', '/bin/bash -c "cp -R /app/data/data /tmp/;chmod 777 -R /tmp/data"', - '/bin/bash -c "cd /app/data;python3 benchmark_driver.py --no-docker --bench mem"' + '/bin/bash -c "cd /app/data;python3 benchmark_driver.py --no-docker --bench mem"', ] # ports = { # '7444': '7444', @@ -30,7 +30,6 @@ def start_docker(self, **kwargs): container_folder=container_folder, exec_commands=exec_commands, # ports=ports, - ) return code, contents @@ -45,19 +44,23 @@ def __init__(self): def import_data(self): print("loading nodes") - query = 'LOAD CSV FROM "/tmp/data/simple-profiles.csv" NO HEADER DELIMITER "\t" AS row '\ - 'CREATE (n:Node {id: row[0]});' + query = ( + 'LOAD CSV FROM "/tmp/data/simple-profiles.csv" NO HEADER DELIMITER "\t" AS row ' + "CREATE (n:Node {id: row[0]});" + ) self.graph.execute(query) print("Creating index") - query = 'CREATE INDEX ON :Node(id);' + query = "CREATE INDEX ON :Node(id);" self.graph.execute(query) print("loading relationships") - query = 'LOAD CSV FROM "/tmp/data/simple-relationships.csv" NO HEADER DELIMITER "\t" AS row ' \ - 'MATCH (n1:Node {id: row[0]}), (n2:Node {id: row[1]}) CREATE (n1)-[:FOLLOWS]->(n2);' + query = ( + 'LOAD CSV FROM "/tmp/data/simple-relationships.csv" NO HEADER DELIMITER "\t" AS row ' + "MATCH (n1:Node {id: row[0]}), (n2:Node {id: row[1]}) CREATE (n1)-[:FOLLOWS]->(n2);" + ) self.graph.execute(query) def setup(self): - self.graph = Memgraph(host='127.0.0.1', port=7687) + self.graph = Memgraph(host="127.0.0.1", port=7687) # query = "MATCH (n) DETACH DELETE n" # self.graph.execute(query) self.import_data() diff --git a/benchmark/neo4j_bench.py b/comparison-benchmark/python/neo4j_bench.py similarity index 60% rename from benchmark/neo4j_bench.py rename to comparison-benchmark/python/neo4j_bench.py index e99f5ee6e9..3570bddf8a 100755 --- a/benchmark/neo4j_bench.py +++ b/comparison-benchmark/python/neo4j_bench.py @@ -14,28 +14,34 @@ def create_graph_projection(tx): - tx.run(""" + tx.run( + """ CALL gds.graph.project.cypher( 'social', 'MATCH (n) RETURN id(n) AS id', 'MATCH (n)-[r:FOLLOWS]->(m) RETURN id(n) AS source, id(m) AS target') YIELD graphName AS graph, nodeQuery, nodeCount AS nodes, relationshipQuery, relationshipCount AS rels - """) + """ + ) def query_degree(tx): - result = tx.run(""" + result = tx.run( + """ MATCH p=(n)-[r:FOLLOWS]->() RETURN n.id, COUNT(p) - """) + """ + ) return list(result) def get_out_neighbors(tx): - result = tx.run(""" + result = tx.run( + """ MATCH p=(n)-[:FOLLOWS]->(neighbor) RETURN n.id, COUNT(p) - """) + """ + ) return list(result) @@ -45,58 +51,77 @@ def run_pagerank(tx): def run_connected_components(tx): - result = tx.run(""" + result = tx.run( + """ CALL gds.wcc.stream("social") - """) + """ + ) return list(result) def execute_bash_command(command, background=False): print("Executing command: ", command) if background: - subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + subprocess.Popen( + command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) return - process = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + process = subprocess.Popen( + command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) stdout, stderr = process.communicate() - return stdout.decode('utf-8'), stderr.decode('utf-8') + return stdout.decode("utf-8"), stderr.decode("utf-8") def write_array_to_csv(arr, file_path): - with open(file_path, 'w', newline='') as csv_file: - writer = csv.writer(csv_file, delimiter='\t') + with open(file_path, "w", newline="") as csv_file: + writer = csv.writer(csv_file, delimiter="\t") writer.writerows(arr) def modify_data(): print("Generating data...") - file_dir = os.path.abspath(os.getcwd()) + '/data/' + file_dir = os.path.abspath(os.getcwd()) + "/data/" print("File dir: ", file_dir) - if 'simple-profiles-header-neo4j.csv' not in os.listdir(file_dir): + if "simple-profiles-header-neo4j.csv" not in os.listdir(file_dir): print("Generating node header") - write_array_to_csv([['node:ID', 'name']], file_dir + 'simple-profiles-header-neo4j.csv') + write_array_to_csv( + [["node:ID", "name"]], file_dir + "simple-profiles-header-neo4j.csv" + ) print("Generating relationship header") - write_array_to_csv([['node:START_ID', 'node:END_ID', ':TYPE']], - file_dir + 'simple-relationships-headers-neo4j.csv') + write_array_to_csv( + [["node:START_ID", "node:END_ID", ":TYPE"]], + file_dir + "simple-relationships-headers-neo4j.csv", + ) print("Generating node data") - df = pd.read_csv(file_dir + 'simple-profiles.csv', sep='\t', header=None) - df['copy'] = df[0].copy() - df.to_csv(file_dir + 'simple-profiles-neo4j.csv', index=None, header=None, sep='\t') + df = pd.read_csv(file_dir + "simple-profiles.csv", sep="\t", header=None) + df["copy"] = df[0].copy() + df.to_csv( + file_dir + "simple-profiles-neo4j.csv", index=None, header=None, sep="\t" + ) print("Generating relationship data") - df = pd.read_csv(file_dir + 'simple-relationships.csv', sep='\t', header=None) - df['type'] = 'FOLLOWS' - df.to_csv(file_dir + 'simple-relationships-neo4j.csv', sep='\t', index=None, header=None) + df = pd.read_csv(file_dir + "simple-relationships.csv", sep="\t", header=None) + df["type"] = "FOLLOWS" + df.to_csv( + file_dir + "simple-relationships-neo4j.csv", + sep="\t", + index=None, + header=None, + ) print("Done") def import_data(): - return execute_bash_command("neo4j-admin database import full --overwrite-destination --delimiter='TAB' " - "--nodes=/var/lib/neo4j/import/data2/data/simple-profiles-header-neo4j.csv," - "/var/lib/neo4j/import/data2/data/simple-profiles-neo4j.csv " - "--relationships=/var/lib/neo4j/import/data2/data/simple-relationships-headers-neo4j.csv," - "/var/lib/neo4j/import/data2/data/simple-relationships-neo4j.csv neo4j") + return execute_bash_command( + "neo4j-admin database import full --overwrite-destination --delimiter='TAB' " + "--nodes=/var/lib/neo4j/import/data2/data/simple-profiles-header-neo4j.csv," + "/var/lib/neo4j/import/data2/data/simple-profiles-neo4j.csv " + "--relationships=/var/lib/neo4j/import/data2/data/simple-relationships-headers-neo4j.csv," + "/var/lib/neo4j/import/data2/data/simple-relationships-neo4j.csv neo4j" + ) # tx.run(""" # LOAD CSV FROM 'file:///data2/data/simple-relationships.csv' AS row # FIELDTERMINATOR '\t' @@ -110,16 +135,13 @@ def import_data(): class Neo4jBench(BenchmarkBase): def start_docker(self, **kwargs): modify_data() - image_name = 'neo4j:5.8.0' - container_folder = '/var/lib/neo4j/import/data2/' + image_name = "neo4j:5.8.0" + container_folder = "/var/lib/neo4j/import/data2/" envs = { - 'NEO4J_AUTH': 'neo4j/password', - 'NEO4J_PLUGINS': '["graph-data-science"]' - } - ports = { - '7474': '7474', - '7687': '7687' + "NEO4J_AUTH": "neo4j/password", + "NEO4J_PLUGINS": '["graph-data-science"]', } + ports = {"7474": "7474", "7687": "7687"} exec_commands = [ '/bin/bash -c "apt update && apt install python3-pip -y"', '/bin/bash -c "python3 -m pip install neo4j requests tqdm pandas numpy docker"', @@ -128,9 +150,15 @@ def start_docker(self, **kwargs): '/bin/bash -c "cd /var/lib/neo4j/import/data2/; python3 benchmark_driver.py --no-docker --bench neo"', ] # image_path = 'DockerFiles/pyneo' image_path ports - code, contents = super().start_docker(image_name=image_name, container_folder=container_folder, - exec_commands=exec_commands, envs=envs, wait=35, ports=ports, - start_cmd='tail -f /dev/null') + code, contents = super().start_docker( + image_name=image_name, + container_folder=container_folder, + exec_commands=exec_commands, + envs=envs, + wait=35, + ports=ports, + start_cmd="tail -f /dev/null", + ) return code, contents def shutdown(self): @@ -139,7 +167,6 @@ def shutdown(self): def __init__(self): self.driver = None - def name(self): return "Neo4j" @@ -154,9 +181,12 @@ def setup(self): print("status: ", stout) print("error: ", sterr) print("Starting neo4j") - execute_bash_command('export NEO4J_AUTH="neo4j/password"; export NEO4J_PLUGINS=\'[' - '"graph-data-science"]\';/bin/bash -c "tini -s -g -- ' - '/startup/docker-entrypoint.sh neo4j start &"', background=True) + execute_bash_command( + 'export NEO4J_AUTH="neo4j/password"; export NEO4J_PLUGINS=\'[' + '"graph-data-science"]\';/bin/bash -c "tini -s -g -- ' + '/startup/docker-entrypoint.sh neo4j start &"', + background=True, + ) print("Sleeping for 50 seconds...") time.sleep(50) # print("Updating password") diff --git a/benchmark/networkx_bench.py b/comparison-benchmark/python/networkx_bench.py similarity index 68% rename from benchmark/networkx_bench.py rename to comparison-benchmark/python/networkx_bench.py index 03aede1713..28b9894ee8 100755 --- a/benchmark/networkx_bench.py +++ b/comparison-benchmark/python/networkx_bench.py @@ -1,5 +1,6 @@ from benchmark_base import BenchmarkBase import csv + # Dont fail if not imported locally try: import networkx as nx @@ -12,13 +13,15 @@ class NetworkXBench(BenchmarkBase): def start_docker(self, **kwargs): - image_name = 'python:3.10-bullseye' - container_folder = '/app/data' + image_name = "python:3.10-bullseye" + container_folder = "/app/data" exec_commands = [ - 'pip install requests tqdm docker networkx pandas numpy scipy', - '/bin/bash -c "cd /app/data;python benchmark_driver.py --no-docker --bench nx"' + "pip install requests tqdm docker networkx pandas numpy scipy", + '/bin/bash -c "cd /app/data;python benchmark_driver.py --no-docker --bench nx"', ] - code, contents = super().start_docker(image_name, container_folder, exec_commands) + code, contents = super().start_docker( + image_name, container_folder, exec_commands + ) return code, contents def shutdown(self): @@ -33,8 +36,8 @@ def name(self): def setup(self): self.graph = nx.DiGraph() - with open(simple_relationship_file, 'r') as f: - reader = csv.reader(f, delimiter='\t') + with open(simple_relationship_file, "r") as f: + reader = csv.reader(f, delimiter="\t") for row in reader: self.graph.add_edge(int(row[0]), int(row[1])) @@ -52,4 +55,6 @@ def page_rank(self): return nx.pagerank(self.graph) def connected_components(self): - return [len(comp) for comp in nx.connected_components(self.graph.to_undirected())] + return [ + len(comp) for comp in nx.connected_components(self.graph.to_undirected()) + ] diff --git a/benchmark/profile_bench.py b/comparison-benchmark/python/profile_bench.py similarity index 91% rename from benchmark/profile_bench.py rename to comparison-benchmark/python/profile_bench.py index 3ceb85c2ac..09e32a13a1 100755 --- a/benchmark/profile_bench.py +++ b/comparison-benchmark/python/profile_bench.py @@ -28,13 +28,13 @@ def setup(): 3: KuzuBench(), 4: NetworkXBench(), 5: Neo4jBench(), - 6: MemgraphBench() + 6: MemgraphBench(), } def run_benchmark(choice): driver = setup()[choice] - fns = ['setup', 'degree', 'out_neighbours', 'page_rank', 'connected_components'] + fns = ["setup", "degree", "out_neighbours", "page_rank", "connected_components"] for fn in fns: print("** Running " + fn + "...") start_time = time.time() diff --git a/benchmark/raphtory_bench.py b/comparison-benchmark/python/raphtory_bench.py similarity index 66% rename from benchmark/raphtory_bench.py rename to comparison-benchmark/python/raphtory_bench.py index 462dec9906..180eafa9dd 100755 --- a/benchmark/raphtory_bench.py +++ b/comparison-benchmark/python/raphtory_bench.py @@ -7,22 +7,28 @@ import raphtory from tqdm import tqdm from raphtory.algorithms import pagerank, weakly_connected_components -except ImportError: - pass +except ImportError as e: + print("IMPORT ERROR") + print(e) + print("Cannot continue. Exiting") + import sys + + sys.exit(1) simple_relationship_file = "data/simple-relationships.csv" class RaphtoryBench(BenchmarkBase): - def start_docker(self, **kwargs): - image_name = 'python:3.10-bullseye' - container_folder = '/app/data' + image_name = "python:3.10-bullseye" + container_folder = "/app/data" exec_commands = [ - 'pip install raphtory requests tqdm pandas numpy docker', - '/bin/bash -c "cd /app/data;python benchmark_driver.py --no-docker --bench r"' + "pip install raphtory requests tqdm pandas numpy docker", + '/bin/bash -c "cd /app/data;python benchmark_driver.py --no-docker --bench r"', ] - code, contents = super().start_docker(image_name, container_folder, exec_commands) + code, contents = super().start_docker( + image_name, container_folder, exec_commands + ) return code, contents def shutdown(self): @@ -36,9 +42,9 @@ def __init__(self): def setup(self): # Load edges - self.graph = raphtory.Graph(multiprocessing.cpu_count()) - with open(simple_relationship_file, 'r') as f: - reader = csv.reader(f, delimiter='\t') + self.graph = raphtory.Graph() + with open(simple_relationship_file, "r") as f: + reader = csv.reader(f, delimiter="\t") for row in reader: self.graph.add_edge(1, row[0], row[1], {}) diff --git a/benchmark/readme.md b/comparison-benchmark/readme.md similarity index 70% rename from benchmark/readme.md rename to comparison-benchmark/readme.md index 0c3ad2a36f..c4b9437518 100755 --- a/benchmark/readme.md +++ b/comparison-benchmark/readme.md @@ -3,6 +3,66 @@ This is the raphtory benchmarking suite. It is designed to test the performance of raphtory against other graph processing systems. +There are two benchmarks suites, one for python with support for multiple systems, and one for rust with support for + raphtory only. + + +## Rust Suite + +This only does a light benchmark of raphtory, as it is designed to be used standalone. + + Raphtory Quick Benchmark + + Usage: raphtory-rust-benchmark [OPTIONS] + + Options: + --header Set if the file has a header, default is False + --delimiter Delimiter of the csv file [default: "\t"] + --file-path Path to a csv file [default: ] + --from-column Position of the from column in the csv [default: 0] + --to-column Position of the to column in the csv [default: 1] + --time-column Position of the time column in the csv, default will ignore time [default: -1] + --download Download default files + --debug Debug to print more info to the screen + -h, --help Print help + -V, --version Print version + + +First download the example file by cd'ing into the `raphtory-rust-benchmark` folder and running + + cargo run -- --download + +This will download the example file into tmp folder on your system, it will give you the file path. + +You can then run the benchmark by running, with the file path it has given you + + cargo run -- --file-path + +You can also provide your own file path, but please ensure you have set the correct arguments. +I.e Whether it has a header, what the delimiter is, and what columns are what. + +e.g. + + cargo run -- --file-path="/Users/1337/Documents/dev/Data/lotr.csv" --delimiter="," --from-column=0 --to-column=1 + +The results for a 1000 edge file are below + + Raphtory Quick Benchmark + Running setup... + Setup took 0.015264357 seconds + Graph has 864 vertices and 1000 edges + Degree: 0.001719875 seconds + Out neighbours: 0.000247832 seconds + Page rank: 0.012001127 seconds + Connected components: 0.025755603 seconds + + + +## Python Suite + +This benchmarks the python version of raphtory. +Please ensure your python environment has raphtory installed. + Systems currently supported are: - [raphtory](https://github.com/Pometry/Raphtory) - [neo4j](https://neo4j.com/) @@ -29,7 +89,6 @@ More information available [here](https://snap.stanford.edu/data/soc-pokec.html) - pandas - raphtory - neo4j - - # How to run @@ -135,4 +194,4 @@ Some key notes: https://memgraph.com/docs/memgraph/import-data/load-csv-clause#one-type-of-nodes-and-relationships - Cozo - - Triggered a segmentation fault when running the connected components algorithm \ No newline at end of file + - Triggered a segmentation fault when running the connected components algorithm diff --git a/raphtory-io/Cargo.toml b/comparison-benchmark/rust/raphtory-rust-benchmark/Cargo.toml similarity index 50% rename from raphtory-io/Cargo.toml rename to comparison-benchmark/rust/raphtory-rust-benchmark/Cargo.toml index e9bbe9375a..83d0bff70a 100644 --- a/raphtory-io/Cargo.toml +++ b/comparison-benchmark/rust/raphtory-rust-benchmark/Cargo.toml @@ -1,6 +1,6 @@ [package] -name = "raphtory-io" -description = "raphtory-io, contains all connectors and example datasets for raphtory" +name = "raphtory-rust-benchmark" +description = "Raphtory Quick Rust Benchmark" edition.workspace = true rust-version.workspace = true version.workspace = true @@ -15,20 +15,11 @@ homepage.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -raphtory = { path = "../raphtory", version = "0.4.0" } -reqwest = { version = "0.11.14", features = ["blocking"] } -neo4rs = "0.6.0" -csv = "1.1.6" +raphtory = { path = "../../../raphtory", version = "0.5.7", package = "raphtory", features = ["io"] } +chrono = { version = "0.4", features = ["serde"] } serde = { version = "1", features = ["derive","rc"] } -serde_json = "1" -rayon = "1" -regex = "1" -chrono = "0.4" -itertools="0.10" - -zip = "0.5" -bzip2 = "0.4" -flate2 = "1.0" - -[dev-dependencies] -tokio = { version = "1.27.0", features = ["full"] } +clap = { version = "4.3.11", features = ["derive"] } +csv = "1.2.2" +tar = "0.4.38" +flate2 = "1.0.26" +ordered-float = "3.7.0" diff --git a/comparison-benchmark/rust/raphtory-rust-benchmark/src/main.rs b/comparison-benchmark/rust/raphtory-rust-benchmark/src/main.rs new file mode 100644 index 0000000000..7b8616eed8 --- /dev/null +++ b/comparison-benchmark/rust/raphtory-rust-benchmark/src/main.rs @@ -0,0 +1,212 @@ +use chrono::NaiveDateTime; +use clap::{ArgAction, Parser}; +use csv::StringRecord; +use flate2::read::GzDecoder; +use raphtory::{ + algorithms::{ + algorithm_result::AlgorithmResult, connected_components::weakly_connected_components, + pagerank::unweighted_page_rank, + }, + graph_loader::{fetch_file, source::csv_loader::CsvLoader}, + prelude::{AdditionOps, Graph, GraphViewOps, VertexViewOps, NO_PROPS}, +}; +use std::{ + fs::File, + io::{self, Read, Write}, + path::Path, + time::Instant, +}; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None )] +struct Args { + /// Set if the file has a header, default is False + #[arg(long, action=ArgAction::SetTrue)] + header: bool, + + /// Delimiter of the csv file + #[arg(long, default_value = "\t")] + delimiter: String, + + /// Path to a csv file + #[arg(long, default_value = "")] + file_path: String, + + /// Position of the from column in the csv + #[arg(long, default_value = "0")] + from_column: usize, + + /// Position of the to column in the csv + #[arg(long, default_value = "1")] + to_column: usize, + + /// Position of the time column in the csv, Expected time is in unix ms, default will ignore time and set it to 1 + #[arg(long, default_value = "-1")] + time_column: i32, + + /// Download default files + #[arg(long, action=ArgAction::SetTrue)] + download: bool, + + /// Debug to print more info to the screen + #[arg(long, action=ArgAction::SetTrue)] + debug: bool, +} + +fn main() { + println!( + " +██████╗ ███████╗███╗ ██╗ ██████╗██╗ ██╗███╗ ███╗ █████╗ ██████╗ ██╗ ██╗ +██╔══██╗██╔════╝████╗ ██║██╔════╝██║ ██║████╗ ████║██╔══██╗██╔══██╗██║ ██╔╝ +██████╔╝█████╗ ██╔██╗ ██║██║ ███████║██╔████╔██║███████║██████╔╝█████╔╝ +██╔══██╗██╔══╝ ██║╚██╗██║██║ ██╔══██║██║╚██╔╝██║██╔══██║██╔══██╗██╔═██╗ +██████╔╝███████╗██║ ╚████║╚██████╗██║ ██║██║ ╚═╝ ██║██║ ██║██║ ██║██║ ██╗ +╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═╝ +" + ); + let args = Args::parse(); + // Set default values + let debug = args.debug; + if debug { + println!( + " + .___ .____ ____ . . ___ __ __ ___ .___ .____ + / ` / / \\ / / .' \\ | | .' `. / ` / + | | |__. |,_-< | | | |\\ /| | | | | |__. + | | | | ` | | | _ | \\/ | | | | | | + /---/ /----/ `----' `._.' `.___| / / `.__.' /---/ /----/ + " + ); + println!("Debug mode enabled.\nArguments: {:?}", args); + } + let header = args.header; + let delimiter = args.delimiter; + let file_path = args.file_path; + let from_column = args.from_column; + let to_column = args.to_column; + let time_column = args.time_column; + let download = args.download; + + if download { + let url = "https://osf.io/download/nbq6h/"; + println!("Downloading default file from url {}...", url); + // make err msg from url and custom string + let err_msg = format!("Failed to download file from {}", url); + let path = fetch_file("simple-relationships.csv.gz", true, url, 1200).expect(&err_msg); + println!("Downloaded file to {}", path.to_str().unwrap()); + println!("Unpacking file..."); + // extract a file from a gz archive + // Open the input .csv.gz file + let input_file = File::open(&path).expect("Failed to open downloaded file"); + let gz_decoder = GzDecoder::new(input_file); + let path_str = path.to_str().unwrap(); + let dst_file = if path_str.len() >= 3 { + let new_length = path_str.len() - 3; + path_str[..new_length].to_owned() + } else { + path_str.to_owned() + }; + let mut output_file = File::create(&dst_file) + .expect("Failed to create new file to decompress downloaded data"); + // Decompress and write the content to the output file + let mut buffer = vec![0; 4096]; + let mut gz_reader = io::BufReader::new(gz_decoder); + loop { + let bytes_read = gz_reader.read(&mut buffer).unwrap_or(0); + if bytes_read == 0 { + break; + } + output_file.write_all(&buffer[..bytes_read]).unwrap(); + } + + // exit program + println!("Downloaded+Unpacked file, please run again without --download flag and with --file-path={}", dst_file); + return; + } + + if file_path.is_empty() { + println!("You did not set a file path"); + return; + } + if !Path::new(&file_path).exists() { + println!("File path does not exist or is not a file {}", &file_path); + return; + } + + if debug { + println!("Reading file {}", &file_path); + } + + println!("Running setup..."); + let mut now = Instant::now(); + // Iterate over the CSV records + let g = { + let g = Graph::new(); + CsvLoader::new(file_path) + .set_header(header) + .set_delimiter(&delimiter) + .load_rec_into_graph(&g, |generic_loader: StringRecord, g: &Graph| { + let src_id = generic_loader + .get(from_column) + .map(|s| s.to_owned()) + .unwrap(); + let dst_id = generic_loader.get(to_column).map(|s| s.to_owned()).unwrap(); + let mut edge_time = NaiveDateTime::from_timestamp_opt(1, 0).unwrap(); + if time_column != -1 { + edge_time = NaiveDateTime::from_timestamp_millis( + generic_loader + .get(time_column as usize) + .unwrap() + .parse() + .unwrap(), + ) + .unwrap(); + } + if debug { + println!("Adding edge {} -> {} at time {}", src_id, dst_id, edge_time); + } + g.add_edge(edge_time, src_id, dst_id, NO_PROPS, None) + .expect("Failed to add edge"); + }) + .expect("Failed to load graph from CSV data files"); + g + }; + println!("Setup took {} seconds", now.elapsed().as_secs_f64()); + + if debug { + println!( + "Graph has {} vertices and {} edges", + g.count_vertices(), + g.count_edges() + ) + } + + // Degree of all nodes + now = Instant::now(); + let _degree = g.vertices().iter().map(|v| v.degree()).collect::>(); + println!("Degree: {} seconds", now.elapsed().as_secs_f64()); + + // Out neighbours of all nodes with time + now = Instant::now(); + let _out_neighbours = g + .vertices() + .iter() + .map(|v| v.out_neighbours()) + .collect::>(); + println!("Out neighbours: {} seconds", now.elapsed().as_secs_f64()); + + // page rank with time + now = Instant::now(); + let _page_rank: Vec<_> = unweighted_page_rank(&g, 1000, None, None, true) + .into_iter() + .collect(); + println!("Page rank: {} seconds", now.elapsed().as_secs_f64()); + + // connected components with time + now = Instant::now(); + let _cc: AlgorithmResult = weakly_connected_components(&g, usize::MAX, None); + println!( + "Connected components: {} seconds", + now.elapsed().as_secs_f64() + ); +} diff --git a/dev/bootstrap.sh b/dev/bootstrap.sh new file mode 100755 index 0000000000..afb7121944 --- /dev/null +++ b/dev/bootstrap.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +cd "$DIR/.." + +log() { + echo "$(basename ${BASH_SOURCE[0]}): $@" +} + +install_hooks() { + git config core.hooksPath \ + || git config core.hooksPath ./dev/hooks +} + +install_rust_nightly() { + rustup toolchain install nightly +} + +log 'installing rust nightly...' +install_rust_nightly +log 'configuring hooks...' +install_hooks \ No newline at end of file diff --git a/dev/hooks/pre-commit b/dev/hooks/pre-commit new file mode 100755 index 0000000000..3731928a78 --- /dev/null +++ b/dev/hooks/pre-commit @@ -0,0 +1,18 @@ +#!/bin/bash + +# Run rustfmt on the whole repository using nightly toolchain +cargo +nightly fmt --all -- --check + +# Capture the exit code of the previous command +RESULT=$? + +# If the result is non-zero (i.e., there were formatting errors), abort the commit +if [ $RESULT -ne 0 ]; then + if [ -z "$(git ls-files --others --modified --exclude-standard)" ]; then + echo "There are formatting errors. Running cargo fmt. Please check the formatting changes and add them before committing." + cargo +nightly fmt --all + else + echo "There are formatting errors and un-staged files. Please run 'cargo +nightly fmt --all' to fix the formatting before committing." + fi + exit 1 +fi diff --git a/dev/readme.md b/dev/readme.md new file mode 100644 index 0000000000..93961ea3b0 --- /dev/null +++ b/dev/readme.md @@ -0,0 +1,40 @@ +# Quick start dev setup + +If you run `./dev/bootstrap.sh` it will setup the environment for you. + +In this case it will + +- Install a rust nightly +- setup your git hooks to point to the dev hooks in `./dev/hooks` + +You need to run this only once, you will have to `chmod +x ./dev/bootstrap.sh` first. + +## how to run + +From the raphtory root folder + + chmod +x ./dev/bootstrap.sh + ./dev/bootstrap.sh + +## dev hooks + +### pre-commit hook + +The pre-commit hook will run `cargo fmt` on all staged files. If the formatting fails, the commit will be aborted. + + + #!/bin/bash + + # Run rustfmt on the whole repository using nightly toolchain + cargo +nightly fmt --all -- --check + + # Capture the exit code of the previous command + RESULT=$? + + # If the result is non-zero (i.e., there were formatting errors), abort the commit + if [ $RESULT -ne 0 ]; then + echo "There are formatting errors. Please run 'cargo +nightly fmt' and fix them before committing." + exit 1 + fi + + diff --git a/docs/requirements.txt b/docs/requirements.txt index 4335d9d446..3f33f163cb 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -3,56 +3,58 @@ alabaster==0.7.13 appnope==0.1.3 asttokens==2.2.1 attrs==23.1.0 +autodocsumm==0.2.11 Babel==2.12.1 backcall==0.2.0 beautifulsoup4==4.12.2 bleach==6.0.0 -certifi==2023.5.7 +certifi==2023.7.22 charset-normalizer==3.1.0 -contourpy==1.0.7 +contourpy==1.1.0 cycler==0.11.0 decorator==5.1.1 defusedxml==0.7.1 docutils==0.19 executing==1.2.0 fastjsonschema==2.17.1 -fonttools==4.39.4 +fonttools==4.40.0 idna==3.4 imagesize==1.4.1 -ipython==8.13.2 +ipython==8.14.0 jedi==0.18.2 Jinja2==3.1.2 jsonpickle==3.0.1 jsonschema==4.17.3 -jupyter_client==8.2.0 -jupyter_core==5.3.0 +jupyter_client==8.3.0 +jupyter_core==5.3.1 jupyterlab-pygments==0.2.2 kiwisolver==1.4.4 -MarkupSafe==2.1.2 +MarkupSafe==2.1.3 matplotlib==3.7.1 matplotlib-inline==0.1.6 -mistune==2.0.5 +maturin==1.1.0 +mistune==3.0.1 nbclient==0.8.0 -nbconvert==7.4.0 -nbformat==5.8.0 +nbconvert==7.6.0 +nbformat==5.9.0 nbsphinx==0.9.2 networkx==3.1 -numpy==1.24.3 +numpy==1.25.0 numpydoc==1.5.0 packaging==23.1 -pandas==2.0.2 +pandas==2.0.3 pandocfilters==1.5.0 parso==0.8.3 pexpect==4.8.0 pickleshare==0.7.5 -Pillow==9.5.0 -platformdirs==3.5.1 -prompt-toolkit==3.0.38 +Pillow==10.0.0 +platformdirs==3.8.0 +prompt-toolkit==3.0.39 ptyprocess==0.7.0 pure-eval==0.2.2 pydata-sphinx-theme==0.13.3 Pygments==2.15.1 -pyparsing==3.0.9 +pyparsing==3.1.0 pyrsistent==0.19.3 python-dateutil==2.8.2 pytz==2023.3 @@ -62,7 +64,7 @@ requests==2.31.0 six==1.16.0 snowballstemmer==2.2.0 soupsieve==2.4.1 -Sphinx==6.2.1 +sphinx==6.2.1 sphinx-copybutton==0.5.2 sphinx-toggleprompt==0.4.0 sphinx_design==0.4.1 @@ -74,10 +76,11 @@ sphinxcontrib-qthelp==1.0.3 sphinxcontrib-serializinghtml==1.1.5 stack-data==0.6.2 tinycss2==1.2.1 -tornado==6.3.2 +tomli==2.0.1 +tornado==6.3.3 traitlets==5.9.0 -typing_extensions==4.6.2 +typing_extensions==4.7.1 tzdata==2023.3 -urllib3==2.0.2 +urllib3==2.0.3 wcwidth==0.2.6 webencodings==0.5.1 diff --git a/docs/source/_static/css/getting_started.css b/docs/source/_static/css/getting_started.css index 5537711779..c5397780b7 100644 --- a/docs/source/_static/css/getting_started.css +++ b/docs/source/_static/css/getting_started.css @@ -146,13 +146,13 @@ ul.task-bullet > li > p:first-child { } .comparison-card .sd-btn-secondary { - background-color: #6c757d !important; - border-color: #6c757d !important; + background-color: #AE0D22 !important; + border-color: #AE0D22 !important; } .comparison-card .sd-btn-secondary:hover { - background-color: #5a6268 !important; - border-color: #545b62 !important; + background-color: #AE0D22 !important; + border-color: #AE0D22 !important; } .comparison-card .card-footer { diff --git a/docs/source/_static/css/raphtory.css b/docs/source/_static/css/raphtory.css index f48a17e899..d60d948f58 100644 --- a/docs/source/_static/css/raphtory.css +++ b/docs/source/_static/css/raphtory.css @@ -38,13 +38,13 @@ table { } .intro-card .sd-btn-secondary { - background-color: #CC0808 !important; - border-color: #CC0808 !important; + background-color: #AE0D22 !important; + border-color: #AE0D22 !important; } .intro-card .sd-btn-secondary:hover { - background-color: #E0311D !important; - border-color: #E0311D !important; + background-color: #AE0D22 !important; + border-color: #AE0D22 !important; } .card, .card img { @@ -56,19 +56,19 @@ table { } h1, h2 { - color: #f04040; + color: #AE0D22; } html[data-theme="light"] { - --pst-color-primary: #f04040; + --pst-color-primary: #AE0D22; } html[data-theme="dark"] { - --pst-color-primary: #f04040; + --pst-color-primary: #AE0D22; } :root { - --pst-color-primary: #f04040; + --pst-color-primary: #AE0D22; --pst-color-link: #eb6060; - --pst-color-primary-light: #f04040; + --pst-color-primary-light: #AE0D22; } \ No newline at end of file diff --git a/docs/source/_static/logo.svg b/docs/source/_static/logo.svg new file mode 100644 index 0000000000..92ae28a812 --- /dev/null +++ b/docs/source/_static/logo.svg @@ -0,0 +1,155 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/source/_static/rust-crate.jpg b/docs/source/_static/rust-crate.jpg new file mode 100644 index 0000000000..693436767a Binary files /dev/null and b/docs/source/_static/rust-crate.jpg differ diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst deleted file mode 100644 index 38936f4510..0000000000 --- a/docs/source/api/index.rst +++ /dev/null @@ -1,16 +0,0 @@ -{{ header }} - -.. _api: - -============= -API -============= - -.. toctree:: - :maxdepth: 2 - - - raphtory - rust - - diff --git a/docs/source/api/raphtory.rst b/docs/source/api/raphtory.rst deleted file mode 100644 index 311937c49a..0000000000 --- a/docs/source/api/raphtory.rst +++ /dev/null @@ -1,67 +0,0 @@ -.. _overview: - -{{ header }} - -**************** -Raphtory -**************** - -.. automodule:: raphtory - :members: - :undoc-members: - :show-inheritance: - :private-members: - :inherited-members: - -raphtory.algorithms -------------------- - -.. automodule:: raphtory.algorithms - :members: - :undoc-members: - :show-inheritance: - :private-members: - :inherited-members: - - -raphtory.vis -------------------- - -.. automodule:: raphtory.vis - :members: - :undoc-members: - :show-inheritance: - :private-members: - :inherited-members: - - -raphtory.nullmodels --------------------------- - -.. automodule:: raphtory.nullmodels - :members: - :undoc-members: - :show-inheritance: - :private-members: - :inherited-members: - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/source/api/rust.rst b/docs/source/api/rust.rst deleted file mode 100644 index cd796635f2..0000000000 --- a/docs/source/api/rust.rst +++ /dev/null @@ -1,4 +0,0 @@ -Rust -================ - -Our rust module is hosted on docs.rs, and can be found `here `__. \ No newline at end of file diff --git a/docs/source/conf.py b/docs/source/conf.py index f47f661b71..aa51d6f068 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,13 +1,24 @@ +# do not add from __future__ import annotations as it will break the typehints parsing +# (I think this is a bug in autodoc and may be fixed at some point) + # Configuration file for the Sphinx documentation builder. # # For the full list of built-in configuration values, see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html + from sphinx.ext.autosummary import _import_by_name import os +import re import sys import warnings import raphtory +from sphinx.util.typing import stringify_annotation +from sphinx.util import inspect + +# for type annotations resolution (need to actually import everything that we want to use in a type hint in the docs) +from typing import * +from raphtory import * import jinja2 @@ -17,7 +28,11 @@ project = 'Raphtory' copyright = '2023, Pometry' author = 'Pometry' -release = '2020' +release = '2023' + + + + # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration @@ -26,7 +41,6 @@ "IPython.sphinxext.ipython_directive", "IPython.sphinxext.ipython_console_highlighting", "matplotlib.sphinxext.plot_directive", - "numpydoc", "sphinx_copybutton", "sphinx_design", "sphinx_toggleprompt", @@ -41,6 +55,7 @@ "sphinx.ext.mathjax", "sphinx.ext.todo", "nbsphinx", + "autodocsumm", ] templates_path = ['_templates'] @@ -55,30 +70,28 @@ :suppress: import raphtory - from raphtory import vis + from raphtory import export import os os.chdir(r'{os.path.dirname(os.path.dirname(__file__))}') """ - html_context = { "header": header, } - # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output html_theme = "pydata_sphinx_theme" - -html_logo = "_static/logo.svg" html_static_path = ['_static', 'images'] html_css_files = [ + "css/custom.css", "css/getting_started.css", "css/raphtory.css", ] +html_logo = "_static/logo.svg" html_use_modindex = True htmlhelp_basename = "raphtory" @@ -91,8 +104,14 @@ "issue": ("https://github.com/pometry/raphtory/issues/%s", "GH %s"), } +intersphinx_mapping = {'python': ('https://docs.python.org/3', None)} + autosummary_generate = True autosummary_imported_members = True +autodoc_typehints = "both" +autodoc_typehints_description_target = "documented" +autodoc_type_aliases = {} + # numpydoc def rstjinja(app, docname, source): @@ -107,6 +126,70 @@ def rstjinja(app, docname, source): rendered = app.builder.templates.render_string(src, app.config.html_context) source[0] = rendered + +def add_typehints(app, objtype: str, name: str, obj, + options: dict, args: str, retann: str) -> tuple[str | Any, str | Any] | tuple[str, None]: + """Record type hints to env object. + + This function does the same as the sphinx.ext.autodoc.typehints extension but for + signatures that are defined in the docstring. + """ + if not hasattr(obj, "__annotations__"): + # If an object has annotations, typehints extension will handle it, otherwise, + # we need to look at the signature for the type hints + + # make sure we set the configuration option in the same way + if app.config.autodoc_typehints_format == 'short': + mode = 'smart' + else: + mode = 'fully-qualified' + + try: + if callable(obj): + # build a mock function from the signature to get the correct annotations + exec_parts = [f"def _annotations_moc"] + if args is not None: + exec_parts.append(args) + else: + exec_parts.append("()") + if retann: + exec_parts.append(f" -> {retann}") + exec_parts.append(":\n pass") + res = globals() + exec("".join(exec_parts), res) + + # extract type hints and store them in the appropriate temp data + # (this is the same as what the typehints extension does) + annotations = app.env.temp_data.setdefault('annotations', {}) + annotation = annotations.setdefault(name, {}) + sig = inspect.signature(res["_annotations_moc"], type_aliases=app.config.autodoc_type_aliases) + for param in sig.parameters.values(): + if param.annotation is not param.empty: + annotation[param.name] = stringify_annotation(param.annotation, mode) + if sig.return_annotation is not sig.empty: + retann = stringify_annotation(sig.return_annotation, mode) + annotation['return'] = retann + kwargs = {} + if app.config.autodoc_typehints in ('none', 'description'): + kwargs.setdefault('show_annotation', False) + if app.config.autodoc_typehints_format == "short": + kwargs.setdefault('unqualified_typehints', True) + + # we need to reparse the signature to get the correct formatting for links to work + # and to enable the 'description' option to strip the type hints from the signature + args = inspect.stringify_signature(sig, **kwargs) + if args: + matched = re.match(r'^(\(.*\))\s+->\s+(.*)$', args) + if matched: + args = matched.group(1) + retann = matched.group(2) + return args, retann + else: + return args, None + except (TypeError, ValueError): + pass + + def setup(app): app.connect("source-read", rstjinja) - + app.connect('autodoc-process-signature', add_typehints, priority=0) diff --git a/docs/source/development/community.rst b/docs/source/development/community.rst deleted file mode 100644 index 061094b4a6..0000000000 --- a/docs/source/development/community.rst +++ /dev/null @@ -1,17 +0,0 @@ -.. _develop.community: - -********** -Community -********** - -.. _community.version: - - -Join the growing community of open-source enthusiasts using Raphtory to power their graph analysis projects! - -- Follow `Twitter`_ for the latest Raphtory news and development - -- Join our `Slack`_ to chat with us and get answers to your questions! - -.. _Twitter: https://twitter.com/raphtory -.. _Slack: https://join.slack.com/t/raphtory/shared_invite/zt-xbebws9j-VgPIFRleJFJBwmpf81tvxA diff --git a/docs/source/development/contributing.rst b/docs/source/development/contributing.rst deleted file mode 100644 index 88aad4ddc6..0000000000 --- a/docs/source/development/contributing.rst +++ /dev/null @@ -1,78 +0,0 @@ -{{ header }} - -.. _contributing: - -============= -Contributing -============= - -We're happy that you're considering contributing! - -To help you get started we've prepared the following guidelines. - -How Do I Contribute? -~~~~~~~~~~~~~~~~~~~~ - -There are many ways to contribute: - -- Report a bug -- Request a feature/enhancement -- Fix bugs -- Work on requested/approved features -- Refactor codebase -- Write tests -- Fix documentation - -Project Layout -~~~~~~~~~~~~~~~ - -- `raphtory`: Raphtory Core written in rust -- `py-raphtory`: Raphtory python library (written in rust, converted to python with PyO3) -- `python`: Raphtory python helper scripts -- `benchmark`: Benchmarking scripts used to compare Raphtory to other platforms -- `raphtory-benchmark`: Benchmarking scripts run in the CI/CD pipeline -- `raphtory-graphql`: GraphQL server for raphtory -- `raphtory-io`: IO module for raphtory -- `js-raphtory`: Raphtory javascript library - - -- `docs`: Documentation (built and hosted using sphinx and readthedocs) -- `examples`: Example raphtory projects in both python and rust -- `resource`: Sample CSV files - - -Documentation -============== - -Raphtory documentations can be found in `docs` directory. -They are built using `Sphinx `__ and hosted by readthedocs. - -After making your changes, you're good to build them. - -- Ensure that all development dependencies are already installed. - ```bash - $ cd docs && pip install -q -r requirements.txt - ``` - -- Build docs - ```bash - $ cd docs && make html - ``` - -- View docs - ```bash - $ open build/html/index.html - ``` - -Community Guidelines -===================== - -This project follows `Google's Open Source Community Guidelines `__. - - -License -======== - -Raphtory it licensed under `GNU General Public License v3.0`. - -This docs page is licensed under `BSD 3-Clause License`. \ No newline at end of file diff --git a/docs/source/development/index.rst b/docs/source/development/index.rst deleted file mode 100644 index f331ba21b5..0000000000 --- a/docs/source/development/index.rst +++ /dev/null @@ -1,18 +0,0 @@ -{{ header }} - -.. _development: - -=========== -Development -=========== - - -.. toctree:: - :maxdepth: 2 - - contributing - policies - roadmap - community - - diff --git a/docs/source/development/policies.rst b/docs/source/development/policies.rst deleted file mode 100644 index a7a59eea76..0000000000 --- a/docs/source/development/policies.rst +++ /dev/null @@ -1,47 +0,0 @@ -.. _develop.policies: - -******** -Policies -******** - -.. _policies.version: - -Raphtory uses a loose variant of semantic versioning (`SemVer`_) to govern -deprecations, API compatibility, and version numbering. - -A raphtory release number is made up of ``MAJOR.MINOR.PATCH``. - -API breaking changes should only occur in **major** releases. These changes -will be documented, with clear guidance on what is changing, why it's changing, -and how to migrate existing code to the new behavior. - -Whenever possible, a deprecation path will be provided rather than an outright -breaking change. - -raphtory will introduce deprecations in **minor** releases. These deprecations -will preserve the existing behavior while emitting a warning that provide -guidance on: - -* How to achieve similar behavior if an alternative is available -* The raphtory version in which the deprecation will be enforced. - -We will not introduce new deprecations in patch releases. - -Deprecations will only be enforced in **major** releases. For example, if a -behavior is deprecated in raphtory 1.2.0, it will continue to work, with a -warning, for all releases in the 1.x series. The behavior will change and the -deprecation removed in the next major release (2.0.0). - -.. note:: - - raphtory will sometimes make *behavior changing* bug fixes, as part of - minor or patch releases. Whether or not a change is a bug fix or an - API-breaking change is a judgement call. We'll do our best, and we - invite you to participate in development discussion on the issue - tracker or mailing list. - -These policies do not apply to features marked as **experimental** in the documentation. -Raphtory may change the behavior of experimental features at any time. - - -.. _SemVer: https://semver.org diff --git a/docs/source/development/roadmap.rst b/docs/source/development/roadmap.rst deleted file mode 100644 index c2c9aca6cc..0000000000 --- a/docs/source/development/roadmap.rst +++ /dev/null @@ -1,19 +0,0 @@ -.. _develop.roadmap: - -******** -Roadmap -******** - -.. _roadmap.version: - - -As an open-source project, our project roadmap is public and open to -suggestions. We welcome your feedback and contributions to the project. - -We work in 2-week sprints, and our roadmap is organized by sprint on Github. -View the current sprint and the next sprint on our `Github project`_ - -Each sprint is organized by issues, which are organized by priority in our fortnightly sprint planning meetings. -These are assigned to team members, and tracked on the Github project board, completed upon merging a pull request. - -.. _Github project: https://github.com/orgs/Pometry/projects/1/ \ No newline at end of file diff --git a/docs/source/getting_started/images/lotr-graphic.png b/docs/source/getting_started/images/lotr-graphic.png deleted file mode 100644 index 3386b8325f..0000000000 Binary files a/docs/source/getting_started/images/lotr-graphic.png and /dev/null differ diff --git a/docs/source/getting_started/index.rst b/docs/source/getting_started/index.rst deleted file mode 100644 index efdbf03301..0000000000 --- a/docs/source/getting_started/index.rst +++ /dev/null @@ -1,158 +0,0 @@ -{{ header }} - -.. _getting_started: - -=============== -Getting started -=============== - - -Installation (python) ----------------------- - -.. grid:: 1 2 2 2 - :gutter: 2 - - .. grid-item-card:: Install via pip? - :class-card: install-card - :columns: 12 12 6 6 - :padding: 3 - - Raphtory can be installed via pip from `PyPI `__. - - ++++ - - .. code-block:: bash - - pip install raphtory - - .. grid-item-card:: Prefer rust? - :class-card: install-card - :columns: 12 12 6 6 - :padding: 3 - - Install into you rust project via crates from `Crates `__. - - ++++ - - .. code-block:: bash - - cargo add raphtory - - -Building / Source ------------------ - -.. grid:: 1 2 2 2 - :gutter: 2 - - .. grid-item-card:: Python from source? - :class-card: install-card - :columns: 12 - :padding: 3 - - Building a specific version? Installing from source? Developing? - Check the python advanced installation pages. - - +++ - - .. button-ref:: install-python - :ref-type: ref - :click-parent: - :color: secondary - :expand: - - Advanced python installation - - .. grid-item-card:: Rust from source? - :class-card: install-card - :columns: 12 - :padding: 3 - - Developing a new functionality? You prefer rust over python? - - +++ - - .. button-ref:: install-rust - :ref-type: ref - :click-parent: - :color: secondary - :expand: - - Advanced rust installation - - -.. _gentle_intro: - -Intro to Raphtory ------------------- - -.. raw:: html - -
-
- -
- -
-
- -Blah Blah Blah Blah. - -.. raw:: html - -
-
-
- -
- -
-
- -Nlah Nlah Nlah Nlah. - -.. raw:: html - -
-
-
-
-
- -
-
- -Tutorials ---------- - -.. If you update this toctree, also update the manual toctree in the - main index.rst.template - -.. toctree:: - :maxdepth: 2 - :hidden: - - overview - installation/index - intro_tutorials/index diff --git a/docs/source/getting_started/installation/index.rst b/docs/source/getting_started/installation/index.rst deleted file mode 100644 index 820b9aa4eb..0000000000 --- a/docs/source/getting_started/installation/index.rst +++ /dev/null @@ -1,9 +0,0 @@ -========================= -Installation -========================= - -.. toctree:: - :maxdepth: 1 - - install_python - install_rust diff --git a/docs/source/getting_started/installation/install_python.rst b/docs/source/getting_started/installation/install_python.rst deleted file mode 100644 index ec9a8f1cb8..0000000000 --- a/docs/source/getting_started/installation/install_python.rst +++ /dev/null @@ -1,98 +0,0 @@ -.. _install-python: - -{{ header }} - -===================== -Python -===================== - -The easiest way to install raphtory is to install it -via pip, `pip install raphtory`. -This is the recommended installation method for most users. - -.. _install.version: - -Python version support ----------------------- - -Officially Python 3.10. - -Installing raphtory -------------------- - -Installing from PyPI -~~~~~~~~~~~~~~~~~~~~ - -Raphtory can be installed via pip from -`PyPI `__. - -.. note:: - You must have ``pip>=23`` to install from PyPI. - -:: - - pip install raphtory - - -Installing from source -~~~~~~~~~~~~~~~~~~~~~~ - -Installing from source is the quickest way to: - -* Try a new feature that will be shipped in the next release (that is, a feature from a pull-request that was recently merged to the main branch). -* Check whether a bug you encountered has been fixed since the last release. - -Note that first uninstalling raphtory might be required to be able to install from source, as version numbers may not be up to date:: - - pip uninstall raphtory -y - -Requirements ------------- - -To install raphtory from source, you need the following: - -* `git `__ to clone the repository. -* `rust `__ to build the rust modules. -* `python `__ to run the setup script. -* `pip `__ to install the python package. -* `virtualenv` to create a virtual environment for the python package or `conda` -* `maturin `__ to build the python package. -* `requirements` listed in the requirements.txt file. - -Installing directly from github source --------------------------------------- - -The following will pull the raphtory repository from git and install the python package from source. - - pip install -e 'git+https://github.com/Pometry/Raphtory.git#egg=raphtory&subdirectory=python' - - -Installing directly from source -------------------------------- - -If you are developing raphtory and want to build & install the python package locally, you can do so with the following command: - - make build-all - or - cd python && maturin develop - - -Running the test suite ----------------------- - -Raphtory is equipped with an exhaustive set of unit tests. -To run it on your machine to verify that everything is working -(and that you have all of the dependencies installed), make sure you have `pytest -`__ >= 7.0 - -Test dependencies: - - python -m pip install -q pytest networkx numpy seaborn pandas nbmake pytest-xdist matplotlib - -To run `raphtory` python tests: - - cd python && pytest - -To run notebook tests: - - cd python/tests && pytest --nbmake --nbmake-timeout=1200 . diff --git a/docs/source/getting_started/installation/install_rust.rst b/docs/source/getting_started/installation/install_rust.rst deleted file mode 100644 index 088904be9d..0000000000 --- a/docs/source/getting_started/installation/install_rust.rst +++ /dev/null @@ -1,99 +0,0 @@ -.. _install-rust: - -{{ header }} - -=================== -Rust -=================== - -The easiest way to install raphtory is to install it -via cargo, `cargo add raphtory`. -This is the recommended installation method for most users. - -.. _install.version-rust: - -Rust version support ----------------------- - -Officially Rust 1.67.1 - -Installing raphtory -------------------- - -Installing from Cargo -~~~~~~~~~~~~~~~~~~~~~ - -Raphtory can be installed via pip from -`Cargo `__. - -.. note:: - You must have ``rust>=1.67.1`` to install from cargo. - -:: - - cargo add raphtory - - -Installing from source -~~~~~~~~~~~~~~~~~~~~~~ - -Installing from source is the quickest way to: - -* Try a new feature that will be shipped in the next release (that is, a feature from a pull-request that was recently merged to the main branch). -* Check whether a bug you encountered has been fixed since the last release. - -Note that first uninstalling raphtory might be required to be able to install from source, as version numbers may not be up to date:: - - cargo remove raphtory - -Requirements ------------- - -To install raphtory from source, you need the following: - -* `git `__ to clone the repository. -* `rust `__ to build the rust modules. -* `make `__ to run the build script. - -Installing directly from source -------------------------------- - -Building the rust core is done using cargo. The following command will build the core. - - make rust-build - -or - - cargo build - -Import the raphtory package into a rust project ------------------------------------------------ - -To use the raphtory core in a rust project, add the following to your Cargo.toml file: -Note: The path should be the path to the raphtory directory - - - - [dependencies] - - raphtory = {path = "../raphtory", version = "0.3.0" } - - -or - - - [dependencies] - - raphtory = "0.3.0" - - -Running the test suite ----------------------- - -Raphtory is equipped with an exhaustive set of unit tests. -To run it on your machine to verify that everything is working -(and that you have all of the dependencies installed) - -To run `raphtory` rust tests: - - cargo test \ No newline at end of file diff --git a/docs/source/getting_started/intro_tutorials/01_quickstart.rst b/docs/source/getting_started/intro_tutorials/01_quickstart.rst deleted file mode 100644 index 33d3e98370..0000000000 --- a/docs/source/getting_started/intro_tutorials/01_quickstart.rst +++ /dev/null @@ -1,143 +0,0 @@ -.. _gettingstarted_quickstart: - -{{ header }} - -How do I create a graph, add nodes/edges, add properties, run algorithms? -========================================================================== - -.. raw:: html - -
    -
  • - -I want to create a graph - -.. ipython:: python - - import raphtory - g = raphtory.Graph() - -To load the raphtory package and start working with it, import the -package. We recommend to import the package under the alias ``raphtory``. - - -.. raw:: html - -
  • -
- -How do I add nodes and edges? -===================================== - -.. raw:: html - -
    -
  • - -I want to add two nodes and an edge into the graph. - -.. ipython:: python - - g.add_vertex(0, "Ben") - g.add_vertex(1, "Hamza") - g.add_edge(2, "Ben", "Hamza") - - -Here we have added a node called "Ben" a time 0, and a node called "Hamza" at time 1. -Next we added an edge between "Ben" and "Hamza" at time 2. - -.. raw:: html - -
  • -
- -.. note:: - These don't have any properties, but we will add them below! - - - -How do I add nodes and edges with properties? -============================================== - -.. raw:: html - -
    -
  • - -I want to add properties with my nodes and edges. - -.. ipython:: python - - g.add_vertex(3, "Rachel", {"class": "student", "age": 20}) - g.add_vertex(4, "Shivam", {"class": "student", "age": 21}) - g.add_edge(5, "Rachel", "Shivam", {"class": "friendship"}) - - -Here we have added a node called "Rachel" a time 3, with the properies class and age. -Similarly, we have doen the same for a node called "Shivam" at time 4. -Next we added an edge between "Rachel" and "Shivam" at time 5 with the property name "class" and the value "friendship". - -.. raw:: html - -
  • -
- - - -How do I run an algorithm? -===================================== - -.. raw:: html - -
    -
  • - -I'd like to run a Max Out Degree algorithm. - -.. ipython:: python - - from raphtory import algorithms - print("Graph - Max out degree: %i" % algorithms.max_out_degree(g)) - -Here we have imported the algorithms package, and then run the max out degree algorithm on the graph. - - -.. raw:: html - -
  • -
- - - -How do I view / visualise my graph? -===================================== - -.. raw:: html - -
    -
  • - -I'd like to view my graph. - -.. ipython:: python - - from raphtory import vis - vis.to_networkx(g) - -or you can show it with pyvis vis - - -.. ipython:: python - - from raphtory import vis - v = vis.to_pyvis(g) - v.show('graph.html') - -Here we have imported the vis package, and then converted the graph to a networkx / pyvis graph. -We can then view the graph in a notebook, or save it to a file. - -.. raw:: html - -
  • -
- diff --git a/docs/source/getting_started/intro_tutorials/index.rst b/docs/source/getting_started/intro_tutorials/index.rst deleted file mode 100644 index 2420671f35..0000000000 --- a/docs/source/getting_started/intro_tutorials/index.rst +++ /dev/null @@ -1,14 +0,0 @@ -{{ header }} - -.. _gettingstarted: - -========================= -Getting started tutorials -========================= - -.. toctree:: - :maxdepth: 1 - - 01_quickstart - lotr.ipynb - diff --git a/docs/source/getting_started/intro_tutorials/lotr.csv b/docs/source/getting_started/intro_tutorials/lotr.csv deleted file mode 100644 index 7515aed3e2..0000000000 --- a/docs/source/getting_started/intro_tutorials/lotr.csv +++ /dev/null @@ -1,2650 +0,0 @@ -SRC,DST,TIME -Gandalf,Elrond,33 -Frodo,Bilbo,114 -Blanco,Marcho,146 -Frodo,Bilbo,205 -Thorin,Gandalf,270 -Thorin,Bilbo,270 -Gandalf,Bilbo,270 -Gollum,Bilbo,286 -Gollum,Bilbo,306 -Gollum,Bilbo,308 -Bilbo,Elrond,317 -Frodo,Samwise,319 -Gandalf,Bilbo,320 -Gollum,Bilbo,324 -Frodo,Gandalf,329 -Peregrin,Elessar,356 -Arwen,Aragorn,358 -Barahir,Faramir,359 -Bilbo,Findegil,360 -Meriadoc,Peregrin,363 -Peregrin,Elendil,368 -Galadriel,Celeborn,374 -Frodo,Bilbo,387 -Frodo,Bilbo,388 -Frodo,Bilbo,389 -Frodo,Bilbo,390 -Frodo,Bilbo,393 -Frodo,Bilbo,399 -Hamfast,Bilbo,402 -Gandalf,Bilbo,483 -Gandalf,Bilbo,543 -Frodo,Gandalf,555 -Frodo,Bilbo,555 -Gandalf,Bilbo,555 -Frodo,Bilbo,562 -Gandalf,Bilbo,730 -Frodo,Bilbo,808 -Frodo,Bilbo,815 -Gandalf,Bilbo,843 -Frodo,Gandalf,861 -Frodo,Bilbo,898 -Frodo,Merry,929 -Frodo,Merry,930 -Frodo,Bilbo,942 -Frodo,Bilbo,944 -Odo,Frodo,959 -Frodo,Bilbo,1038 -Meriadoc,Peregrin,1042 -Meriadoc,Merry,1042 -Meriadoc,Pippin,1042 -Meriadoc,Fredegar,1042 -Peregrin,Merry,1042 -Peregrin,Pippin,1042 -Peregrin,Fredegar,1042 -Merry,Pippin,1042 -Merry,Fredegar,1042 -Pippin,Fredegar,1042 -Pippin,Merry,1044 -Pippin,Bilbo,1044 -Merry,Bilbo,1044 -Frodo,Gandalf,1056 -Sam,Halfast,1089 -Frodo,Bilbo,1106 -Frodo,Gandalf,1130 -Gandalf,Bilbo,1135 -Gandalf,Bilbo,1156 -Frodo,Gandalf,1160 -Frodo,Saruman,1185 -Gandalf,Bilbo,1229 -Frodo,Gandalf,1234 -Frodo,Gandalf,1241 -Isildur,Elendil,1309 -Isildur,Gil-galad,1309 -Isildur,Sauron,1309 -Elendil,Gil-galad,1309 -Elendil,Sauron,1309 -Gil-galad,Sauron,1309 -Sméagol,Déagol,1324 -Sméagol,Déagol,1330 -Sméagol,Déagol,1331 -Sméagol,Déagol,1336 -Frodo,Gollum,1356 -Frodo,Gollum,1359 -Frodo,Gandalf,1390 -Frodo,Gollum,1400 -Frodo,Bilbo,1401 -Isildur,Gollum,1407 -Isildur,Déagol,1407 -Gollum,Déagol,1407 -Frodo,Gandalf,1417 -Isildur,Elendil,1420 -Frodo,Gollum,1429 -Gollum,Déagol,1441 -Gollum,Bilbo,1459 -Gandalf,Bilbo,1469 -Frodo,Bilbo,1484 -Frodo,Gollum,1484 -Bilbo,Gollum,1484 -Frodo,Bilbo,1567 -Frodo,Gollum,1567 -Bilbo,Gollum,1567 -Gandalf,Bilbo,1654 -Frodo,Gandalf,1656 -Sam,Gandalf,1677 -Sam,Frodo,1687 -Sam,Frodo,1705 -Sam,Gandalf,1710 -Sam,Gandalf,1711 -Sam,Frodo,1714 -Gandalf,Bilbo,1721 -Frodo,Gandalf,1741 -Gandalf,Bilbo,1748 -Sam,Elrond,1756 -Frodo,Gandalf,1783 -Frodo,Gandalf,1785 -Pippin,Frodo,1789 -Pippin,Merry,1789 -Pippin,Fredegar,1789 -Frodo,Merry,1789 -Frodo,Fredegar,1789 -Merry,Fredegar,1789 -Frodo,Gandalf,1792 -Frodo,Bilbo,1802 -Frodo,Gandalf,1804 -Merry,Fredegar,1806 -Frodo,Gandalf,1809 -Lobelia,Frodo,1816 -Lobelia,Lotho,1816 -Frodo,Lotho,1816 -Pippin,Sam,1825 -Lobelia,Sam,1826 -Lobelia,Frodo,1826 -Sam,Frodo,1826 -Pippin,Sam,1829 -Sam,Frodo,1869 -Sam,Frodo,1871 -Peregrin,Bilbo,1879 -Pippin,Sam,1888 -Pippin,Frodo,1951 -Pippin,Sam,1955 -Pippin,Bilbo,1981 -Frodo,Gandalf,1999 -Pippin,Frodo,2024 -Pippin,Sam,2024 -Frodo,Sam,2024 -Frodo,Gandalf,2056 -Frodo,Bilbo,2083 -Pippin,Frodo,2110 -Pippin,Sam,2110 -Frodo,Sam,2110 -Frodo,Bilbo,2175 -Sam,Frodo,2204 -Pippin,Frodo,2242 -Frodo,Bilbo,2246 -Frodo,Bilbo,2250 -Sam,Frodo,2254 -Sam,Gandalf,2254 -Frodo,Gandalf,2254 -Sam,Frodo,2318 -Sam,Frodo,2358 -Sam,Frodo,2393 -Pippin,Sam,2446 -Pippin,Frodo,2488 -Maggot,Pippin,2490 -Pippin,Frodo,2511 -Pippin,Sam,2511 -Frodo,Sam,2511 -Pippin,Sam,2513 -Maggot,Pippin,2519 -Pippin,Peregrin,2521 -Sam,Frodo,2541 -Maggot,Frodo,2607 -Frodo,Bilbo,2613 -Frodo,Peregrin,2628 -Pippin,Frodo,2633 -Pippin,Sam,2633 -Frodo,Sam,2633 -Pippin,Sam,2659 -Sam,Frodo,2668 -Frodo,Merry,2694 -Pippin,Frodo,2742 -Pippin,Merry,2742 -Pippin,Sam,2742 -Frodo,Merry,2742 -Frodo,Sam,2742 -Merry,Sam,2742 -Maggot,Frodo,2756 -Meriadoc,Frodo,2786 -Pippin,Bilbo,2794 -Maggot,Frodo,2821 -Pippin,Frodo,2829 -Maggot,Merry,2834 -Pippin,Frodo,2835 -Maggot,Frodo,2837 -Maggot,Bilbo,2837 -Frodo,Bilbo,2837 -Maggot,Merry,2838 -Pippin,Merry,2845 -Frodo,Merry,2850 -Pippin,Frodo,2857 -Sam,Frodo,2882 -Sam,Merry,2919 -Sam,Frodo,2922 -Frodo,Gandalf,2925 -Sam,Frodo,2938 -Pippin,Merry,2949 -Gandalf,Merry,2972 -Frodo,Gandalf,2999 -Gandalf,Fredegar,3060 -Pippin,Merry,3239 -Sam,Frodo,3265 -Pippin,Frodo,3271 -Pippin,Sam,3271 -Frodo,Sam,3271 -Sam,Frodo,3278 -Pippin,Sam,3282 -Pippin,Merry,3291 -Sam,Frodo,3316 -Sam,Frodo,3337 -Sam,Frodo,3345 -Sam,Merry,3356 -Frodo,Goldberry,3461 -Tom,Goldberry,3482 -Tom,Goldberry,3499 -Tom,Goldberry,3514 -Tom,Frodo,3526 -Pippin,Merry,3544 -Tom,Goldberry,3616 -Tom,Goldberry,3686 -Tom,Frodo,3702 -Tom,Frodo,3703 -Tom,Gandalf,3703 -Tom,Bilbo,3703 -Frodo,Gandalf,3703 -Frodo,Bilbo,3703 -Gandalf,Bilbo,3703 -Tom,Frodo,3705 -Tom,Frodo,3714 -Tom,Gandalf,3718 -Frodo,Merry,3721 -Tom,Frodo,3733 -Pippin,Sam,3837 -Pippin,Merry,3837 -Sam,Merry,3837 -Frodo,Gandalf,3914 -Frodo,Bilbo,3914 -Gandalf,Bilbo,3914 -Pippin,Sam,3919 -Pippin,Merry,3919 -Sam,Merry,3919 -Pippin,Sam,3938 -Pippin,Merry,3938 -Sam,Merry,3938 -Frodo,Merry,3946 -Tom,Frodo,3960 -Pippin,Sam,3976 -Pippin,Merry,3976 -Sam,Merry,3976 -Tom,Frodo,3988 -Tom,Merry,4037 -Pippin,Sam,4046 -Pippin,Merry,4046 -Sam,Merry,4046 -Tom,Goldberry,4060 -Butterbur,Barliman,4126 -Tom,Goldberry,4131 -Butterbur,Barliman,4261 -Sam,Frodo,4269 -Nob,Butterbur,4332 -Pippin,Frodo,4342 -Pippin,Sam,4342 -Frodo,Sam,4342 -Butterbur,Barliman,4353 -Pippin,Sam,4380 -Frodo,Butterbur,4389 -Frodo,Bilbo,4418 -Butterbur,Bilbo,4421 -Pippin,Sam,4482 -Pippin,Frodo,4565 -Pippin,Sam,4565 -Frodo,Sam,4565 -Sam,Frodo,4620 -Pippin,Frodo,4629 -Pippin,Sam,4629 -Frodo,Sam,4629 -Sam,Frodo,4692 -Sam,Frodo,4697 -Nob,Butterbur,4714 -Pippin,Sam,4734 -Pippin,Butterbur,4736 -Frodo,Butterbur,4749 -Pippin,Frodo,4902 -Pippin,Sam,4902 -Frodo,Sam,4902 -Frodo,Gandalf,4910 -Pippin,Gandalf,4926 -Pippin,Sam,4930 -Nob,Merry,5012 -Frodo,Merry,5089 -Nob,Merry,5090 -Nob,Gandalf,5090 -Merry,Gandalf,5090 -Bob,Bill,5228 -Bob,Bill,5229 -Butterbur,Merry,5242 -Tom,Butterbur,5249 -Nob,Butterbur,5268 -Nob,Bob,5268 -Butterbur,Bob,5268 -Pippin,Frodo,5274 -Pippin,Merry,5274 -Pippin,Sam,5274 -Frodo,Merry,5274 -Frodo,Sam,5274 -Merry,Sam,5274 -Nob,Bob,5276 -Pippin,Frodo,5410 -Elendil,Gil-galad,5434 -Pippin,Sam,5468 -Frodo,Merry,5470 -Merry,Gandalf,5500 -Frodo,Merry,5534 -Sam,Peregrin,5538 -Pippin,Gandalf,5544 -Merry,Gil-galad,5615 -Pippin,Frodo,5619 -Pippin,Merry,5619 -Frodo,Merry,5619 -Frodo,Gandalf,5620 -Lúthien,Beren,5653 -Lúthien,Barahir,5653 -Beren,Barahir,5653 -Beren,Lúthien,5654 -Beren,Thingol,5654 -Lúthien,Thingol,5654 -Beren,Barahir,5657 -Beren,Thingol,5657 -Barahir,Thingol,5657 -Beren,Lúthien,5660 -Beren,Sauron,5660 -Beren,Thingol,5660 -Lúthien,Sauron,5660 -Lúthien,Thingol,5660 -Sauron,Thingol,5660 -Beren,Thingol,5667 -Beren,Elwing,5667 -Beren,Eärendil,5667 -Beren,Lúthien,5667 -Beren,Dior,5667 -Thingol,Elwing,5667 -Thingol,Eärendil,5667 -Thingol,Lúthien,5667 -Thingol,Dior,5667 -Elwing,Eärendil,5667 -Elwing,Lúthien,5667 -Elwing,Dior,5667 -Eärendil,Lúthien,5667 -Eärendil,Dior,5667 -Lúthien,Dior,5667 -Sam,Merry,5682 -Pippin,Frodo,5683 -Pippin,Merry,5713 -Sam,Frodo,5714 -Sam,Frodo,5752 -Pippin,Merry,5758 -Frodo,Gandalf,5775 -Sam,Merry,5999 -Pippin,Merry,6010 -Sam,Frodo,6011 -Pippin,Merry,6012 -Sam,Frodo,6020 -Frodo,Bilbo,6037 -Frodo,Bilbo,6044 -Gandalf,Bilbo,6118 -Frodo,Bilbo,6126 -Frodo,Bilbo,6127 -Glorfindel,Elrond,6161 -Glorfindel,Frodo,6195 -Elrond,Aragorn,6200 -Glorfindel,Frodo,6220 -Pippin,Sam,6225 -Pippin,Merry,6225 -Sam,Merry,6225 -Glorfindel,Frodo,6267 -Frodo,Lúthien,6325 -Frodo,Gandalf,6381 -Butterbur,Bill,6429 -Gandalf,Barliman,6431 -Frodo,Gandalf,6531 -Glorfindel,Aragorn,6549 -Frodo,Gandalf,6578 -Glorfindel,Elrond,6583 -Sam,Frodo,6628 -Glorfindel,Gandalf,6660 -Glorfindel,Elrond,6660 -Gandalf,Elrond,6660 -Glorfindel,Frodo,6661 -Glorfindel,Gandalf,6661 -Glorfindel,Elrond,6661 -Frodo,Gandalf,6661 -Frodo,Elrond,6661 -Gandalf,Elrond,6661 -Frodo,Elrond,6669 -Arwen,Frodo,6673 -Arwen,Lúthien,6673 -Arwen,Elrond,6673 -Frodo,Lúthien,6673 -Frodo,Elrond,6673 -Lúthien,Elrond,6673 -Frodo,Elrond,6676 -Pippin,Frodo,6682 -Pippin,Merry,6682 -Frodo,Merry,6682 -Thorin,Glóin,6693 -Gandalf,Elrond,6701 -Frodo,Glóin,6709 -Frodo,Beorn,6710 -Frodo,Grimbeorn,6710 -Beorn,Grimbeorn,6710 -Bard,Bain,6717 -Bard,Brand,6717 -Bain,Brand,6717 -Dwalin,Bombur,6728 -Dwalin,Glóin,6728 -Dwalin,Bofur,6728 -Bombur,Glóin,6728 -Bombur,Bofur,6728 -Glóin,Bofur,6728 -Ori,Balin,6730 -Ori,Óin,6730 -Balin,Óin,6730 -Frodo,Glóin,6736 -Smaug,Bilbo,6747 -Frodo,Glóin,6748 -Arwen,Elrond,6754 -Frodo,Gandalf,6757 -Sam,Frodo,6810 -Sam,Bilbo,6810 -Frodo,Bilbo,6810 -Gandalf,Elrond,6833 -Gandalf,Bilbo,6835 -Frodo,Bilbo,6855 -Frodo,Bilbo,6935 -Eärendil,Elrond,6969 -Frodo,Bilbo,6987 -Frodo,Aragorn,6994 -Arwen,Frodo,6995 -Sam,Bilbo,7006 -Sam,Bilbo,7009 -Frodo,Elrond,7018 -Gandalf,Bilbo,7024 -Gandalf,Elrond,7037 -Sam,Frodo,7040 -Sam,Bilbo,7040 -Frodo,Bilbo,7040 -Frodo,Gandalf,7041 -Glorfindel,Glóin,7047 -Glorfindel,Frodo,7047 -Glóin,Frodo,7047 -Frodo,Elrond,7048 -Gimli,Glóin,7051 -Elrond,Glorfindel,7052 -Elrond,Galdor,7052 -Elrond,Erestor,7052 -Elrond,Círdan,7052 -Glorfindel,Galdor,7052 -Glorfindel,Erestor,7052 -Glorfindel,Círdan,7052 -Galdor,Erestor,7052 -Galdor,Círdan,7052 -Erestor,Círdan,7052 -Legolas,Thranduil,7053 -Frodo,Bilbo,7058 -Gandalf,Elrond,7059 -Gandalf,Boromir,7059 -Elrond,Boromir,7059 -Frodo,Glóin,7064 -Balin,Óin,7077 -Balin,Ori,7077 -Óin,Ori,7077 -Elrond,Sauron,7127 -Elrond,Sauron,7128 -Isildur,Elendil,7134 -Isildur,Anárion,7134 -Elendil,Anárion,7134 -Elendil,Gil-galad,7135 -Elendil,Sauron,7135 -Gil-galad,Sauron,7135 -Gil-galad,Elrond,7142 -Elwing,Eärendil,7145 -Elwing,Lúthien,7145 -Elwing,Dior,7145 -Eärendil,Lúthien,7145 -Eärendil,Dior,7145 -Lúthien,Dior,7145 -Elendil,Gil-galad,7148 -Isildur,Elendil,7149 -Isildur,Gil-galad,7149 -Isildur,Sauron,7149 -Elendil,Gil-galad,7149 -Elendil,Sauron,7149 -Gil-galad,Sauron,7149 -Gil-galad,Círdan,7162 -Isildur,Valandil,7170 -Isildur,Elendil,7170 -Isildur,Ohtar,7170 -Valandil,Elendil,7170 -Valandil,Ohtar,7170 -Elendil,Ohtar,7170 -Isildur,Elendil,7179 -Isildur,Gil-galad,7179 -Isildur,Anárion,7179 -Elendil,Gil-galad,7179 -Elendil,Anárion,7179 -Gil-galad,Anárion,7179 -Meneldil,Anárion,7192 -Elrond,Aragorn,7235 -Isildur,Elendil,7241 -Isildur,Elrond,7241 -Isildur,Aragorn,7241 -Elendil,Elrond,7241 -Elendil,Aragorn,7241 -Elrond,Aragorn,7241 -Frodo,Bilbo,7270 -Aragorn,Boromir,7279 -Isildur,Elendil,7281 -Isildur,Denethor,7281 -Elendil,Denethor,7281 -Gollum,Bilbo,7330 -Frodo,Bilbo,7336 -Galdor,Elrond,7353 -Frodo,Gandalf,7356 -Frodo,Galdor,7356 -Frodo,Glóin,7356 -Gandalf,Galdor,7356 -Gandalf,Glóin,7356 -Galdor,Glóin,7356 -Gollum,Gandalf,7391 -Isildur,Aragorn,7404 -Gollum,Aragorn,7405 -Isildur,Gandalf,7407 -Isildur,Saruman,7434 -Isildur,Boromir,7434 -Saruman,Boromir,7434 -Isildur,Elendil,7440 -Gil-galad,Sauron,7448 -Celebrimbor,Sauron,7454 -Gollum,Denethor,7455 -Gollum,Aragorn,7455 -Denethor,Aragorn,7455 -Gandalf,Elrond,7484 -Sméagol,Gollum,7509 -Legolas,Glóin,7525 -Saruman,Radagast,7581 -Gandalf,Saruman,7599 -Saruman,Sauron,7714 -Saruman,Radagast,7739 -Sauron,Aragorn,7769 -Glorfindel,Galdor,7904 -Gandalf,Elrond,7911 -Glorfindel,Saruman,7913 -Glorfindel,Erestor,7932 -Frodo,Boromir,7948 -Balin,Thrór,7992 -Glorfindel,Sauron,8028 -Bilbo,Boromir,8060 -Gandalf,Bilbo,8062 -Frodo,Elrond,8097 -Beren,Túrin,8105 -Beren,Hador,8105 -Beren,Húrin,8105 -Túrin,Hador,8105 -Túrin,Húrin,8105 -Hador,Húrin,8105 -Pippin,Frodo,8117 -Pippin,Merry,8117 -Pippin,Sam,8117 -Frodo,Merry,8117 -Frodo,Sam,8117 -Merry,Sam,8117 -Legolas,Gollum,8145 -Sam,Frodo,8150 -Sam,Bilbo,8150 -Frodo,Bilbo,8150 -Elrond,Thranduil,8157 -Elrond,Aragorn,8158 -Frodo,Gandalf,8183 -Frodo,Gandalf,8190 -Frodo,Bilbo,8190 -Gandalf,Bilbo,8190 -Gimli,Legolas,8255 -Gimli,Glóin,8255 -Legolas,Glóin,8255 -Isildur,Aragorn,8257 -Frodo,Boromir,8262 -Pippin,Frodo,8276 -Pippin,Gandalf,8276 -Frodo,Gandalf,8276 -Pippin,Elrond,8286 -Elendil,Aragorn,8292 -Gandalf,Elrond,8295 -Gandalf,Aragorn,8295 -Elrond,Aragorn,8295 -Frodo,Bilbo,8296 -Pippin,Merry,8297 -Pippin,Frodo,8297 -Pippin,Beren,8297 -Pippin,Bilbo,8297 -Pippin,Sam,8297 -Pippin,Lúthien,8297 -Merry,Frodo,8297 -Merry,Beren,8297 -Merry,Bilbo,8297 -Merry,Sam,8297 -Merry,Lúthien,8297 -Frodo,Beren,8297 -Frodo,Bilbo,8297 -Frodo,Sam,8297 -Frodo,Lúthien,8297 -Beren,Bilbo,8297 -Beren,Sam,8297 -Beren,Lúthien,8297 -Bilbo,Sam,8297 -Bilbo,Lúthien,8297 -Sam,Lúthien,8297 -Frodo,Bilbo,8300 -Frodo,Bilbo,8336 -Frodo,Bilbo,8341 -Elrond,Boromir,8373 -Frodo,Bilbo,8379 -Thorin,Gandalf,8380 -Sam,Bill,8384 -Pippin,Sam,8386 -Frodo,Bilbo,8390 -Elrond,Aragorn,8391 -Sam,Frodo,8397 -Gandalf,Elrond,8405 -Gandalf,Aragorn,8438 -Frodo,Gandalf,8459 -Gimli,Legolas,8473 -Sam,Aragorn,8525 -Sam,Aragorn,8534 -Sam,Aragorn,8538 -Sam,Frodo,8563 -Frodo,Gandalf,8598 -Frodo,Aragorn,8598 -Gandalf,Aragorn,8598 -Gandalf,Aragorn,8600 -Gandalf,Aragorn,8614 -Frodo,Aragorn,8619 -Gandalf,Aragorn,8647 -Gimli,Sauron,8686 -Frodo,Merry,8690 -Frodo,Bilbo,8710 -Gandalf,Boromir,8712 -Legolas,Gandalf,8765 -Aragorn,Boromir,8779 -Aragorn,Boromir,8791 -Aragorn,Boromir,8796 -Merry,Aragorn,8822 -Pippin,Legolas,8827 -Pippin,Merry,8827 -Legolas,Merry,8827 -Sam,Boromir,8828 -Gimli,Gandalf,8829 -Gimli,Bill,8829 -Gandalf,Bill,8829 -Frodo,Aragorn,8830 -Pippin,Sam,8866 -Pippin,Merry,8866 -Sam,Merry,8866 -Aragorn,Boromir,8867 -Frodo,Gandalf,8871 -Gandalf,Boromir,8892 -Gandalf,Sauron,8912 -Balin,Fundin,8919 -Sam,Frodo,8952 -Pippin,Sam,8978 -Pippin,Elrond,8978 -Sam,Elrond,8978 -Pippin,Sam,8982 -Gandalf,Aragorn,9006 -Boromir,Aragorn,9018 -Gandalf,Boromir,9059 -Gimli,Frodo,9098 -Gimli,Gandalf,9098 -Frodo,Gandalf,9098 -Sam,Bill,9134 -Gimli,Legolas,9154 -Gandalf,Bill,9162 -Sam,Frodo,9169 -Sam,Bill,9179 -Gandalf,Fëanor,9210 -Gandalf,Aragorn,9240 -Peregrin,Gandalf,9258 -Sam,Bill,9282 -Gimli,Gandalf,9316 -Sam,Frodo,9329 -Sam,Frodo,9334 -Sam,Frodo,9343 -Sam,Gandalf,9343 -Frodo,Gandalf,9343 -Sam,Frodo,9350 -Gimli,Gandalf,9374 -Legolas,Sam,9402 -Legolas,Frodo,9402 -Legolas,Boromir,9402 -Sam,Frodo,9402 -Sam,Boromir,9402 -Frodo,Boromir,9402 -Gimli,Gandalf,9410 -Gimli,Glóin,9413 -Gimli,Gandalf,9417 -Gimli,Legolas,9444 -Gimli,Boromir,9444 -Gimli,Aragorn,9444 -Legolas,Boromir,9444 -Legolas,Aragorn,9444 -Boromir,Aragorn,9444 -Pippin,Gandalf,9468 -Pippin,Merry,9468 -Gandalf,Merry,9468 -Merry,Aragorn,9478 -Thorin,Bilbo,9605 -Gandalf,Daeron,9670 -Balin,Frodo,9676 -Balin,Bilbo,9676 -Frodo,Bilbo,9676 -Gimli,Frodo,9687 -Ori,Gimli,9717 -Balin,Fundin,9755 -Balin,Gimli,9852 -Aragorn,Boromir,9853 -Pippin,Merry,9861 -Frodo,Aragorn,9864 -Aragorn,Boromir,9870 -Pippin,Frodo,9877 -Pippin,Merry,9877 -Pippin,Aragorn,9877 -Frodo,Merry,9877 -Frodo,Aragorn,9877 -Merry,Aragorn,9877 -Balin,Gimli,9878 -Balin,Legolas,9878 -Gimli,Legolas,9878 -Sam,Frodo,9901 -Frodo,Gandalf,9903 -Gandalf,Bilbo,9982 -Gandalf,Boromir,10098 -Gandalf,Aragorn,10098 -Boromir,Aragorn,10098 -Aragorn,Boromir,10144 -Aragorn,Boromir,10152 -Sam,Frodo,10155 -Sam,Frodo,10235 -Pippin,Sam,10251 -Sam,Frodo,10273 -Sam,Aragorn,10273 -Frodo,Aragorn,10273 -Legolas,Aragorn,10279 -Aragorn,Boromir,10280 -Gimli,Frodo,10298 -Gimli,Sam,10298 -Gimli,Aragorn,10298 -Frodo,Sam,10298 -Frodo,Aragorn,10298 -Sam,Aragorn,10298 -Sam,Aragorn,10299 -Merry,Bilbo,10334 -Sam,Frodo,10356 -Sam,Aragorn,10356 -Frodo,Aragorn,10356 -Gimli,Frodo,10360 -Gimli,Aragorn,10478 -Legolas,Sam,10515 -Legolas,Frodo,10515 -Sam,Frodo,10515 -Legolas,Frodo,10518 -Sam,Frodo,10521 -Orophin,Rúmil,10531 -Haldir,Aragorn,10540 -Frodo,Elrond,10550 -Legolas,Aragorn,10555 -Haldir,Pippin,10568 -Haldir,Legolas,10568 -Haldir,Merry,10568 -Pippin,Legolas,10568 -Pippin,Merry,10568 -Legolas,Merry,10568 -Legolas,Frodo,10591 -Haldir,Rúmil,10645 -Haldir,Amroth,10795 -Frodo,Aragorn,10843 -Frodo,Aragorn,10845 -Frodo,Amroth,10850 -Galadriel,Celeborn,10863 -Haldir,Frodo,10869 -Haldir,Galadriel,10879 -Haldir,Celeborn,10879 -Galadriel,Celeborn,10879 -Legolas,Frodo,10883 -Galadriel,Celeborn,10892 -Haldir,Frodo,10896 -Gimli,Glóin,10909 -Haldir,Legolas,10933 -Balin,Celeborn,10938 -Balin,Aragorn,10938 -Celeborn,Aragorn,10938 -Frodo,Galadriel,10964 -Legolas,Aragorn,10978 -Frodo,Boromir,11009 -Sam,Frodo,11054 -Sam,Frodo,11062 -Sam,Frodo,11095 -Sam,Frodo,11140 -Frodo,Gandalf,11189 -Frodo,Gandalf,11192 -Frodo,Saruman,11192 -Gandalf,Saruman,11192 -Frodo,Galadriel,11248 -Frodo,Gandalf,11279 -Legolas,Boromir,11333 -Legolas,Aragorn,11333 -Boromir,Aragorn,11333 -Celeborn,Aragorn,11344 -Legolas,Galadriel,11359 -Frodo,Aragorn,11362 -Gandalf,Boromir,11363 -Elendil,Sauron,11364 -Frodo,Gandalf,11365 -Frodo,Boromir,11365 -Gandalf,Boromir,11365 -Frodo,Boromir,11367 -Frodo,Boromir,11375 -Frodo,Boromir,11380 -Frodo,Aragorn,11380 -Boromir,Aragorn,11380 -Pippin,Sam,11382 -Pippin,Merry,11382 -Sam,Merry,11382 -Pippin,Merry,11471 -Pippin,Frodo,11471 -Pippin,Boromir,11471 -Pippin,Gimli,11471 -Pippin,Sam,11471 -Pippin,Aragorn,11471 -Pippin,Legolas,11471 -Merry,Frodo,11471 -Merry,Boromir,11471 -Merry,Gimli,11471 -Merry,Sam,11471 -Merry,Aragorn,11471 -Merry,Legolas,11471 -Frodo,Boromir,11471 -Frodo,Gimli,11471 -Frodo,Sam,11471 -Frodo,Aragorn,11471 -Frodo,Legolas,11471 -Boromir,Gimli,11471 -Boromir,Sam,11471 -Boromir,Aragorn,11471 -Boromir,Legolas,11471 -Gimli,Sam,11471 -Gimli,Aragorn,11471 -Gimli,Legolas,11471 -Sam,Aragorn,11471 -Sam,Legolas,11471 -Aragorn,Legolas,11471 -Galadriel,Celeborn,11486 -Aragorn,Boromir,11516 -Galadriel,Celeborn,11528 -Celeborn,Galadriel,11539 -Celeborn,Aragorn,11539 -Galadriel,Aragorn,11539 -Elendil,Elessar,11549 -Arwen,Celebrían,11551 -Pippin,Merry,11553 -Pippin,Boromir,11553 -Merry,Boromir,11553 -Gimli,Galadriel,11566 -Gimli,Glóin,11572 -Gimli,Galadriel,11575 -Gimli,Glóin,11589 -Gimli,Glóin,11649 -Gimli,Glóin,11655 -Sam,Frodo,11680 -Gimli,Legolas,11730 -Pippin,Merry,11731 -Pippin,Boromir,11731 -Pippin,Aragorn,11731 -Merry,Boromir,11731 -Merry,Aragorn,11731 -Boromir,Aragorn,11731 -Pippin,Frodo,11732 -Frodo,Aragorn,11736 -Sam,Frodo,11739 -Sam,Frodo,11744 -Sam,Frodo,11771 -Sam,Frodo,11794 -Gollum,Aragorn,11841 -Merry,Aragorn,11895 -Sam,Frodo,11903 -Legolas,Galadriel,11943 -Frodo,Boromir,11956 -Frodo,Boromir,12025 -Frodo,Aragorn,12025 -Boromir,Aragorn,12025 -Legolas,Aragorn,12030 -Legolas,Frodo,12042 -Legolas,Aragorn,12042 -Frodo,Aragorn,12042 -Pippin,Merry,12063 -Aragorn,Boromir,12066 -Sam,Frodo,12117 -Isildur,Anárion,12126 -Isildur,Valandil,12127 -Isildur,Elendil,12127 -Isildur,Elessar,12127 -Valandil,Elendil,12127 -Valandil,Elessar,12127 -Elendil,Elessar,12127 -Frodo,Aragorn,12205 -Sam,Frodo,12226 -Sam,Boromir,12226 -Frodo,Boromir,12226 -Gandalf,Bilbo,12236 -Frodo,Boromir,12260 -Frodo,Boromir,12321 -Pippin,Sam,12469 -Pippin,Merry,12469 -Sam,Merry,12469 -Denethor,Elrond,12500 -Gimli,Sam,12520 -Legolas,Meriadoc,12521 -Legolas,Peregrin,12521 -Legolas,Boromir,12521 -Meriadoc,Peregrin,12521 -Meriadoc,Boromir,12521 -Peregrin,Boromir,12521 -Sam,Aragorn,12563 -Aragorn,Boromir,12601 -Frodo,Boromir,12615 -Pippin,Frodo,12634 -Pippin,Merry,12634 -Frodo,Merry,12634 -Gimli,Legolas,12637 -Sam,Aragorn,12645 -Sam,Frodo,12702 -Sam,Frodo,12730 -Sam,Frodo,12736 -Sam,Frodo,12769 -Samwise,Boromir,12805 -Samwise,Aragorn,12805 -Boromir,Aragorn,12805 -Gimli,Legolas,12876 -Gimli,Legolas,12878 -Legolas,Aragorn,12881 -Pippin,Frodo,12893 -Pippin,Merry,12893 -Pippin,Sam,12893 -Frodo,Merry,12893 -Frodo,Sam,12893 -Merry,Sam,12893 -Gimli,Sauron,12935 -Gandalf,Saruman,12946 -Gimli,Legolas,12961 -Denethor,Boromir,12986 -Frodo,Aragorn,13052 -Gimli,Frodo,13056 -Legolas,Aragorn,13191 -Gimli,Legolas,13238 -Gimli,Legolas,13309 -Gimli,Legolas,13429 -Gimli,Aragorn,13429 -Legolas,Aragorn,13429 -Saruman,Sauron,13474 -Gimli,Gandalf,13478 -Aragorn,Boromir,13479 -Gimli,Legolas,13530 -Sauron,Aragorn,13562 -Isildur,Elendil,13572 -Isildur,Elessar,13572 -Isildur,Aragorn,13572 -Elendil,Elessar,13572 -Elendil,Aragorn,13572 -Elessar,Aragorn,13572 -Gimli,Legolas,13576 -Legolas,Aragorn,13578 -Denethor,Boromir,13585 -Sauron,Thengel,13588 -Denethor,Sauron,13644 -Gandalf,Saruman,13655 -Gandalf,Shadowfax,13660 -Gandalf,Shadowfax,13663 -Gandalf,Aragorn,13663 -Shadowfax,Aragorn,13663 -Gimli,Legolas,13739 -Gimli,Legolas,13794 -Pippin,Merry,13839 -Legolas,Gandalf,13845 -Gimli,Gandalf,13846 -Gandalf,Aragorn,13848 -Legolas,Aragorn,13870 -Celeborn,Aragorn,13882 -Gimli,Saruman,13908 -Legolas,Aragorn,13909 -Gimli,Legolas,13949 -Gimli,Aragorn,13949 -Legolas,Aragorn,13949 -Pippin,Frodo,13952 -Pippin,Merry,13967 -Gandalf,Elrond,13986 -Pippin,Merry,14151 -Pippin,Merry,14177 -Pippin,Merry,14236 -Pippin,Merry,14331 -Frodo,Gandalf,14364 -Gandalf,Shadowfax,14365 -Pippin,Merry,14394 -Pippin,Merry,14436 -Pippin,Merry,14468 -Pippin,Merry,14544 -Pippin,Merry,14621 -Meriadoc,Merry,14799 -Pippin,Peregrin,14800 -Pippin,Merry,14804 -Pippin,Merry,15032 -Tom,Galadriel,15038 -Tom,Elrond,15038 -Galadriel,Elrond,15038 -Treebeard,Gandalf,15049 -Treebeard,Saruman,15049 -Gandalf,Saruman,15049 -Sam,Gandalf,15050 -Gandalf,Galadriel,15059 -Pippin,Merry,15258 -Treebeard,Merry,15265 -Treebeard,Pippin,15265 -Merry,Pippin,15265 -Pippin,Merry,15267 -Pippin,Merry,15292 -Treebeard,Pippin,15294 -Treebeard,Merry,15298 -Treebeard,Pippin,15298 -Merry,Pippin,15298 -Pippin,Merry,15312 -Pippin,Merry,15338 -Sam,Frodo,15374 -Legolas,Aragorn,15600 -Pippin,Merry,15612 -Pippin,Merry,15619 -Pippin,Boromir,15619 -Merry,Boromir,15619 -Gimli,Legolas,15669 -Gimli,Aragorn,15669 -Legolas,Aragorn,15669 -Pippin,Merry,15672 -Aragorn,Boromir,15901 -Pippin,Merry,15958 -Gandalf,Saruman,16001 -Gandalf,Saruman,16002 -Treebeard,Gandalf,16018 -Pippin,Merry,16036 -Gimli,Saruman,16040 -Legolas,Aragorn,16046 -Gandalf,Aragorn,16109 -Elessar,Aragorn,16187 -Gandalf,Shadowfax,16250 -Legolas,Aragorn,16259 -Gimli,Shadowfax,16260 -Gandalf,Shadowfax,16268 -Gimli,Legolas,16293 -Gimli,Gandalf,16293 -Gimli,Aragorn,16293 -Legolas,Gandalf,16293 -Legolas,Aragorn,16293 -Gandalf,Aragorn,16293 -Gimli,Gandalf,16298 -Gimli,Legolas,16410 -Elendil,Thengel,16455 -Elendil,Aragorn,16455 -Thengel,Aragorn,16455 -Denethor,Aragorn,16456 -Gandalf,Wormtongue,16556 -Gandalf,Thengel,16571 -Gimli,Gandalf,16579 -Gandalf,Wormtongue,16663 -Gandalf,Wormtongue,16851 -Gandalf,Saruman,16910 -Legolas,Aragorn,16949 -Gimli,Gandalf,16951 -Gimli,Legolas,17015 -Gandalf,Shadowfax,17026 -Legolas,Aragorn,17034 -Gimli,Galadriel,17041 -Gandalf,Shadowfax,17059 -Legolas,Gandalf,17094 -Helm,Erkenbrand,17122 -Helm,Erkenbrand,17178 -Gandalf,Wormtongue,17194 -Helm,Gandalf,17196 -Helm,Erkenbrand,17196 -Gandalf,Erkenbrand,17196 -Legolas,Aragorn,17207 -Helm,Erkenbrand,17239 -Helm,Gamling,17258 -Legolas,Aragorn,17457 -Gamling,Saruman,17476 -Gandalf,Aragorn,17581 -Elendil,Aragorn,17650 -Legolas,Erkenbrand,17707 -Legolas,Aragorn,17707 -Erkenbrand,Aragorn,17707 -Gimli,Gamling,17710 -Gandalf,Saruman,17770 -Gimli,Legolas,17785 -Gimli,Gandalf,17785 -Gimli,Aragorn,17785 -Legolas,Gandalf,17785 -Legolas,Aragorn,17785 -Gandalf,Aragorn,17785 -Gimli,Legolas,17820 -Gimli,Gandalf,17820 -Legolas,Gandalf,17820 -Legolas,Gandalf,17821 -Gimli,Legolas,17828 -Legolas,Helm,17848 -Legolas,Galadriel,17857 -Gimli,Legolas,17881 -Gandalf,Shadowfax,17989 -Shadowfax,Gandalf,18002 -Grimbold,Erkenbrand,18006 -Helm,Saruman,18010 -Saruman,Wormtongue,18116 -Saruman,Wormtongue,18157 -Gimli,Legolas,18177 -Pippin,Merry,18196 -Treebeard,Merry,18213 -Treebeard,Gandalf,18230 -Gimli,Legolas,18252 -Gimli,Aragorn,18252 -Legolas,Aragorn,18252 -Gimli,Merry,18299 -Pippin,Aragorn,18431 -Gimli,Gandalf,18452 -Pippin,Saruman,18465 -Treebeard,Saruman,18528 -Gandalf,Galadriel,18554 -Gandalf,Elrond,18554 -Galadriel,Elrond,18554 -Treebeard,Merry,18634 -Treebeard,Gandalf,18654 -Treebeard,Gandalf,18664 -Treebeard,Gandalf,18669 -Sam,Frodo,18689 -Sam,Boromir,18689 -Frodo,Boromir,18689 -Saruman,Aragorn,18825 -Treebeard,Merry,18829 -Pippin,Merry,18936 -Saruman,Wormtongue,18954 -Gandalf,Saruman,18988 -Gandalf,Saruman,18993 -Gandalf,Saruman,19202 -Gandalf,Saruman,19207 -Saruman,Aragorn,19218 -Gandalf,Saruman,19271 -Gimli,Legolas,19275 -Gimli,Aragorn,19275 -Legolas,Aragorn,19275 -Treebeard,Gandalf,19276 -Gimli,Legolas,19292 -Gimli,Legolas,19300 -Pippin,Merry,19326 -Pippin,Gandalf,19362 -Pippin,Merry,19362 -Pippin,Aragorn,19362 -Gandalf,Merry,19362 -Gandalf,Aragorn,19362 -Merry,Aragorn,19362 -Pippin,Merry,19366 -Pippin,Merry,19437 -Pippin,Merry,19453 -Gandalf,Saruman,19471 -Pippin,Gandalf,19475 -Pippin,Sam,19489 -Pippin,Gandalf,19519 -Gandalf,Aragorn,19687 -Pippin,Gandalf,19756 -Pippin,Gandalf,19762 -Pippin,Aragorn,19762 -Gandalf,Aragorn,19762 -Merry,Aragorn,19771 -Shadowfax,Gandalf,19793 -Elendil,Elrond,19848 -Wormtongue,Aragorn,19918 -Peregrin,Shadowfax,19921 -Pippin,Gandalf,19957 -Sam,Frodo,20041 -Sam,Frodo,20047 -Sam,Frodo,20230 -Sam,Frodo,20267 -Frodo,Bilbo,20483 -Frodo,Gollum,20519 -Sam,Frodo,20598 -Sam,Frodo,20607 -Sam,Gollum,20627 -Frodo,Gollum,20653 -Sam,Frodo,20655 -Sam,Gollum,20686 -Frodo,Gollum,20688 -Sam,Frodo,20735 -Sam,Gollum,20769 -Sam,Frodo,20777 -Sam,Frodo,20792 -Sam,Gollum,20838 -Sam,Frodo,20844 -Sam,Gollum,20844 -Frodo,Gollum,20844 -Sam,Frodo,20849 -Sam,Gollum,20859 -Sam,Gollum,20861 -Sam,Frodo,20888 -Frodo,Gollum,20903 -Sam,Samwise,20916 -Sam,Frodo,20985 -Sam,Gollum,20985 -Frodo,Gollum,20985 -Sam,Frodo,21041 -Sam,Frodo,21066 -Sam,Frodo,21137 -Sam,Frodo,21147 -Sam,Frodo,21153 -Sam,Gollum,21167 -Sam,Frodo,21168 -Sam,Gollum,21172 -Sam,Frodo,21214 -Sam,Frodo,21229 -Frodo,Gollum,21299 -Sam,Gollum,21301 -Frodo,Gollum,21369 -Sam,Gollum,21455 -Frodo,Gollum,21466 -Frodo,Gollum,21490 -Sam,Frodo,21492 -Sam,Gollum,21492 -Frodo,Gollum,21492 -Frodo,Gollum,21501 -Frodo,Gandalf,21551 -Frodo,Bilbo,21551 -Gandalf,Bilbo,21551 -Frodo,Gollum,21558 -Isildur,Elendil,21588 -Isildur,Frodo,21588 -Elendil,Frodo,21588 -Sam,Frodo,21626 -Sam,Gollum,21626 -Frodo,Gollum,21626 -Gollum,Aragorn,21673 -Frodo,Gollum,21680 -Frodo,Gollum,21683 -Gollum,Aragorn,21687 -Gandalf,Aragorn,21702 -Gandalf,Saruman,21703 -Gandalf,Aragorn,21703 -Saruman,Aragorn,21703 -Frodo,Samwise,21705 -Frodo,Gandalf,21706 -Sam,Frodo,21727 -Frodo,Gollum,21753 -Sam,Frodo,21765 -Frodo,Gollum,21825 -Sam,Frodo,21860 -Sam,Gollum,21860 -Frodo,Gollum,21860 -Sam,Gollum,21887 -Frodo,Elrond,21938 -Sam,Gollum,21945 -Sam,Frodo,21970 -Sam,Gollum,21970 -Frodo,Gollum,21970 -Sam,Frodo,22061 -Sam,Gollum,22081 -Sam,Frodo,22143 -Frodo,Boromir,22150 -Frodo,Hamfast,22183 -Frodo,Samwise,22183 -Hamfast,Samwise,22183 -Boromir,Aragorn,22187 -Denethor,Boromir,22194 -Mablung,Damrod,22233 -Sam,Frodo,22280 -Sam,Faramir,22341 -Frodo,Faramir,22343 -Sam,Frodo,22349 -Sam,Boromir,22349 -Frodo,Boromir,22349 -Elendil,Boromir,22366 -Isildur,Elendil,22367 -Isildur,Aragorn,22367 -Elendil,Aragorn,22367 -Sam,Frodo,22384 -Sam,Faramir,22384 -Frodo,Faramir,22384 -Frodo,Boromir,22391 -Frodo,Denethor,22441 -Frodo,Faramir,22441 -Denethor,Faramir,22441 -Mablung,Damrod,22556 -Samwise,Frodo,22557 -Samwise,Faramir,22557 -Frodo,Faramir,22557 -Sam,Frodo,22568 -Sam,Mablung,22568 -Sam,Damrod,22568 -Sam,Faramir,22568 -Frodo,Mablung,22568 -Frodo,Damrod,22568 -Frodo,Faramir,22568 -Mablung,Damrod,22568 -Mablung,Faramir,22568 -Damrod,Faramir,22568 -Samwise,Faramir,22571 -Isildur,Frodo,22593 -Boromir,Faramir,22687 -Mablung,Damrod,22692 -Mablung,Damrod,22710 -Sam,Frodo,22718 -Mablung,Damrod,22723 -Sam,Frodo,22824 -Sam,Faramir,22824 -Frodo,Faramir,22824 -Sam,Frodo,22832 -Sam,Frodo,22844 -Frodo,Boromir,22845 -Frodo,Aragorn,22849 -Boromir,Faramir,22852 -Sam,Faramir,22890 -Samwise,Faramir,22891 -Bilbo,Aragorn,22908 -Sam,Frodo,22959 -Frodo,Faramir,22986 -Frodo,Faramir,23030 -Sam,Frodo,23064 -Frodo,Faramir,23077 -Frodo,Faramir,23094 -Anborn,Faramir,23143 -Anborn,Frodo,23166 -Frodo,Gandalf,23212 -Frodo,Gollum,23277 -Frodo,Faramir,23277 -Gollum,Faramir,23277 -Anborn,Gollum,23341 -Frodo,Faramir,23375 -Gollum,Faramir,23389 -Frodo,Gollum,23399 -Frodo,Faramir,23420 -Frodo,Gollum,23435 -Frodo,Faramir,23435 -Gollum,Faramir,23435 -Anborn,Gollum,23477 -Sam,Frodo,23545 -Frodo,Elrond,23572 -Frodo,Gollum,23576 -Frodo,Faramir,23576 -Gollum,Faramir,23576 -Samwise,Faramir,23577 -Frodo,Gollum,23578 -Sam,Frodo,23617 -Sam,Gollum,23624 -Sam,Frodo,23678 -Sam,Gollum,23678 -Frodo,Gollum,23678 -Sam,Frodo,23726 -Sam,Gollum,23726 -Frodo,Gollum,23726 -Sam,Gandalf,23787 -Sam,Gollum,23789 -Sam,Frodo,23816 -Sam,Frodo,23837 -Frodo,Gollum,23855 -Sam,Frodo,23864 -Sam,Gollum,23890 -Sam,Frodo,23896 -Sam,Frodo,23910 -Sam,Frodo,23917 -Sam,Frodo,23940 -Gandalf,Galadriel,24026 -Gandalf,Faramir,24026 -Gandalf,Elrond,24026 -Gandalf,Aragorn,24026 -Galadriel,Faramir,24026 -Galadriel,Elrond,24026 -Galadriel,Aragorn,24026 -Faramir,Elrond,24026 -Faramir,Aragorn,24026 -Elrond,Aragorn,24026 -Sam,Frodo,24050 -Sam,Frodo,24058 -Sam,Frodo,24099 -Sam,Frodo,24138 -Sam,Frodo,24219 -Sam,Frodo,24220 -Sam,Frodo,24253 -Sam,Frodo,24284 -Sam,Frodo,24292 -Sam,Frodo,24334 -Frodo,Gollum,24339 -Sam,Frodo,24355 -Gollum,Shelob,24375 -Sam,Frodo,24413 -Sam,Frodo,24436 -Sam,Frodo,24442 -Sam,Frodo,24515 -Gollum,Faramir,24578 -Ungoliant,Shelob,24618 -Sam,Frodo,24664 -Sam,Gollum,24681 -Sam,Gollum,24687 -Sam,Gollum,24690 -Sam,Gollum,24692 -Sam,Gollum,24693 -Sam,Frodo,24711 -Sam,Frodo,24715 -Sam,Samwise,24715 -Frodo,Samwise,24715 -Sam,Shelob,24736 -Sam,Frodo,24742 -Hamfast,Samwise,24753 -Frodo,Galadriel,24792 -Frodo,Faramir,24816 -Frodo,Bilbo,24817 -Frodo,Bilbo,24857 -Sam,Frodo,24873 -Sam,Shagrat,25066 -Shelob,Shagrat,25096 -Gorbag,Shagrat,25308 -Gorbag,Shagrat,25310 -Pippin,Gandalf,25348 -Pippin,Gandalf,25359 -Pippin,Gandalf,25366 -Pippin,Gandalf,25380 -Pippin,Gandalf,25394 -Gandalf,Denethor,25399 -Gandalf,Boromir,25411 -Amroth,Imrahil,25440 -Pippin,Gandalf,25441 -Pippin,Ecthelion,25446 -Peregrin,Gandalf,25447 -Gandalf,Shadowfax,25472 -Pippin,Gandalf,25480 -Pippin,Gandalf,25485 -Denethor,Ecthelion,25521 -Pippin,Boromir,25523 -Pippin,Aragorn,25523 -Boromir,Aragorn,25523 -Gandalf,Faramir,25533 -Pippin,Boromir,25541 -Pippin,Denethor,25550 -Meriadoc,Pippin,25560 -Pippin,Denethor,25561 -Pippin,Gandalf,25572 -Pippin,Denethor,25573 -Pippin,Peregrin,25574 -Pippin,Denethor,25574 -Pippin,Ecthelion,25574 -Peregrin,Denethor,25574 -Peregrin,Ecthelion,25574 -Denethor,Ecthelion,25574 -Pippin,Gandalf,25578 -Gandalf,Denethor,25579 -Théoden,Saruman,25583 -Pippin,Gandalf,25584 -Gandalf,Denethor,25585 -Pippin,Gandalf,25586 -Treebeard,Gandalf,25589 -Gandalf,Denethor,25592 -Pippin,Gandalf,25599 -Pippin,Denethor,25600 -Pippin,Gandalf,25621 -Pippin,Gandalf,25629 -Isildur,Denethor,25635 -Isildur,Boromir,25635 -Denethor,Boromir,25635 -Pippin,Boromir,25636 -Pippin,Faramir,25636 -Boromir,Faramir,25636 -Denethor,Faramir,25654 -Beregond,Aragorn,25692 -Pippin,Peregrin,25697 -Gandalf,Shadowfax,25715 -Beregond,Shadowfax,25716 -Pippin,Shadowfax,25723 -Pippin,Shadowfax,25726 -Beregond,Pippin,25741 -Beregond,Pippin,25749 -Beregond,Peregrin,25753 -Beregond,Pippin,25780 -Pippin,Peregrin,25817 -Pippin,Peregrin,25867 -Beregond,Gandalf,25868 -Beregond,Pippin,25868 -Gandalf,Pippin,25868 -Pippin,Beregond,25869 -Pippin,Denethor,25872 -Pippin,Boromir,25872 -Denethor,Boromir,25872 -Beregond,Bergil,25917 -Pippin,Merry,25928 -Pippin,Bergil,25928 -Merry,Bergil,25928 -Pippin,Bergil,25930 -Pippin,Bergil,25935 -Pippin,Forlong,25939 -Forlong,Bergil,25941 -Amroth,Imrahil,25961 -Pippin,Gandalf,25987 -Pippin,Faramir,26001 -Gandalf,Merry,26010 -Gandalf,Shadowfax,26010 -Gandalf,Aragorn,26010 -Merry,Shadowfax,26010 -Merry,Aragorn,26010 -Shadowfax,Aragorn,26010 -Gimli,Legolas,26013 -Merry,Aragorn,26031 -Gimli,Legolas,26034 -Gimli,Merry,26034 -Gimli,Aragorn,26034 -Legolas,Merry,26034 -Legolas,Aragorn,26034 -Merry,Aragorn,26034 -Merry,Aragorn,26042 -Théoden,Saruman,26073 -Halbarad,Aragorn,26089 -Gimli,Legolas,26099 -Gimli,Merry,26099 -Legolas,Merry,26099 -Gimli,Legolas,26102 -Gimli,Halbarad,26115 -Legolas,Aragorn,26116 -Legolas,Gandalf,26125 -Legolas,Galadriel,26125 -Gandalf,Galadriel,26125 -Gimli,Gandalf,26126 -Meriadoc,Théoden,26151 -Théoden,Merry,26160 -Théoden,Aragorn,26160 -Merry,Aragorn,26160 -Éomer,Elrond,26169 -Éomer,Halbarad,26169 -Éomer,Aragorn,26169 -Elrond,Halbarad,26169 -Elrond,Aragorn,26169 -Halbarad,Aragorn,26169 -Gimli,Legolas,26170 -Merry,Aragorn,26171 -Éomer,Théoden,26176 -Éomer,Merry,26186 -Éomer,Aragorn,26186 -Merry,Aragorn,26186 -Éomer,Aragorn,26188 -Éomer,Théoden,26190 -Éomer,Aragorn,26190 -Théoden,Aragorn,26190 -Gimli,Legolas,26197 -Gimli,Merry,26197 -Legolas,Merry,26197 -Éomer,Théoden,26202 -Gimli,Legolas,26212 -Gandalf,Aragorn,26219 -Isildur,Elendil,26234 -Isildur,Théoden,26234 -Isildur,Sauron,26234 -Elendil,Théoden,26234 -Elendil,Sauron,26234 -Théoden,Sauron,26234 -Gimli,Aragorn,26236 -Legolas,Elrond,26249 -Legolas,Aragorn,26249 -Elrond,Aragorn,26249 -Arvedui,Aragorn,26250 -Gimli,Aragorn,26257 -Gimli,Legolas,26260 -Gimli,Aragorn,26260 -Legolas,Aragorn,26260 -Isildur,Sauron,26263 -Isildur,Sauron,26266 -Gimli,Legolas,26272 -Gimli,Aragorn,26272 -Legolas,Aragorn,26272 -Gimli,Legolas,26274 -Helm,Halbarad,26276 -Éowyn,Elrond,26278 -Éowyn,Aragorn,26278 -Elrond,Aragorn,26278 -Helm,Théoden,26279 -Éowyn,Aragorn,26283 -Gimli,Legolas,26295 -Gimli,Aragorn,26295 -Legolas,Aragorn,26295 -Gimli,Legolas,26330 -Gimli,Glóin,26361 -Gimli,Aragorn,26362 -Gimli,Glóin,26371 -Gimli,Legolas,26398 -Gimli,Legolas,26400 -Gimli,Amroth,26407 -Isildur,Elessar,26428 -Isildur,Halbarad,26428 -Elessar,Halbarad,26428 -Gimli,Legolas,26433 -Pippin,Amroth,26438 -Gimli,Pippin,26465 -Gimli,Legolas,26465 -Gimli,Aragorn,26465 -Pippin,Legolas,26465 -Pippin,Aragorn,26465 -Legolas,Aragorn,26465 -Sam,Frodo,26466 -Éomer,Théoden,26476 -Gandalf,Shadowfax,26497 -Dúnhere,Gandalf,26500 -Éowyn,Théoden,26544 -Éowyn,Merry,26544 -Théoden,Merry,26544 -Éomer,Aragorn,26549 -Pippin,Gandalf,26571 -Pippin,Frodo,26571 -Pippin,Gimli,26571 -Pippin,Sam,26571 -Pippin,Legolas,26571 -Gandalf,Frodo,26571 -Gandalf,Gimli,26571 -Gandalf,Sam,26571 -Gandalf,Legolas,26571 -Frodo,Gimli,26571 -Frodo,Sam,26571 -Frodo,Legolas,26571 -Gimli,Sam,26571 -Gimli,Legolas,26571 -Sam,Legolas,26571 -Éowyn,Dúnhere,26576 -Éowyn,Éomer,26576 -Éowyn,Théoden,26576 -Dúnhere,Éomer,26576 -Dúnhere,Théoden,26576 -Éomer,Théoden,26576 -Éomer,Aragorn,26584 -Brego,Baldor,26592 -Merry,Aragorn,26598 -Éowyn,Éomer,26599 -Brego,Baldor,26603 -Éomer,Aragorn,26614 -Merry,Boromir,26622 -Hirgon,Denethor,26628 -Théoden,Denethor,26630 -Hirgon,Denethor,26634 -Hirgon,Gandalf,26638 -Hirgon,Denethor,26638 -Gandalf,Denethor,26638 -Éomer,Théoden,26705 -Éomer,Merry,26706 -Éowyn,Merry,26712 -Théoden,Merry,26714 -Éowyn,Aragorn,26721 -Gimli,Merry,26725 -Meriadoc,Merry,26759 -Pippin,Gandalf,26788 -Pippin,Denethor,26800 -Pippin,Gandalf,26801 -Éomer,Gandalf,26819 -Éomer,Denethor,26819 -Gandalf,Denethor,26819 -Pippin,Denethor,26820 -Pippin,Denethor,26821 -Pippin,Gandalf,26862 -Beregond,Pippin,26868 -Beregond,Faramir,26868 -Pippin,Faramir,26868 -Beregond,Faramir,26909 -Beregond,Pippin,26910 -Gandalf,Shadowfax,26929 -Pippin,Faramir,26930 -Pippin,Boromir,26932 -Pippin,Denethor,26949 -Pippin,Faramir,26953 -Pippin,Gandalf,26955 -Pippin,Gandalf,26957 -Pippin,Faramir,26957 -Gandalf,Faramir,26957 -Pippin,Frodo,26958 -Pippin,Gandalf,26958 -Frodo,Gandalf,26958 -Pippin,Gandalf,26959 -Boromir,Denethor,26991 -Boromir,Faramir,26991 -Denethor,Faramir,26991 -Boromir,Denethor,26998 -Boromir,Faramir,26998 -Denethor,Faramir,26998 -Pippin,Gandalf,27035 -Pippin,Denethor,27035 -Gandalf,Denethor,27035 -Pippin,Gandalf,27056 -Pippin,Gandalf,27058 -Pippin,Frodo,27060 -Pippin,Gandalf,27060 -Frodo,Gandalf,27060 -Pippin,Gollum,27090 -Frodo,Gollum,27094 -Boromir,Faramir,27114 -Denethor,Boromir,27125 -Gandalf,Faramir,27128 -Beregond,Faramir,27161 -Nazgûl,Denethor,27184 -Nazgûl,Sauron,27184 -Denethor,Sauron,27184 -Pippin,Gandalf,27187 -Shadowfax,Amroth,27234 -Shadowfax,Faramir,27234 -Amroth,Faramir,27234 -Denethor,Faramir,27255 -Nazgûl,Faramir,27259 -Imrahil,Faramir,27261 -Denethor,Faramir,27265 -Pippin,Gandalf,27269 -Gandalf,Denethor,27322 -Denethor,Faramir,27350 -Denethor,Faramir,27362 -Denethor,Faramir,27368 -Denethor,Faramir,27387 -Pippin,Denethor,27389 -Denethor,Faramir,27404 -Pippin,Denethor,27422 -Beregond,Pippin,27428 -Beregond,Faramir,27428 -Pippin,Faramir,27428 -Beregond,Pippin,27435 -Beregond,Faramir,27435 -Pippin,Faramir,27435 -Gandalf,Shadowfax,27488 -Pippin,Merry,27536 -Éomer,Merry,27539 -Éomer,Théoden,27567 -Théoden,Ghân-buri-Ghân,27665 -Éomer,Denethor,27690 -Éomer,Théoden,27771 -Théoden,Thengel,27796 -Éowyn,Merry,27873 -Éowyn,Meriadoc,27881 -Éomer,Amroth,27905 -Merry,Gandalf,27906 -Éowyn,Éomer,27908 -Éomer,Théoden,27918 -Éowyn,Théoden,27951 -Imrahil,Amroth,27953 -Éowyn,Éomer,27962 -Éomer,Imrahil,27972 -Éomer,Húrin,27972 -Imrahil,Húrin,27972 -Arwen,Elrond,28013 -Isildur,Elessar,28014 -Isildur,Aragorn,28014 -Elessar,Aragorn,28014 -Gimli,Legolas,28019 -Gimli,Halbarad,28019 -Legolas,Halbarad,28019 -Elendil,Aragorn,28020 -Éomer,Aragorn,28021 -Éomer,Imrahil,28034 -Éomer,Aragorn,28034 -Imrahil,Aragorn,28034 -Grimbold,Halbarad,28038 -Grimbold,Dúnhere,28044 -Grimbold,Fastred,28044 -Grimbold,Horn,28044 -Grimbold,Harding,28044 -Dúnhere,Fastred,28044 -Dúnhere,Horn,28044 -Dúnhere,Harding,28044 -Fastred,Horn,28044 -Fastred,Harding,28044 -Horn,Harding,28044 -Gandalf,Denethor,28049 -Gandalf,Shadowfax,28053 -Pippin,Shadowfax,28055 -Pippin,Faramir,28067 -Pippin,Denethor,28070 -Pippin,Faramir,28070 -Denethor,Faramir,28070 -Pippin,Gandalf,28073 -Gandalf,Faramir,28074 -Pippin,Shadowfax,28077 -Pippin,Beregond,28089 -Beregond,Denethor,28102 -Pippin,Gandalf,28104 -Pippin,Denethor,28104 -Gandalf,Denethor,28104 -Gandalf,Denethor,28109 -Denethor,Faramir,28116 -Beregond,Gandalf,28122 -Beregond,Pippin,28122 -Beregond,Denethor,28122 -Gandalf,Pippin,28122 -Gandalf,Denethor,28122 -Pippin,Denethor,28122 -Gandalf,Denethor,28174 -Beregond,Faramir,28177 -Beregond,Gandalf,28191 -Beregond,Denethor,28191 -Beregond,Ecthelion,28191 -Gandalf,Denethor,28191 -Gandalf,Ecthelion,28191 -Denethor,Ecthelion,28191 -Beregond,Faramir,28195 -Beregond,Gandalf,28197 -Beregond,Pippin,28197 -Beregond,Faramir,28197 -Gandalf,Pippin,28197 -Gandalf,Faramir,28197 -Pippin,Faramir,28197 -Gandalf,Amroth,28204 -Beregond,Gandalf,28210 -Beregond,Pippin,28210 -Beregond,Faramir,28210 -Gandalf,Pippin,28210 -Gandalf,Faramir,28210 -Pippin,Faramir,28210 -Pippin,Beregond,28212 -Denethor,Sauron,28225 -Beregond,Faramir,28229 -Éowyn,Théoden,28249 -Pippin,Merry,28276 -Beregond,Bergil,28285 -Éowyn,Meriadoc,28301 -Éowyn,Faramir,28301 -Meriadoc,Faramir,28301 -Éomer,Imrahil,28323 -Éomer,Aragorn,28323 -Imrahil,Aragorn,28323 -Éomer,Elendil,28327 -Éomer,Imrahil,28334 -Éowyn,Éomer,28338 -Éowyn,Imrahil,28339 -Éowyn,Gandalf,28345 -Denethor,Faramir,28346 -Galadriel,Aragorn,28350 -Amroth,Faramir,28352 -Éomer,Imrahil,28365 -Elessar,Aragorn,28366 -Éowyn,Meriadoc,28368 -Éowyn,Gandalf,28368 -Meriadoc,Gandalf,28368 -Éowyn,Merry,28370 -Éowyn,Faramir,28370 -Éowyn,Aragorn,28370 -Merry,Faramir,28370 -Merry,Aragorn,28370 -Faramir,Aragorn,28370 -Éomer,Faramir,28373 -Éomer,Elrond,28373 -Éomer,Aragorn,28373 -Faramir,Elrond,28373 -Faramir,Aragorn,28373 -Elrond,Aragorn,28373 -Gandalf,Faramir,28386 -Shadowfax,Aragorn,28387 -Gandalf,Aragorn,28390 -Aragorn,Faramir,28419 -Faramir,Aragorn,28421 -Faramir,Aragorn,28432 -Faramir,Aragorn,28436 -Beregond,Gandalf,28443 -Beregond,Imrahil,28443 -Gandalf,Imrahil,28443 -Pippin,Gandalf,28444 -Éowyn,Aragorn,28448 -Théoden,Wormtongue,28464 -Saruman,Wormtongue,28468 -Éomer,Aragorn,28472 -Éowyn,Aragorn,28483 -Éowyn,Aragorn,28484 -Éomer,Éowyn,28488 -Éowyn,Éomer,28499 -Éomer,Gandalf,28503 -Pippin,Gandalf,28510 -Pippin,Merry,28510 -Pippin,Aragorn,28510 -Gandalf,Merry,28510 -Gandalf,Aragorn,28510 -Merry,Aragorn,28510 -Pippin,Merry,28511 -Merry,Aragorn,28516 -Pippin,Gandalf,28518 -Pippin,Aragorn,28528 -Meriadoc,Saruman,28532 -Meriadoc,Aragorn,28532 -Saruman,Aragorn,28532 -Merry,Gandalf,28543 -Éowyn,Gandalf,28565 -Éowyn,Faramir,28565 -Éowyn,Aragorn,28565 -Gandalf,Faramir,28565 -Gandalf,Aragorn,28565 -Faramir,Aragorn,28565 -Éowyn,Gandalf,28566 -Éowyn,Faramir,28566 -Éowyn,Aragorn,28566 -Gandalf,Faramir,28566 -Gandalf,Aragorn,28566 -Faramir,Aragorn,28566 -Beregond,Meriadoc,28568 -Elrond,Aragorn,28573 -Gimli,Pippin,28578 -Gimli,Legolas,28578 -Gimli,Merry,28578 -Pippin,Legolas,28578 -Pippin,Merry,28578 -Legolas,Merry,28578 -Gimli,Legolas,28579 -Legolas,Aragorn,28581 -Legolas,Imrahil,28583 -Legolas,Aragorn,28583 -Imrahil,Aragorn,28583 -Legolas,Aragorn,28587 -Meriadoc,Peregrin,28589 -Meriadoc,Imrahil,28589 -Peregrin,Imrahil,28589 -Legolas,Merry,28603 -Legolas,Merry,28619 -Gimli,Glóin,28632 -Éowyn,Merry,28635 -Pippin,Legolas,28639 -Pippin,Merry,28639 -Legolas,Merry,28639 -Angbor,Aragorn,28652 -Lúthien,Sauron,28683 -Isildur,Aragorn,28690 -Gandalf,Aragorn,28715 -Théoden,Imrahil,28719 -Théoden,Gimli,28719 -Théoden,Aragorn,28719 -Théoden,Éomer,28719 -Théoden,Legolas,28719 -Imrahil,Gimli,28719 -Imrahil,Aragorn,28719 -Imrahil,Éomer,28719 -Imrahil,Legolas,28719 -Gimli,Aragorn,28719 -Gimli,Éomer,28719 -Gimli,Legolas,28719 -Aragorn,Éomer,28719 -Aragorn,Legolas,28719 -Éomer,Legolas,28719 -Gandalf,Elrond,28720 -Gandalf,Aragorn,28720 -Elrond,Aragorn,28720 -Amroth,Imrahil,28730 -Gandalf,Sauron,28793 -Imrahil,Aragorn,28799 -Imrahil,Gimli,28818 -Imrahil,Glóin,28818 -Imrahil,Aragorn,28818 -Gimli,Glóin,28818 -Gimli,Aragorn,28818 -Glóin,Aragorn,28818 -Éomer,Elrond,28820 -Éomer,Amroth,28820 -Éomer,Imrahil,28820 -Éomer,Aragorn,28820 -Elrond,Amroth,28820 -Elrond,Imrahil,28820 -Elrond,Aragorn,28820 -Amroth,Imrahil,28820 -Amroth,Aragorn,28820 -Imrahil,Aragorn,28820 -Gimli,Legolas,28836 -Gimli,Gandalf,28836 -Gimli,Elrond,28836 -Gimli,Aragorn,28836 -Legolas,Gandalf,28836 -Legolas,Elrond,28836 -Legolas,Aragorn,28836 -Gandalf,Elrond,28836 -Gandalf,Aragorn,28836 -Elrond,Aragorn,28836 -Gandalf,Imrahil,28872 -Gandalf,Faramir,28872 -Imrahil,Faramir,28872 -Gandalf,Aragorn,28876 -Nazgûl,Aragorn,28894 -Gandalf,Imrahil,28926 -Gandalf,Gimli,28926 -Gandalf,Peregrin,28926 -Gandalf,Elrond,28926 -Gandalf,Aragorn,28926 -Gandalf,Éomer,28926 -Gandalf,Legolas,28926 -Imrahil,Gimli,28926 -Imrahil,Peregrin,28926 -Imrahil,Elrond,28926 -Imrahil,Aragorn,28926 -Imrahil,Éomer,28926 -Imrahil,Legolas,28926 -Gimli,Peregrin,28926 -Gimli,Elrond,28926 -Gimli,Aragorn,28926 -Gimli,Éomer,28926 -Gimli,Legolas,28926 -Peregrin,Elrond,28926 -Peregrin,Aragorn,28926 -Peregrin,Éomer,28926 -Peregrin,Legolas,28926 -Elrond,Aragorn,28926 -Elrond,Éomer,28926 -Elrond,Legolas,28926 -Aragorn,Éomer,28926 -Aragorn,Legolas,28926 -Éomer,Legolas,28926 -Gandalf,Sauron,28956 -Sam,Frodo,28958 -Pippin,Imrahil,28960 -Saruman,Sauron,28982 -Elrond,Amroth,29020 -Elrond,Imrahil,29020 -Amroth,Imrahil,29020 -Beregond,Frodo,29024 -Beregond,Gandalf,29024 -Beregond,Pippin,29024 -Beregond,Imrahil,29024 -Frodo,Gandalf,29024 -Frodo,Pippin,29024 -Frodo,Imrahil,29024 -Gandalf,Pippin,29024 -Gandalf,Imrahil,29024 -Pippin,Imrahil,29024 -Beregond,Pippin,29040 -Pippin,Merry,29069 -Pippin,Denethor,29069 -Pippin,Aragorn,29069 -Merry,Denethor,29069 -Merry,Aragorn,29069 -Denethor,Aragorn,29069 -Sam,Frodo,29070 -Hamfast,Samwise,29072 -Frodo,Shelob,29084 -Frodo,Shagrat,29084 -Shelob,Shagrat,29084 -Gorbag,Shagrat,29101 -Gorbag,Shagrat,29163 -Gorbag,Shagrat,29164 -Gorbag,Sam,29169 -Sam,Frodo,29207 -Gorbag,Shagrat,29246 -Gorbag,Shagrat,29288 -Nazgûl,Shagrat,29309 -Sam,Shagrat,29317 -Sam,Shagrat,29323 -Sam,Shagrat,29344 -Sam,Shagrat,29350 -Sam,Frodo,29427 -Sam,Frodo,29440 -Sam,Frodo,29449 -Sam,Frodo,29461 -Sam,Frodo,29480 -Sam,Frodo,29499 -Sam,Frodo,29503 -Sam,Frodo,29511 -Sam,Frodo,29521 -Sam,Frodo,29532 -Sam,Frodo,29545 -Gorbag,Sam,29578 -Sam,Frodo,29583 -Sam,Frodo,29584 -Sam,Frodo,29593 -Sam,Faramir,29619 -Sam,Frodo,29623 -Sam,Galadriel,29649 -Sam,Frodo,29654 -Sam,Frodo,29679 -Sam,Frodo,29684 -Sam,Frodo,29689 -Sam,Frodo,29694 -Sam,Frodo,29709 -Sam,Shagrat,29717 -Sam,Frodo,29739 -Sam,Frodo,29751 -Sam,Frodo,29764 -Sam,Frodo,29780 -Sam,Frodo,29788 -Gorbag,Shagrat,29792 -Sam,Frodo,29795 -Sam,Frodo,29807 -Sam,Frodo,29836 -Sam,Frodo,29851 -Sam,Faramir,29855 -Sam,Frodo,29863 -Sam,Frodo,29874 -Sam,Frodo,29888 -Sam,Frodo,29972 -Sam,Frodo,29987 -Sam,Shelob,29987 -Sam,Gollum,29987 -Frodo,Shelob,29987 -Frodo,Gollum,29987 -Shelob,Gollum,29987 -Sam,Frodo,29988 -Sam,Frodo,30000 -Sam,Frodo,30011 -Sam,Frodo,30029 -Sam,Frodo,30031 -Sam,Frodo,30035 -Sam,Frodo,30042 -Sam,Frodo,30043 -Frodo,Gollum,30061 -Sam,Frodo,30064 -Sam,Frodo,30085 -Sam,Frodo,30118 -Sam,Frodo,30141 -Sam,Frodo,30153 -Sam,Frodo,30170 -Frodo,Gandalf,30173 -Sam,Frodo,30187 -Sam,Frodo,30276 -Sam,Faramir,30276 -Frodo,Faramir,30276 -Sam,Frodo,30316 -Sam,Frodo,30351 -Sam,Frodo,30365 -Sam,Frodo,30368 -Sam,Frodo,30371 -Sam,Frodo,30379 -Sam,Frodo,30413 -Sam,Frodo,30419 -Frodo,Gollum,30437 -Sam,Gollum,30440 -Sam,Frodo,30453 -Sam,Gollum,30453 -Frodo,Gollum,30453 -Sam,Gollum,30488 -Sam,Frodo,30513 -Sam,Gollum,30536 -Sam,Frodo,30547 -Gollum,Gandalf,30571 -Gandalf,Sauron,30601 -Gandalf,Aragorn,30610 -Meneldor,Gandalf,30615 -Sam,Frodo,30618 -Sam,Frodo,30622 -Sam,Frodo,30628 -Sam,Frodo,30630 -Sam,Gandalf,30668 -Gandalf,Sauron,30681 -Sam,Frodo,30694 -Sam,Galadriel,30694 -Frodo,Galadriel,30694 -Frodo,Samwise,30706 -Sam,Frodo,30716 -Sam,Frodo,30721 -Sam,Aragorn,30723 -Sam,Frodo,30725 -Sam,Frodo,30729 -Sam,Frodo,30736 -Frodo,Gandalf,30737 -Sam,Frodo,30741 -Frodo,Gandalf,30744 -Frodo,Bilbo,30744 -Gandalf,Bilbo,30744 -Éomer,Gimli,30745 -Éomer,Legolas,30745 -Éomer,Gandalf,30745 -Éomer,Imrahil,30745 -Gimli,Legolas,30745 -Gimli,Gandalf,30745 -Gimli,Imrahil,30745 -Legolas,Gandalf,30745 -Legolas,Imrahil,30745 -Gandalf,Imrahil,30745 -Peregrin,Merry,30751 -Pippin,Merry,30759 -Pippin,Gandalf,30759 -Pippin,Frodo,30759 -Pippin,Gimli,30759 -Pippin,Sam,30759 -Pippin,Legolas,30759 -Merry,Gandalf,30759 -Merry,Frodo,30759 -Merry,Gimli,30759 -Merry,Sam,30759 -Merry,Legolas,30759 -Gandalf,Frodo,30759 -Gandalf,Gimli,30759 -Gandalf,Sam,30759 -Gandalf,Legolas,30759 -Frodo,Gimli,30759 -Frodo,Sam,30759 -Frodo,Legolas,30759 -Gimli,Sam,30759 -Gimli,Legolas,30759 -Sam,Legolas,30759 -Sam,Frodo,30760 -Pippin,Frodo,30763 -Pippin,Merry,30763 -Frodo,Merry,30763 -Gandalf,Bilbo,30772 -Gimli,Pippin,30775 -Gimli,Frodo,30775 -Gimli,Sam,30775 -Pippin,Frodo,30775 -Pippin,Sam,30775 -Frodo,Sam,30775 -Sam,Frodo,30798 -Éomer,Théoden,30860 -Éomer,Faramir,30860 -Théoden,Faramir,30860 -Éowyn,Merry,30888 -Éowyn,Faramir,30888 -Merry,Faramir,30888 -Merry,Faramir,30889 -Éowyn,Faramir,30896 -Éowyn,Amroth,30903 -Éowyn,Finduilas,30903 -Amroth,Finduilas,30903 -Éowyn,Faramir,30905 -Merry,Faramir,30945 -Éowyn,Faramir,30949 -Elendil,Aragorn,30950 -Éowyn,Faramir,30960 -Éowyn,Faramir,30981 -Éowyn,Húrin,30991 -Éowyn,Faramir,30991 -Húrin,Faramir,30991 -Éomer,Gandalf,30995 -Éomer,Imrahil,30995 -Gandalf,Imrahil,30995 -Húrin,Faramir,31008 -Faramir,Aragorn,31009 -Isildur,Valandil,31013 -Isildur,Elendil,31013 -Isildur,Elessar,31013 -Isildur,Aragorn,31013 -Valandil,Elendil,31013 -Valandil,Elessar,31013 -Valandil,Aragorn,31013 -Elendil,Elessar,31013 -Elendil,Aragorn,31013 -Elessar,Aragorn,31013 -Eärnur,Faramir,31017 -Faramir,Aragorn,31022 -Frodo,Gandalf,31023 -Frodo,Faramir,31023 -Frodo,Aragorn,31023 -Gandalf,Faramir,31023 -Gandalf,Aragorn,31023 -Faramir,Aragorn,31023 -Húrin,Elessar,31025 -Húrin,Faramir,31025 -Elessar,Faramir,31025 -Beregond,Faramir,31038 -Faramir,Aragorn,31039 -Éomer,Aragorn,31040 -Éowyn,Elrond,31045 -Éowyn,Faramir,31045 -Elrond,Faramir,31045 -Gimli,Legolas,31048 -Gimli,Aragorn,31048 -Legolas,Aragorn,31048 -Frodo,Gandalf,31052 -Frodo,Aragorn,31052 -Gandalf,Aragorn,31052 -Gandalf,Bilbo,31053 -Pippin,Frodo,31054 -Pippin,Gandalf,31054 -Frodo,Gandalf,31054 -Gandalf,Aragorn,31057 -Gandalf,Aragorn,31058 -Gandalf,Aragorn,31067 -Gandalf,Nimloth,31086 -Nimloth,Aragorn,31094 -Glorfindel,Arwen,31102 -Glorfindel,Erestor,31102 -Glorfindel,Elrond,31102 -Glorfindel,Galadriel,31102 -Glorfindel,Celeborn,31102 -Arwen,Erestor,31102 -Arwen,Elrond,31102 -Arwen,Galadriel,31102 -Arwen,Celeborn,31102 -Erestor,Elrond,31102 -Erestor,Galadriel,31102 -Erestor,Celeborn,31102 -Elrond,Galadriel,31102 -Elrond,Celeborn,31102 -Galadriel,Celeborn,31102 -Frodo,Gandalf,31103 -Arwen,Elessar,31106 -Arwen,Aragorn,31106 -Elessar,Aragorn,31106 -Arwen,Frodo,31108 -Frodo,Aragorn,31109 -Arwen,Bilbo,31113 -Arwen,Elrond,31113 -Bilbo,Elrond,31113 -Éomer,Théoden,31119 -Éomer,Gimli,31129 -Éomer,Glóin,31129 -Gimli,Glóin,31129 -Éomer,Gimli,31131 -Théoden,Merry,31141 -Pippin,Gandalf,31142 -Pippin,Shadowfax,31142 -Pippin,Frodo,31142 -Pippin,Gimli,31142 -Pippin,Aragorn,31142 -Pippin,Legolas,31142 -Pippin,Samwise,31142 -Gandalf,Shadowfax,31142 -Gandalf,Frodo,31142 -Gandalf,Gimli,31142 -Gandalf,Aragorn,31142 -Gandalf,Legolas,31142 -Gandalf,Samwise,31142 -Shadowfax,Frodo,31142 -Shadowfax,Gimli,31142 -Shadowfax,Aragorn,31142 -Shadowfax,Legolas,31142 -Shadowfax,Samwise,31142 -Frodo,Gimli,31142 -Frodo,Aragorn,31142 -Frodo,Legolas,31142 -Frodo,Samwise,31142 -Gimli,Aragorn,31142 -Gimli,Legolas,31142 -Gimli,Samwise,31142 -Aragorn,Legolas,31142 -Aragorn,Samwise,31142 -Legolas,Samwise,31142 -Elrond,Arwen,31143 -Elrond,Galadriel,31143 -Elrond,Amroth,31143 -Elrond,Celeborn,31143 -Arwen,Galadriel,31143 -Arwen,Amroth,31143 -Arwen,Celeborn,31143 -Galadriel,Amroth,31143 -Galadriel,Celeborn,31143 -Amroth,Celeborn,31143 -Théoden,Thengel,31144 -Elessar,Aragorn,31146 -Théoden,Thengel,31152 -Helm,Théoden,31153 -Théoden,Merry,31156 -Éowyn,Éomer,31161 -Fréa,Fengel,31162 -Fréa,Helm,31162 -Fréa,Théoden,31162 -Fréa,Baldor,31162 -Fréa,Déor,31162 -Fréa,Brego,31162 -Fréa,Goldwine,31162 -Fréa,Folcwine,31162 -Fréa,Aldor,31162 -Fréa,Walda,31162 -Fréa,Folca,31162 -Fréa,Thengel,31162 -Fréa,Gram,31162 -Fengel,Helm,31162 -Fengel,Théoden,31162 -Fengel,Baldor,31162 -Fengel,Déor,31162 -Fengel,Brego,31162 -Fengel,Goldwine,31162 -Fengel,Folcwine,31162 -Fengel,Aldor,31162 -Fengel,Walda,31162 -Fengel,Folca,31162 -Fengel,Thengel,31162 -Fengel,Gram,31162 -Helm,Théoden,31162 -Helm,Baldor,31162 -Helm,Déor,31162 -Helm,Brego,31162 -Helm,Goldwine,31162 -Helm,Folcwine,31162 -Helm,Aldor,31162 -Helm,Walda,31162 -Helm,Folca,31162 -Helm,Thengel,31162 -Helm,Gram,31162 -Théoden,Baldor,31162 -Théoden,Déor,31162 -Théoden,Brego,31162 -Théoden,Goldwine,31162 -Théoden,Folcwine,31162 -Théoden,Aldor,31162 -Théoden,Walda,31162 -Théoden,Folca,31162 -Théoden,Thengel,31162 -Théoden,Gram,31162 -Baldor,Déor,31162 -Baldor,Brego,31162 -Baldor,Goldwine,31162 -Baldor,Folcwine,31162 -Baldor,Aldor,31162 -Baldor,Walda,31162 -Baldor,Folca,31162 -Baldor,Thengel,31162 -Baldor,Gram,31162 -Déor,Brego,31162 -Déor,Goldwine,31162 -Déor,Folcwine,31162 -Déor,Aldor,31162 -Déor,Walda,31162 -Déor,Folca,31162 -Déor,Thengel,31162 -Déor,Gram,31162 -Brego,Goldwine,31162 -Brego,Folcwine,31162 -Brego,Aldor,31162 -Brego,Walda,31162 -Brego,Folca,31162 -Brego,Thengel,31162 -Brego,Gram,31162 -Goldwine,Folcwine,31162 -Goldwine,Aldor,31162 -Goldwine,Walda,31162 -Goldwine,Folca,31162 -Goldwine,Thengel,31162 -Goldwine,Gram,31162 -Folcwine,Aldor,31162 -Folcwine,Walda,31162 -Folcwine,Folca,31162 -Folcwine,Thengel,31162 -Folcwine,Gram,31162 -Aldor,Walda,31162 -Aldor,Folca,31162 -Aldor,Thengel,31162 -Aldor,Gram,31162 -Walda,Folca,31162 -Walda,Thengel,31162 -Walda,Gram,31162 -Folca,Thengel,31162 -Folca,Gram,31162 -Thengel,Gram,31162 -Éomer,Théoden,31163 -Éowyn,Éomer,31164 -Éowyn,Théoden,31164 -Éomer,Théoden,31164 -Éowyn,Faramir,31166 -Éowyn,Faramir,31167 -Éowyn,Éomer,31168 -Éowyn,Aragorn,31168 -Éomer,Aragorn,31168 -Arwen,Faramir,31170 -Arwen,Imrahil,31170 -Arwen,Aragorn,31170 -Faramir,Imrahil,31170 -Faramir,Aragorn,31170 -Imrahil,Aragorn,31170 -Éowyn,Meriadoc,31172 -Éowyn,Éomer,31172 -Éowyn,Merry,31172 -Meriadoc,Éomer,31172 -Meriadoc,Merry,31172 -Éomer,Merry,31172 -Éowyn,Merry,31174 -Éowyn,Merry,31178 -Gimli,Legolas,31180 -Treebeard,Gandalf,31188 -Treebeard,Gandalf,31192 -Treebeard,Aragorn,31197 -Treebeard,Gandalf,31202 -Treebeard,Merry,31202 -Gandalf,Merry,31202 -Galadriel,Celeborn,31245 -Gimli,Legolas,31247 -Treebeard,Galadriel,31255 -Treebeard,Celeborn,31255 -Galadriel,Celeborn,31255 -Galadriel,Celeborn,31259 -Pippin,Merry,31261 -Pippin,Aragorn,31268 -Pippin,Aragorn,31270 -Celeborn,Galadriel,31274 -Celeborn,Aragorn,31274 -Galadriel,Aragorn,31274 -Gandalf,Saruman,31282 -Saruman,Galadriel,31295 -Saruman,Gandalf,31313 -Saruman,Wormtongue,31313 -Gandalf,Wormtongue,31313 -Saruman,Merry,31330 -Saruman,Wormtongue,31330 -Merry,Wormtongue,31330 -Galadriel,Celeborn,31349 -Gandalf,Elrond,31350 -Frodo,Galadriel,31356 -Sam,Elrond,31357 -Sam,Frodo,31389 -Sam,Elrond,31389 -Frodo,Elrond,31389 -Pippin,Merry,31401 -Pippin,Bilbo,31402 -Frodo,Bilbo,31406 -Sam,Aragorn,31411 -Sam,Frodo,31423 -Gandalf,Bilbo,31436 -Gandalf,Elrond,31436 -Bilbo,Elrond,31436 -Frodo,Elrond,31437 -Frodo,Gandalf,31445 -Pippin,Sam,31474 -Pippin,Sam,31475 -Gandalf,Butterbur,31524 -Gandalf,Barliman,31581 -Butterbur,Merry,31614 -Sam,Bill,31626 -Sam,Bill,31629 -Sam,Bill,31651 -Frodo,Barliman,31652 -Pippin,Gandalf,31656 -Pippin,Lotho,31656 -Gandalf,Lotho,31656 -Merry,Gandalf,31658 -Gandalf,Shadowfax,31684 -Frodo,Lotho,31707 -Pippin,Merry,31716 -Pippin,Merry,31717 -Merry,Bill,31723 -Sam,Bill,31731 -Robin,Sam,31816 -Sam,Frodo,31834 -Frodo,Merry,31844 -Pippin,Sam,31845 -Pippin,Merry,31845 -Sam,Merry,31845 -Sam,Frodo,31863 -Sam,Merry,31875 -Sam,Bill,31880 -Pippin,Frodo,31930 -Pippin,Merry,31930 -Pippin,Sam,31930 -Frodo,Merry,31930 -Frodo,Sam,31930 -Merry,Sam,31930 -Frodo,Lotho,31940 -Pippin,Frodo,31943 -Frodo,Lotho,31950 -Pippin,Lotho,31957 -Sam,Frodo,32044 -Tom,Frodo,32058 -Tom,Sam,32058 -Frodo,Sam,32058 -Frodo,Merry,32075 -Peregrin,Lotho,32080 -Frodo,Merry,32095 -Sam,Frodo,32234 -Sam,Frodo,32238 -Sam,Frodo,32239 -Pippin,Merry,32283 -Meriadoc,Peregrin,32293 -Pippin,Sam,32295 -Pippin,Merry,32295 -Sam,Merry,32295 -Frodo,Merry,32296 -Pippin,Frodo,32301 -Pippin,Merry,32301 -Pippin,Sam,32301 -Frodo,Merry,32301 -Frodo,Sam,32301 -Merry,Sam,32301 -Sam,Frodo,32331 -Merry,Lotho,32348 -Merry,Saruman,32355 -Gandalf,Saruman,32372 -Saruman,Wormtongue,32396 -Saruman,Wormtongue,32399 -Frodo,Saruman,32400 -Sam,Frodo,32404 -Frodo,Saruman,32409 -Frodo,Wormtongue,32422 -Saruman,Wormtongue,32432 -Saruman,Wormtongue,32443 -Frodo,Wormtongue,32444 -Pippin,Fredegar,32459 -Frodo,Lotho,32469 -Pippin,Merry,32473 -Sam,Frodo,32499 -Sam,Frodo,32520 -Sam,Frodo,32523 -Pippin,Merry,32524 -Sam,Frodo,32525 -Sam,Frodo,32527 -Sam,Frodo,32538 -Pippin,Merry,32541 -Sam,Frodo,32544 -Sam,Frodo,32547 -Meriadoc,Peregrin,32548 -Meriadoc,Sam,32548 -Peregrin,Sam,32548 -Sam,Frodo,32552 -Sam,Frodo,32557 -Sam,Frodo,32563 -Sam,Frodo,32565 -Sam,Frodo,32568 -Sam,Frodo,32569 -Sam,Bilbo,32570 -Sam,Frodo,32573 -Sam,Frodo,32579 -Sam,Bilbo,32579 -Frodo,Bilbo,32579 -Sam,Frodo,32584 -Frodo,Bilbo,32586 -Frodo,Bilbo,32595 -Sam,Frodo,32597 -Sam,Frodo,32598 -Sam,Frodo,32599 -Sam,Bill,32599 -Frodo,Bill,32599 -Sam,Frodo,32603 -Sam,Frodo,32613 -Elrond,Sam,32614 -Elrond,Galadriel,32614 -Sam,Galadriel,32614 -Elrond,Galadriel,32619 -Sam,Frodo,32631 -Pippin,Merry,32645 -Pippin,Goldilocks,32645 -Merry,Goldilocks,32645 -Galadriel,Elrond,32650 -Sam,Frodo,32651 -Sam,Bilbo,32651 -Frodo,Bilbo,32651 -Frodo,Gandalf,32656 -Pippin,Merry,32659 -Sam,Gandalf,32663 -Pippin,Frodo,32666 -Pippin,Merry,32666 -Pippin,Galadriel,32666 -Pippin,Sam,32666 -Frodo,Merry,32666 -Frodo,Galadriel,32666 -Frodo,Sam,32666 -Merry,Galadriel,32666 -Merry,Sam,32666 -Galadriel,Sam,32666 -Pippin,Merry,32671 -Pippin,Merry,32674 diff --git a/docs/source/getting_started/intro_tutorials/lotr.ipynb b/docs/source/getting_started/intro_tutorials/lotr.ipynb deleted file mode 100644 index c1c6dd0bac..0000000000 --- a/docs/source/getting_started/intro_tutorials/lotr.ipynb +++ /dev/null @@ -1,384 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Example: Lord of the rings \n", - "\n", - "Now that we know Raphtory is installed and running, let's look at the different ways to get some real data into a graph. \n", - "\n", - "For this first set of tutorials we are going to be building graphs from a Lord of the Rings dataset, looking at when characters interact throughout the trilogy 🧝🏻‍♀️🧙🏻‍♂️💍.\n", - " \n", - "

\n", - " \"Intro\n", - "

\n", - "\n", - "As with the quick start install guide, this and all following python pages are built as iPython notebooks. If you want to follow along on your own machine, click the `open on github` link in the top right of this page.\n", - "\n", - "## Let's have a look at the example data\n", - "\n", - "The data we are going to use is two `csv` files which will be pulled from our Github data repository. These are the structure of the graph (`lotr.csv`) and some metadata about the characters (`lotr_properties.csv`)\n", - "\n", - "For the structure file each line contains two characters that appeared in the same sentence, along with the sentence number, which we will use as a `timestamp`. The first line of the file is `Gandalf,Elrond,33` which tells us that Gandalf and Elrond appears together in sentence 33.\n", - "\n", - "For the properties file each line gives a characters name, their race and gender. For example `Gimli,dwarf,male`.\n", - "\n", - "\n", - "### Downloading the csv from Github 💾\n", - "\n", - "The following `curl` command will download the csv files and save them in the `tmp` directory on your computer. This will be deleted when you restart your computer, but it's only a couple of KB in any case.\n", - "\n" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "metadata": { - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "****Downloading Data****\n", - " % Total % Received % Xferd Average Speed Time Time Time Current\n", - " Dload Upload Total Spent Left Speed\n", - "100 52206 100 52206 0 0 154k 0 --:--:-- --:--:-- --:--:-- 160k\n", - " % Total % Received % Xferd Average Speed Time Time Time Current\n", - " Dload Upload Total Spent Left Speed\n", - "100 686 100 686 0 0 2906 0 --:--:-- --:--:-- --:--:-- 2995\n", - " % Total % Received % Xferd Average Speed Time Time Time Current\n", - " Dload Upload Total Spent Left Speed\n", - "100 69632 100 69632 0 0 287k 0 --:--:-- --:--:-- --:--:-- 296k\n", - "****LOTR GRAPH STRUCTURE****\n", - "Gandalf,Elrond,33\n", - "Frodo,Bilbo,114\n", - "Blanco,Marcho,146\n", - "****LOTR GRAPH PROPERTIES****\n", - "Aragorn,men,male\n", - "Gandalf,ainur,male\n", - "Goldberry,ainur,female\n" - ] - } - ], - "source": [ - "print(\"****Downloading Data****\")\n", - "!curl -o /tmp/lotr.csv https://raw.githubusercontent.com/Raphtory/Data/main/lotr.csv\n", - "!curl -o /tmp/lotr_properties.csv https://raw.githubusercontent.com/Raphtory/Data/main/lotr_properties.csv\n", - "!curl -o /tmp/lotr.db https://raw.githubusercontent.com/Raphtory/Data/main/lotr.db\n", - "print(\"****LOTR GRAPH STRUCTURE****\")\n", - "!head -n 3 /tmp/lotr.csv\n", - "print(\"****LOTR GRAPH PROPERTIES****\")\n", - "!head -n 3 /tmp/lotr_properties.csv" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Setting up our imports and Raphtory\n", - "Now that we have our data we can sort out our imports and create the Raphtory `Graph` which we will use to build our graphs.\n", - "\n", - "The imports are for parsing CSV files, accessing pandas dataframes, and bringing in all the Raphtory classes we will use in the tutorial.\n", - "\n", - "The filenames are pointing at the data we just downloaded. If you change the download location above, make sure to change them here as well." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "import csv\n", - "import pandas as pd\n", - "from raphtory import Graph\n", - "\n", - "structure_file = \"/tmp/lotr.csv\"\n", - "properties_file = \"/tmp/lotr_properties.csv\"\n", - "graph = Graph(1)\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Adding data directly into the Graph\n", - "\n", - "The simplest way to add data into a graph is to directly call the `add_vertex` and `add_edge` functions, which we saw in the quick start guide. These have required arguments defining the time the addition occurred and an identifier for the entity being updated. These functions, however, have several optional arguments allowing us to add `properties` and within this, `types`, on top of the base structure. \n", - "\n", - "\n", - "| Function | Required Arguments | Optional Arguments |\n", - "|--------------|-------------------------------|----------------------------------------------|\n", - "| `add_vertex` | `timestamp`,`vertex_id` | `properties` |\n", - "| `add_edge` | `timestamp`,`src_id`,`dst_id` | `properties` |\n", - "\n", - "\n", - "Lets take a look at this with our example data. In the below code we are opening The Lord of The Rings structural data via the csv reader and looping through each line. \n", - "\n", - "To insert the data we:\n", - "\n", - "* Extract the two characters names, referring to them as the `source_node` and `destination_node`.\n", - "* Extract the sentence number, referring to is as `timestamp`. This is then cast to an `int` as `epoch` timestamps in Raphtory must be a number.\n", - "* Call `add_vertex` for both nodes, setting their type to `Character`.\n", - "* Create an edge between them via `add_edge` and label this a `Co-occurence`." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "with open(structure_file, 'r') as csvfile:\n", - " datareader = csv.reader(csvfile)\n", - " for row in datareader:\n", - "\n", - " source_node = row[0]\n", - " destination_node = row[1]\n", - " timestamp = int(row[2])\n", - " \n", - " graph.add_vertex(timestamp, source_node, {\"vertex_type\": \"Character\"})\n", - " graph.add_vertex(timestamp, destination_node, {\"vertex_type\": \"Character\"}) \n", - " graph.add_edge(timestamp, source_node, destination_node, {\"edge_type\": \"Character_Co-occurence\"})" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Let's see if the data has ingested\n", - "\n", - "To do this, much like the quick start, we can run a query on our graph. As Raphtory allows us to explore the network's history, lets add a bit of this in as well. \n", - "\n", - "Below we check the data contained in the graph by running the `earliest_time()`, `latest_time()`, and `len` the vertices and edges. " - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "metadata": { - "tags": [] - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Earliest time: 33\n", - "Latest time: 32674\n", - "Number of vertices: 139\n", - "Number of edges: 701\n" - ] - } - ], - "source": [ - "print(\"Earliest time: %i\" % graph.earliest_time())\n", - "print(\"Latest time: %i\" % graph.latest_time())\n", - "\n", - "print(\"Number of vertices: %i\" % len(graph.vertices()))\n", - "print(\"Number of edges: %i\" % len(graph.edges()))" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can also access a specific vertex, such as `Gandalf`, and see his degree at different points in time using the `at()` function. \n", - "\n", - "In the first call, we get the entire graph at time 1000, and then check the degree of gandalf. \n", - "\n", - "In the second call, we get the vertex gandalf, get their instance at time 10,000 and the degree. " - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Gandalf's degree at 1000: 4\n", - "Gandalf's degree at 10,000: 26\n" - ] - } - ], - "source": [ - "print(\"Gandalf's degree at 1000: %i\" % graph.at(1000).vertex(\"Gandalf\").degree())\n", - "\n", - "print(\"Gandalf's degree at 10,000: %i\" % graph.vertex(\"Gandalf\").at(10000).degree())" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Updating graphs, merging datasets and adding properties\n", - "\n", - "One cool thing about Raphtory is that we can freely insert new information at any point in time and it will be automatically inserted in chronological order. This makes it really easy to merge datasets or ingest out of order data. \n", - "\n", - "A property on a vertex or edge can be either static or non-static. \n", - "\n", - "* Static properties, do not change and are fixed throughout the life of the graph, e.g. the `name` property. \n", - "* Non-static properties can change over time, e.g. `balance` of a bank account. \n", - "\n", - "All property objects require the user to specify a name and value.\n", - "\n", - "To explore this and to add some properties to our graph, lets load our second dataset!\n", - "\n", - "Below we are opening our property file the same way as the structure file. This data does not have a time element, so we can add the properties as static properties. This means they will be available at evert point in time and the values will stay the same. \n", - "\n", - "Now it's worthwhile noting that we aren't calling a function called `update_vertex` or something similar, even though we know the vertex exists. This is because everything is considered an addition into the history and Raphtory sorts all the ordering internally!" - ] - }, - { - "cell_type": "code", - "execution_count": 23, - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "with open(properties_file, 'r') as csvfile:\n", - " datareader = csv.reader(csvfile)\n", - " for row in datareader:\n", - " graph.add_vertex_properties(row[0], {\"race\": row[1],\"gender\": row[2]})\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Using our properties as part of a query\n", - "To quickly see if our new properties are included in the graph we can write a new query! Lets have a look at the dwarves who have the most interactions.\n", - "\n", - "To start we can create a function which for each vertex and check the size of exploded edges. This takes each edge and measures how many times it was updated. E.g. if Gimli and Balin met four times, in the graph they have one edge between them. But if we explode this edge, we can see each time they met. \n", - "\n", - "We can iterate through each vertex and filter by the **race** property and remove anyone who isn't a **dwarf**.\n", - "\n", - "Finally, we can sort the data into a dataframe to see **Gimli** has by far the most!" - ] - }, - { - "cell_type": "code", - "execution_count": 51, - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
timestampnameinteractions
331247Gimli185
131129Glóin31
210938Balin14
09605Thorin5
\n", - "
" - ], - "text/plain": [ - " timestamp name interactions\n", - "3 31247 Gimli 185\n", - "1 31129 Glóin 31\n", - "2 10938 Balin 14\n", - "0 9605 Thorin 5" - ] - }, - "execution_count": 51, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "result = []\n", - "# This returns an iterator, so we should store the value to avoid a deadlock\n", - "vertices = list(graph.vertices())\n", - "\n", - "for vertex in vertices:\n", - " if vertex.property(\"race\") == \"dwarf\":\n", - " interactions = sum([len(e.explode()) for e in vertex.edges()])\n", - " latest = vertex.latest_time()\n", - " result.append({\"timestamp\": latest, \"name\": vertex.name(), \"interactions\": interactions })\n", - "\n", - "pd.DataFrame(result).sort_values(by=\"interactions\",ascending=False) " - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.10.9" - }, - "vscode": { - "interpreter": { - "hash": "a9a34730827747ae273d5a5e0748f342e2039a3997e32d9a086d01739bd0f055" - } - } - }, - "nbformat": 4, - "nbformat_minor": 4 -} diff --git a/docs/source/getting_started/overview.rst b/docs/source/getting_started/overview.rst deleted file mode 100644 index e3b3757952..0000000000 --- a/docs/source/getting_started/overview.rst +++ /dev/null @@ -1,48 +0,0 @@ -.. _overview: - -{{ header }} - -**************** -Package overview -**************** - -Raphtory is an in-memory graph tool written in Rust with friendly Python APIs on top. -It is blazingly fast, scales to hundreds of millions of edges -on your laptop, and can be dropped into your existing pipelines. - -It supports time traveling, multilayer modelling, and advanced analytics beyond simple querying like -community evolution, dynamic scoring, and mining temporal motifs. - -Successful contributions will be reward with swizzling swag! - -Getting support ---------------- - -The first stop for raphtory issues and ideas is the `GitHub Issue Tracker -`__. If you have a general question, -raphtory community experts can answer through `Slack -`__. - -Bounty Board ------------- - -We offer bounties for the following contributions to the project, please see below to -win some cool swag! `Our Github bounty board `__ - -Community ---------- - -Raphtory is actively supported today by a community of software engineering experts and world-class researchers around -the world. Thanks to `pometry `__ and `all of our contributors `__. - -If you're interested in contributing, please visit the :ref:`contributing guide `. - -Development team ------------------ - -The list of the Core Team members and more detailed information can be found on the `pometry website `__. - -License -------- - -.. literalinclude:: ../../../LICENSE diff --git a/docs/source/images/index_api.svg b/docs/source/images/index_api.svg deleted file mode 100644 index c9eee89983..0000000000 --- a/docs/source/images/index_api.svg +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/source/images/index_contribute.svg b/docs/source/images/index_contribute.svg deleted file mode 100644 index 91b4140ec5..0000000000 --- a/docs/source/images/index_contribute.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - image/svg+xml - - - - - - - - - - - - \ No newline at end of file diff --git a/docs/source/images/index_getting_started.svg b/docs/source/images/index_getting_started.svg deleted file mode 100644 index 3756254230..0000000000 --- a/docs/source/images/index_getting_started.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - image/svg+xml - - - - - - - - - \ No newline at end of file diff --git a/docs/source/images/index_user_guide.svg b/docs/source/images/index_user_guide.svg deleted file mode 100644 index 4eee142cdf..0000000000 --- a/docs/source/images/index_user_guide.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - image/svg+xml - - - - - - - - - \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 76856225c0..1f33acfc47 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -1,112 +1,91 @@ -:notoc: +.. _api_menu: -.. Raphtory documentation master file, created by +{{ header }} -.. module:: Raphtory +**************** +Raphtory +**************** -**************************************** -Raphtory documentation -**************************************** +.. automodule:: raphtory + :autosummary: + :members: + :undoc-members: + :show-inheritance: + :private-members: + :inherited-members: -**Date**: |today| **Version**: |version| +Algorithms +------------------- -**Useful links**: -`Source Repository `__ | -`Issues & Ideas `__ | -`Slack Support `__ +.. automodule:: raphtory.algorithms + :autosummary: + :members: + :undoc-members: + :show-inheritance: + :private-members: + :inherited-members: -:mod:`Raphtory` is an in-memory graph tool written in Rust with friendly Python APIs on top. -It is blazingly fast, scales to hundreds of millions of edges on your laptop, and can be -dropped into your existing pipelines with a simple `pip install raphtory`. +Visualisation +------------------- -.. grid:: 1 2 2 2 - :gutter: 4 - :padding: 2 2 0 0 - :class-container: sd-text-center +.. automodule:: raphtory.export + :autosummary: + :members: + :undoc-members: + :show-inheritance: + :private-members: + :inherited-members: - .. grid-item-card:: Getting started - :img-top: images/index_getting_started.svg - :class-card: intro-card - :shadow: md - New to *Raphtory*? Check out the getting started guides. They contain an - introduction to *Raphtory'* main concepts and links to additional tutorials. +Null Models +-------------------------- - +++ +.. automodule:: raphtory.nullmodels + :autosummary: + :members: + :undoc-members: + :show-inheritance: + :private-members: + :inherited-members: - .. button-ref:: getting_started - :ref-type: ref - :click-parent: - :color: secondary - :expand: +Graph Generation +-------------------------- + +.. automodule:: raphtory.graph_gen + :autosummary: + :members: + :undoc-members: + :show-inheritance: + :private-members: + :inherited-members: + +GraphQL Server +-------------------------- + +.. automodule:: raphtory.graphql + :autosummary: + :members: + :undoc-members: + :show-inheritance: + :inherited-members: - To the getting started guides - .. grid-item-card:: User guide - :img-top: images/index_user_guide.svg - :class-card: intro-card - :shadow: md - The user guide provides in-depth information on the - key concepts of Raphtory with useful background information and explanation. - +++ - .. button-ref:: user_guide - :ref-type: ref - :click-parent: - :color: secondary - :expand: - To the user guide - .. grid-item-card:: API reference - :img-top: images/index_api.svg - :class-card: intro-card - :shadow: md - The reference guide contains a detailed description of - the Raphtory API. The reference describes how the methods work and which parameters can - be used. It assumes that you have an understanding of the key concepts. - +++ - .. button-ref:: api - :ref-type: ref - :click-parent: - :color: secondary - :expand: - To the reference guide - .. grid-item-card:: Developer guide - :img-top: images/index_contribute.svg - :class-card: intro-card - :shadow: md - Saw a typo in the documentation? Want to improve - existing functionalities? The contributing guidelines will guide - you through the process of improving Raphtory. - +++ - .. button-ref:: development - :ref-type: ref - :click-parent: - :color: secondary - :expand: - To the development guide -.. toctree:: - :maxdepth: 3 - :hidden: - :titlesonly: - getting_started/index - development/index - api/index - userguide/index diff --git a/docs/source/userguide/graphql.rst b/docs/source/userguide/graphql.rst deleted file mode 100644 index 7f37f323d9..0000000000 --- a/docs/source/userguide/graphql.rst +++ /dev/null @@ -1,5 +0,0 @@ -{{ header }} - -******************** -GraphQL -******************** diff --git a/docs/source/userguide/index.rst b/docs/source/userguide/index.rst deleted file mode 100644 index c2ff591b2c..0000000000 --- a/docs/source/userguide/index.rst +++ /dev/null @@ -1,18 +0,0 @@ -{{ header }} - -.. _user_guide: - -========== -User Guide -========== - -This guide explains each component of raphtory. - -.. toctree:: - :maxdepth: 2 - - raphtory - rust - js - graphql - io \ No newline at end of file diff --git a/docs/source/userguide/io.rst b/docs/source/userguide/io.rst deleted file mode 100644 index 873ab6a0f4..0000000000 --- a/docs/source/userguide/io.rst +++ /dev/null @@ -1,5 +0,0 @@ -{{ header }} - -******************** -IO -******************** \ No newline at end of file diff --git a/docs/source/userguide/js.rst b/docs/source/userguide/js.rst deleted file mode 100644 index 655c049d97..0000000000 --- a/docs/source/userguide/js.rst +++ /dev/null @@ -1,5 +0,0 @@ -{{ header }} - -******************** -JS -******************** \ No newline at end of file diff --git a/docs/source/userguide/raphtory.rst b/docs/source/userguide/raphtory.rst deleted file mode 100644 index da063304f4..0000000000 --- a/docs/source/userguide/raphtory.rst +++ /dev/null @@ -1,5 +0,0 @@ -{{ header }} - -******************** -Raphtory -******************** \ No newline at end of file diff --git a/docs/source/userguide/rust.rst b/docs/source/userguide/rust.rst deleted file mode 100644 index 5f2b2dbc24..0000000000 --- a/docs/source/userguide/rust.rst +++ /dev/null @@ -1,5 +0,0 @@ -{{ header }} - -******************** -Rust -******************** \ No newline at end of file diff --git a/examples/custom-algorithm/src/main.rs b/examples/custom-algorithm/src/main.rs index 2d4590edf7..97c250f6f8 100644 --- a/examples/custom-algorithm/src/main.rs +++ b/examples/custom-algorithm/src/main.rs @@ -2,7 +2,7 @@ use async_graphql::dynamic::{FieldValue, ResolverContext, TypeRef}; use async_graphql::FieldResult; use dynamic_graphql::internal::TypeName; use dynamic_graphql::SimpleObject; -use raphtory::db::view_api::GraphViewOps; +use raphtory::db::view::GraphViewOps; use raphtory_graphql::{Algorithm, RaphtoryServer}; #[derive(SimpleObject)] @@ -29,7 +29,7 @@ impl Algorithm for DummyAlgorithm { ) -> FieldResult>> { let mandatory_arg = ctx.args.try_get("mandatoryArg")?.u64()?; let optional_arg = ctx.args.get("optionalArg").map(|v| v.u64()).transpose()?; - let num_vertices = graph.num_vertices(); + let num_vertices = graph.count_vertices(); let output = Self { number_of_nodes: num_vertices, message: format!("mandatory arg: '{mandatory_arg}', optional arg: '{optional_arg:?}'"), diff --git a/examples/custom_python_extension/Cargo.toml b/examples/custom_python_extension/Cargo.toml new file mode 100644 index 0000000000..e78c07ab21 --- /dev/null +++ b/examples/custom_python_extension/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "custom_python_extension" +version = "0.1.1" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +raphtory = {path = "../../raphtory", features = ["python"]} +pyo3 = "0.19.2" + +[lib] +crate-type = ["cdylib"] + + +[build-dependencies] +pyo3-build-config = "0.19.2" diff --git a/examples/custom_python_extension/build.rs b/examples/custom_python_extension/build.rs new file mode 100644 index 0000000000..dace4a9ba9 --- /dev/null +++ b/examples/custom_python_extension/build.rs @@ -0,0 +1,3 @@ +fn main() { + pyo3_build_config::add_extension_module_link_args(); +} diff --git a/examples/custom_python_extension/pyproject.toml b/examples/custom_python_extension/pyproject.toml new file mode 100644 index 0000000000..ccc82af269 --- /dev/null +++ b/examples/custom_python_extension/pyproject.toml @@ -0,0 +1,31 @@ +[build-system] +requires = ["maturin>=0.13,<0.14"] +build-backend = "maturin" + +[project] +name = "custom_python_extension" +requires-python = ">=3.8" +classifiers = [ + "Programming Language :: Rust", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] +dependencies = [ + "raphtory" +] + + +[project.urls] +homepage = "https://github.com/pometry/raphtory" +documentation = "https://docs.raphtory.com/" +repository = "https://github.com/pometry/raphtory" +twitter = "https://twitter.com/raphtory/" +slack = "https://join.slack.com/t/raphtory/shared_invite/zt-xbebws9j-VgPIFRleJFJBwmpf81tvxA" +youtube = "https://www.youtube.com/@pometry8546/videos" + +[project.optional-dependencies] +export = ["pyvis >= 0.3.2", "networkx >= 2.6.3", "matplotlib >= 3.4.3", "seaborn >= 0.11.2"] + +[tool.maturin] +features = ["pyo3/extension-module"] +python-source = "python" diff --git a/examples/custom_python_extension/python/custom_python_extension/__init__.py b/examples/custom_python_extension/python/custom_python_extension/__init__.py new file mode 100644 index 0000000000..def2f54143 --- /dev/null +++ b/examples/custom_python_extension/python/custom_python_extension/__init__.py @@ -0,0 +1 @@ +from .custom_python_extension import * diff --git a/examples/custom_python_extension/src/lib.rs b/examples/custom_python_extension/src/lib.rs new file mode 100644 index 0000000000..32544ca621 --- /dev/null +++ b/examples/custom_python_extension/src/lib.rs @@ -0,0 +1,17 @@ +use pyo3::prelude::*; +use raphtory::{db::api::view::internal::DynamicGraph, prelude::GraphViewOps}; + +fn custom_algorithm(graph: &G) -> usize { + graph.count_vertices() +} + +#[pyfunction(name = "custom_algorithm")] +fn py_custom_algorithm(graph: DynamicGraph) -> usize { + custom_algorithm(&graph) +} + +#[pymodule] +fn custom_python_extension(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + m.add_function(wrap_pyfunction!(py_custom_algorithm, m)?)?; + Ok(()) +} diff --git a/examples/custom_python_extension/test/test_custom_algorithm.py b/examples/custom_python_extension/test/test_custom_algorithm.py new file mode 100644 index 0000000000..3b6f0d846c --- /dev/null +++ b/examples/custom_python_extension/test/test_custom_algorithm.py @@ -0,0 +1,16 @@ +from raphtory import Graph +from custom_python_extension import custom_algorithm +from pytest import raises + + +def test_custom_algorithm(): + g = Graph() + for v in range(10): + g.add_vertex(0, v) + assert custom_algorithm(g) == 10 + + +def test_error_for_wrong_type(): + """ calling with the wrong type should still raise a type error (unless it defines a bincode method)""" + with raises(TypeError): + custom_algorithm(1) diff --git a/examples/py/companies_house/companies_house_example.ipynb b/examples/py/companies_house/companies_house_example.ipynb index 314770a02e..742538842f 100644 --- a/examples/py/companies_house/companies_house_example.ipynb +++ b/examples/py/companies_house/companies_house_example.ipynb @@ -25,7 +25,7 @@ "import os, json\n", "import matplotlib.pyplot as plt\n", "from raphtory import Graph\n", - "from raphtory import vis\n", + "from raphtory import export\n", "from datetime import datetime, timedelta" ] }, @@ -617,7 +617,7 @@ "metadata": {}, "outputs": [], "source": [ - "vis.to_pyvis(graph=g2, edge_color='#F6E1D3',shape=\"image\") " + "export.to_pyvis(graph=g2, edge_color='#F6E1D3',shape=\"image\") " ] }, { diff --git a/examples/py/companies_house/companies_house_visualisation.ipynb b/examples/py/companies_house/companies_house_visualisation.ipynb index 13003cde53..dc736f8243 100644 --- a/examples/py/companies_house/companies_house_visualisation.ipynb +++ b/examples/py/companies_house/companies_house_visualisation.ipynb @@ -26,7 +26,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -56,30 +56,11 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Warning: When cdn_resources is 'local' jupyter notebook has issues displaying graphics on chrome/safari. Use cdn_resources='in_line' or cdn_resources='remote' if you have issues viewing graphics in a notebook.\n", - "nx.html\n" - ] - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ - "from raphtory import vis\n", + "from raphtory import export\n", "from raphtory import Graph\n", "import requests\n", "from contextlib import closing\n", @@ -105,8 +86,8 @@ " g.add_edge(1, src, company, properties={\"title\": title, \"weight\": share})\n", " g.add_edge(1, dst, company, properties={\"title\": title, \"weight\": share})\n", "\n", - "vis.to_networkx(graph=g, k=0.8, node_size=250, node_color=\"red\")\n", - "vis.to_pyvis(graph=g, edge_color='#F6E1D3', edge_weight=\"weight\", shape=\"image\", edge_label=\"title\", node_image=\"image\") " + "export.to_networkx(graph=g)\n", + "export.to_pyvis(graph=g, edge_color='#F6E1D3', edge_weight=\"weight\", shape=\"image\", edge_label=\"title\", node_image=\"image\") " ] } ], @@ -126,7 +107,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.9" + "version": "3.11.3" } }, "nbformat": 4, diff --git a/examples/py/math_overflow/degree-trajectories-top5.png b/examples/py/math_overflow/degree-trajectories-top5.png new file mode 100644 index 0000000000..b9e26a1b9d Binary files /dev/null and b/examples/py/math_overflow/degree-trajectories-top5.png differ diff --git a/examples/py/math_overflow/mo_investigate.ipynb b/examples/py/math_overflow/mo_investigate.ipynb new file mode 100644 index 0000000000..141cd7ee9c --- /dev/null +++ b/examples/py/math_overflow/mo_investigate.ipynb @@ -0,0 +1,969 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import pandas as pd\n", + "import raphtory as rp\n", + "import datetime as dt\n", + "import networkx as nx\n", + "\n", + "import seaborn as sns\n", + "sns.set(font_scale=1.2)\n", + "import matplotlib.pyplot as plt\n", + "\n", + "from plotting_utils import *" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Reading in a graph of interactions on Stack Exchange" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
srcdsttime
0141254192988
1341254194656
2121254202612
32511254232804
414161254263166
............
50654521349885801457260401
5065462003111141457261526
5065475142811141457261724
50654856259562591457261848
50654956353563531457262355
\n", + "

506550 rows × 3 columns

\n", + "
" + ], + "text/plain": [ + " src dst time\n", + "0 1 4 1254192988\n", + "1 3 4 1254194656\n", + "2 1 2 1254202612\n", + "3 25 1 1254232804\n", + "4 14 16 1254263166\n", + "... ... ... ...\n", + "506545 21349 88580 1457260401\n", + "506546 20031 1114 1457261526\n", + "506547 51428 1114 1457261724\n", + "506548 56259 56259 1457261848\n", + "506549 56353 56353 1457262355\n", + "\n", + "[506550 rows x 3 columns]" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "file = \"/Users/naomiarnold/CODE/Raphtory/raphtory-research/stackexchange/sx-mathoverflow.txt\"\n", + "sx_df = pd.read_csv(file, names = [\"src\", \"dst\", \"time\"],sep=\" \")\n", + "display(sx_df)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "def load_pandas(data:pd.DataFrame):\n", + " g = rp.Graph()\n", + " for (_, src_id, dst_id, time) in data.itertuples():\n", + " g.add_vertex(timestamp = time, id = src_id)\n", + " g.add_vertex(timestamp = time, id = dst_id)\n", + "\n", + " # Remove self loops\n", + " if src_id != dst_id:\n", + " g.add_edge(timestamp = time, src = src_id, dst = dst_id)\n", + " return g" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "g = load_pandas(sx_df)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "g.add_edge" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "g = nx.Graph()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "g.add_edge" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Working with the aggregate graph object\n", + "\n", + "Take metrics you might expect in something like networkx applied to the full aggregate graph. Get degree of all nodes for example" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[(11142, 968), (6094, 914), (290, 910), (297, 724), (1946, 721), (2841, 551), (18060, 539), (1459, 536), (1409, 506), (1149, 477)]\n" + ] + } + ], + "source": [ + "degrees = sorted([(v.id(), v.in_degree()) for v in g.vertices()], key = lambda x: x[1], reverse=True)\n", + "print(degrees[:10])" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Handy functionality for tracking vertex properties over time in different window sizes\n", + "Get the top 5 all time leaders by degree and track their connectivity over time" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "sns.set()\n", + "sns.set_palette(\"husl\")\n", + "\n", + "window_size = 2592000\n", + "\n", + "n = 5\n", + "leaders = [x[0] for x in degrees[:n]]\n", + "timestamps = [dt.datetime.fromtimestamp(v.latest_time()) for v in g.rolling(window = window_size,step=86400)]\n", + "\n", + "fig, ax = plt.subplots()\n", + "\n", + "for i,vid in enumerate(leaders):\n", + " deg = list(map( lambda v: v.in_degree(), g.vertex(vid).rolling(window = window_size,step=86400)))\n", + " ax.plot(timestamps,deg,label= get_ordinal_number(i+1) + \" node\")\n", + "\n", + "ax.legend()\n", + "ax.set_xlabel(\"Time\")\n", + "ax.set_ylabel(\"In-degree\")\n", + "plt.savefig(\"degree-trajectories-top5.png\")\n", + "plt.show()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Whole graph metrics and different window sizes, including the growing aggregate graph" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "window_sizes = [86400,604800,2592000,31536000]\n", + "window_names = [\"1 day\", \"1 week\", \"1 month\", \"1 year\"]\n", + "\n", + "# Aggregate graph properties\n", + "views = g.expanding(step = 86400)\n", + "timestamps = [dt.datetime.fromtimestamp(v.latest_time()) for v in views]\n", + "\n", + "aggr_vertices = [v.count_vertices() for v in views]\n", + "\n", + "agg_window = np.zeros(len(timestamps),dtype=int)\n", + "df = pd.DataFrame({\"time\": timestamps, \"window\":agg_window, \"vertices\": aggr_vertices})\n", + "\n", + "# Same properties for different window sizes\n", + "for i in range(3):\n", + " views = g.rolling(window=window_sizes[i], step=86400)\n", + " diff_size = int(window_sizes[i]/86400)\n", + "\n", + " timestamps = [dt.datetime.fromtimestamp(v.latest_time()) for v in views]\n", + " vertices = [v.count_vertices() for v in views]\n", + "\n", + " to_join = pd.DataFrame({\"time\": timestamps, \"window\":[window_sizes[i] for k in range(len(timestamps))], \"vertices\": vertices})\n", + " df = pd.concat([df,to_join],copy=False)\n", + "\n", + "for w in window_sizes:\n", + " diff_size = int(w / 86400)\n", + " df.loc[df['window'] == w, 'new_vertices'] = pd.Series(df.loc[df['window'] == 0, 'vertices'].diff(diff_size), index = df.loc[df['window'] == w, 'vertices'].index)\n", + " df.loc[df['window'] == w, 'prop_new'] = np.where(df.loc[df['window'] == w, 'vertices'] < 1, 0, df.loc[df['window'] == w, 'new_vertices'] / df.loc[df['window'] == w, 'vertices'])" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Plotting out the number of active users and the proportion of those that are new" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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r9ROfX016y966qmncuqpp7Jr1y7Ky8uJjY2lR48eLbJi3333XZ5++mlGjhyprFuxYgUzZszg+uuvZ8qUKezdu5d7772X8vJy5syZA8DWrVvJycnhvffe8zleXJw8XbNz506uvPJKLr74Yh577DF+++03brvtNhITExk7tuWp/yqRRZRc7Kra5JNG3pC5G65hRt+5SumGZcU/IyFh1JjDKtDqjScjsmGfPYBalxXoGFMPHmL1iYFLogRo/Oxdi2186qkROf+QhKOU5cKaPFIF35gwj8Hb3L9HW+D9EGnLjNhJOacxLOZYXC43EzpJTSmV5jEw4QgeGvY+d6/+HwICyaasVjtXsLjM+DYskB0KcYZ673+MPr5TG1DtSbPN8ZqaGnr0kF39P/zwA4sXL2bixIl069YtrOMUFBRw3333sXTpUr99P/jgA4444giuuuoqALp168ZNN93EPffcw/3334/BYGDbtm307NmT5OTAF+ibb75Jnz59uOkm2VWdm5vLpk2bePXVV1XjrYNxoHonT2++NWidNm8+3vM8gxPGIkmS0nvS7q5t9o0gRptATZGDdUWr2DxwDWna+tjNGlEO+u0IUw8eescOYUXJr37rNYIWl9vFbwWfs7Lkd07JupBaUU4iOjr1NGIi4Alzu90U5B8iubYHhcad7K3cTmpc/YOj3FGslMDoyMabd6/VJGNqq53H7XaTn58HQHZ26z28DxcON30lmzK4e9DLaAVdq3h3PfrSajXcOeop5iz3nZaNRKhEJPF0n4D26Vnrra+4uM4box+2q2zXrl0cf/zxvPzyywA8/fTT3HjjjcydO5fTTz897FIhGzduRK/X89VXXzFkyBCfbZdccgm33367r8AaDU6nE6tV9oZs3bqV3FzfAoDerFixws9IGzNmDCtXrlRq1Km0P2tK/2L2uktCMtwAKpwlgO+UYK5XQdVwidMnUbG7lordtXyz09eLW1PneWvvWkneZFoCX/MOt43Fhd/w0Z757KxazzObb+WbAwuAyCUPSJLEhg3rMecngQSrC//x2f7q9geU5Y48bRqlj2Vk0jF0i+pL16jIJCQEwqOvDRvWq/ecEDgc9dU1ug9ZUcGfUy2hXl/rGJEyzq+mWiRe2FoLbYSzbkPBW1+d+foK2+x9/PHH0el0HHvssTgcDt577z1OPPFEZs+ezR133MHTTz/N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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, (ax1, ax2) = plt.subplots(ncols=1,nrows=2, gridspec_kw={'height_ratios': [1, 2]}, sharex=True)\n", + "\n", + "for i in range(3):\n", + " sub_df = df[df['window'] == window_sizes[i]]\n", + " ax1.plot(sub_df['time'], sub_df['prop_new'],label=window_names[i])\n", + " ax2.plot(sub_df['time'],sub_df['vertices'],label=window_names[i])\n", + "\n", + "for i in range(6):\n", + " ax1.axvline(dt.datetime(2010+i,12,25),color=\"black\",linestyle=\"--\", alpha=0.3)\n", + " ax2.axvline(dt.datetime(2010+i,12,25),color=\"black\",linestyle=\"--\", alpha=0.3)\n", + "\n", + "ax2.set_xlabel(\"Time\")\n", + "ax1.set_ylabel(\"Proportion new\")\n", + "ax2.set_ylabel(\"Number of active users\")\n", + "\n", + "ax2.legend()\n", + "plt.tight_layout()\n", + "plt.savefig(\"new-existing-users.png\")" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Null model comparisons\n", + "Number of users according to real and timestamp-shuffled data" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "from raphtory.nullmodels import *\n", + "from raphtory.algorithms import weakly_connected_components\n", + "from collections import Counter\n", + "\n", + "experiments = 5\n", + "\n", + "for i in range(3):\n", + " w = window_sizes[i]\n", + " results_vertices = np.zeros((experiments,len(df[df['window']==w])))\n", + " for ex in range(experiments):\n", + " sx_shuffled = shuffle_column(sx_df,col_number=2)\n", + " g_shuff = load_pandas(sx_shuffled)\n", + " views = g_shuff.rolling(window=window_sizes[i],step=86400)\n", + " results_vertices[ex,:] = np.array([v.count_vertices() for v in views])\n", + " df.loc[df['window'] == w, 'vert_shuffled_mean'] = results_vertices.mean(axis=0)\n", + " df.loc[df['window'] == w, 'vert_shuffled_sd'] = results_vertices.std(axis=0)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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NtDQ4A1te73eVkoCAKR2BKUDBw7oV7/6laZOnapx48bpwgsv1PLly0PnP/74Y51//vkaM2aMvvvd7+rll1+OuL6+vl633367Jk+erOLiYl133XXat29fRJlYdQAAgPiEh6Pw0AQAaFncQennP/+5PvvsM91///164YUXNHz4cF122WXatGmTNm7cqDlz5mjKlClavHixLrjgAt144436+OOPQ9ffdttt+uCDD/TII4/oqaee0qZNmzR37tzQ+bbUAQAA4kOPEgDExxVP4a1bt+rDDz/UwoULNX78eEnSLbfcovfff1//+te/tHfvXg0dOlTXXnutJGnQoEFavXq1/vSnP2ny5MkqKyvTiy++qMcee0zHHXecJOn+++/Xd7/7XX322WcqLi7WU0891WodAAAgfuHhiKAEALHF1aPUrVs3Pf744xo1alTomGEYMgxDlZWVWr58ebMwM2nSJK1YsUKmaWrFihWhY40GDBigwsJCLVu2TJJi1gEAAOJHUAKA+MTVo5STk6OTTjop4thrr72mrVu36uabb9Y//vEPFRUVRZzv3r276urqtH//fpWVlalbt25KTU1tVqa0tFSSVFpa2modubm58TQ5xOVi3QoAgH15vfWhbZ/Py+9FwOacTgevAzHEFZSirVy5UjfddJNOO+00nXzyyfJ4PEpJSYko07jv9XpVV1fX7Lwkpaamqr4++AIeq472cDgMdeuW2a5rAQCwgvAHzjocJr8XAZvLzEzldSCGdgelN998U9dff73GjRun+fPnSwoGnugw07ifnp6utLS0FsNOfX290tPT21RHewQCpiora9t1LQAAyc40TXk8ntB+RUW19u+vSWCLACRaTU29LV8HcnLS5XS2rSetXUHpmWee0V133aXvfve7+u1vfxvq8enRo4fKy8sjypaXlysjI0PZ2dkqKirSgQMH5PV6I3qNysvLVVhY2KY62svnC7T7WgAAkln0B5Aej4ffi4DN+f0BXgdiiHtg4sKFC3XnnXfqBz/4ge6///6IwHPcccfp008/jSj/ySefaNy4cXI4HBo/frwCgUBoUQdJ2rx5s8rKyjRhwoQ21QEAAOJzqJEaAIBDiyt5bN68WXfffbe+853vaM6cOdqzZ492796t3bt3q6qqShdffLG+/PJLzZ8/Xxs3btSCBQv06quv6vLLL5ckFRYW6swzz9S8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0O+GoUOHaunSpaF6H3/8cU2dOlWjR4/WxRdfrC1bthyh74x9cA90QiaSxv/+7/+as2fPbrXMtm3bzFGjRpm33HKLuWHDBvPVV181J06caA4ZMsQsKSkxTdM0f/zjH5szZswwP//8c7OkpMT85z//aR577LHmn//8Z9M0TfOpp54yi4uLzbq6ulC9P//5z825c+ceta8NbcM9YC9t+f+ePXu2+atf/Sq0f+aZZ5pDhw41lyxZYpqmaZaVlZlDhw41165da5aWlpoTJ04077zzTnPDhg3mqlWrzCuvvNI85ZRTzJqaGtM0TfO+++4zTznlFPPtt982t27dai5atMgsLi42n3nmGdM0TfOTTz4J3UufffaZWVxcbD700ENH6TsA7gF7asv/e+P/w4wZM8wvv/zSXLlypTlhwgRzwoQJ5j333GNu2rTJfPDBB80hQ4aYa9euNU3TNP/85z+bI0eONJ955hlz8+bN5osvvmiOGzfO/PWvf22apmmWlJSYQ4YMMU866STzzTffNLdt22befvvt5rBhw8xt27aZdXV15pNPPmkOGTLELC8vN+vr682HH37YHDJkiDlnzhxzzZo15hdffGGefvrp5kUXXXTUv09Wxj3QOdCjZDHPP/+88vPzdeutt2rQoEE6/fTTddVVV0WUOeGEE/Sb3/xGY8aMUe/evXXOOedoxIgRWrdunSTp7LPPltfr1ZtvvilJqq6u1ptvvqnzzz+/w78exI97wF6mTZumDz/8UFJw2MbmzZt18sknhz7le/fdd9WrVy8NHTpUf/vb31RUVKR58+Zp0KBBGjlypB588EHt3btXr776qmpra/Xkk0/q5ptv1sknn6y+fftq5syZuvTSS/XEE09E/LtfffWVLr/8cl122WWaO3duh3/daMI9YG/XXHONRo0apeLiYk2aNEnp6em68cYbNWDAAM2ZM0eStH79+lBP4ezZs/WDH/xA/fv317nnnqu5c+fqb3/7m6qqqkJ1XnrppTr11FPVp08fXXvttQoEAvriiy+Ulpam7OxsSVJBQYFSUlIkSW63W/Pnz9ewYcM0evRoff/739dXX33V8d8Mm+IeOHqYo2Qx69at04gRI+R0OkPHxo0bF1Hmoosu0n/+8x/94x//0JYtW7RhwwZt375dAwcOlCR169ZNp556ql588UWdddZZeuWVV5Sdna0TTzyxQ78WtA/3gL2ccsopuueee1RSUqIVK1bo2GOP1UknnaSnnnpKUvBN8qmnnipJWr16tdavX6/i4uKIOurr67Vx40Zt2LBB9fX1uu666+RwNH2O5vP55PV65fF4QsduuOEGNTQ0qFevXh3wVaI13AP21q9fv9B2RkaGevfuLcMwJElpaWmSJK/Xq3379mnPnj0aP358xPUTJ05UQ0ODNm3apLy8PEnSoEGDQucb3xQ3NDQcsg15eXnKysoK7efk5ETcKzi6uAeOHoKSxRiGoUAgEHHM5Wr6bw4EApozZ47Wr1+vs846S9OnT9exxx6rW265JeKamTNn6sc//rH27t2rl156Seeee27EG290XtwD9tK/f38NHDhQH3zwgT777DNNnjxZkydP1m233abt27frww8/1GOPPSYp+H8/adIk3Xrrrc3qyc7OVklJiSTpwQcfDIXmcI2fHErS1VdfrYqKCv3mN7/RCSecoIKCgqP0FSIW7gF7C399lxQRcMOZh5iA3/j7Irye8P/nWNdL4ndDgnEPHD0MvbOYYcOG6auvvpLX6w0dC+/6XLNmjd577z099NBDuv7663XOOeeob9++2rZtW8QPwIknnqiCggI9//zzWr58OUOukgj3gP2ccsop+vDDD7V06VJNmjRJ/fv3V8+ePfW73/1OKSkpoU8PjznmGG3cuFE9evRQv3791K9fP3Xp0kV333231q1bp4EDB8rlcmnnzp2h8/369dO7776rJ554IuKX71lnnaW5c+cqKytLt912W4K+cjTiHkAs+fn5ys/P14oVKyKOL1++XG63W3379m1TPY09FUg+3APxIyhZzIUXXqi6ujrdfPPN2rhxo95++2098sgjofP5+flyuVx65ZVXVFJSolWrVulnP/uZdu/eHfHG2uFw6LzzztNjjz2mUaNGRXTBonPjHrCfadOm6d1339X+/ftDwywnTZqkf/7znzrppJNCnxJedNFFqqqq0vXXX6+1a9dq7dq1uvbaa7Vq1SoNGTJE2dnZ+v73v6+HHnpI//znP1VSUqJFixbp3nvvVffu3Zv9u+np6br99tv15ptv6t///neHfs2IxD2Atrjsssv0zDPPaOHChdq6dav+9a9/6Xe/+52+973vhYZXxZKRkSEp+AGcFYZW2Q33QHwIShZTWFiop556SqWlpZoxY4buueeeiIn8hYWFuueee/Sf//xH06dP1zXXXKPCwkJdeumlzSbdnX/++fJ4PPQkJBnuAfspLi5WZmamiouLlZqaKkk6/vjjFQgEQnNTJKlPnz565plnVFNTowsvvFCzZ8+W2+3W008/rdzcXEnSTTfdpEsuuUQPPfSQzjjjDP3hD3/Q3LlzdfXVV7f4b5944ok699xzdeedd2rv3r1H/4tFi7gH0BY/+tGP9L//+7966qmndOaZZ+qhhx7SFVdcoZtvvrnNdUyaNEljxozR97//fb399ttHsbU4GrgH4mOYrQ04hK0tXbpUc+bM0fvvv9/mTxlgLdwDAADArljMAc1s3LhR69at02OPPaYZM2bwBtmGuAcAAIDdMfQOzWzdulU33XSTunbtqmuvvTbRzUECcA8AAAC7Y+gdAAAAAEShRwkAAAAAohCU0KJf/OIXuvjiixPdDIQZOnSoFi9efFh1rFq1SmeccYZGjhyp3/72t832L774Yv3iF784rH/jSNSBzoHXgc6H1wEkAq8FnQ+vBR2DxRwAG/nDH/4gt9utJUuWKDs7W7fcckvE/ty5cxPdRABHGa8DACReC9qCoATYSEVFhYYPHx56+nb0PgDr43UAgMRrQVsw9C7JXXzxxbrlllt0wQUX6LjjjtNLL70kSXrhhRd0xhlnaPTo0TrjjDP01FNPKRAIhK5bvny5LrnkEo0bN04jR47UGWecoX/+85+J+jIgacuWLbrssss0fvx4FRcX67LLLtM333wTUWbz5s269NJLNWrUKE2ZMkV/+MMfQuceeeQRTZs2LaJ8+LFp06bp008/1YsvvqihQ4c229++fXuzNm3cuFFXXHGFiouLdeKJJ+q6667T7t27Q+e9Xq/uvvtuTZ48WePHj9e9994bcZ+hY/A6YB28DuBw8FpgHbwWdA4EJQv4+9//rksuuUQLFy7UlClT9Nxzz+n//u//9NOf/lQvv/yyfvazn+mPf/yj5s+fL0kqKyvTZZddplGjRukf//iHXnzxRY0ePVq//OUvtWfPngR/Nfb185//XIWFhXrhhRf097//XQ6HQz/96U8jyjzzzDM677zztGTJEl144YW6//779fHHH7ep/kWLFqm4uFhnnHGGPvjgAz3//PMR+z169IgoX1ZWposuukj9+vXTokWL9Nhjj6m6ulrf+973VFtbK0n69a9/rSVLluiee+7Rs88+q9LSUi1fvvzIfEMQF14HrIHXARwuXgusgdeCzoGhdxYwfPhwnX322aH9Rx99VFdddZXOPPNMSVKfPn1UXV2t22+/Xddcc43q6+v1P//zP7rssstkGIYk6corr9SLL76oLVu2KD8/PyFfh91t27ZNxx9/vHr16iW32627775bmzZtUiAQkMMR/Ezjoosu0nnnnSdJ+slPfqIFCxboq6++0uTJk2PWn5ubK7fbrbS0NBUUFEhSs/1wf/vb31RUVKR58+aFjj344IOaNGmSXn31VZ122mlavHixbr31Vp100kmSpLvvvluffPLJ4X4r0A68DlgDrwM4XLwWWAOvBZ0DQckC+vXrF9ret2+fSktLdf/99+uhhx4KHQ8EAqqvr9f27ds1aNAgnX/++Xr66ae1bt06bdu2TWvXrpUk+f3+Dm8/gq699lrdfffdWrhwoSZOnKgpU6borLPOCr0gSlL//v0jrsnJyVF9ff1Rac/q1au1fv16FRcXRxyvr6/Xxo0btXnzZjU0NGjUqFGhc6mpqRoxYsRRaQ9ax+uANfA6gMPFa4E18FrQORCULCAtLS203TgW9KabbtLxxx/frGyPHj20YcMGXXTRRTr22GN1/PHH67TTTlO3bt10wQUXdFib0dwPfvADffe739W7776rjz/+WA8//LB+//vf68UXXwx9oud0Optd19ozo30+X7vbEwgENGnSJN16663NzmVnZ2vnzp0t/vsuFy8ricDrgDXwOoDDxWuBNfBa0DkwR8li8vLylJubq5KSEvXr1y/05+uvv9aDDz4oSXr22WeVl5enP//5z7riiit00kknhcYht/YDhqNn7969uuOOO9TQ0KDzzz9f9957r1566SXt3r1bn376aZvqcLvdqqmpiTi2devWdrfpmGOO0caNG9WjR4/QfdSlSxfdfffdWrdunQYMGKDU1FStXLkydI3P5wt9EonE4XUgOfE6gCON14LkxGtB50FQshjDMHTFFVfoL3/5i5555hlt27ZNb7zxhm677TalpaUpJSVFRUVFKi0t1bvvvqsdO3bo9ddf12233SYpuGIJOl6XLl30zjvvaN68eVqzZo1KSkr07LPPyu12a+TIkW2qY+zYsTpw4ICeeOIJbd++Xc8++6zee++9drfpoosuUlVVla6//nqtXbtWa9eu1bXXXqtVq1ZpyJAhyszM1OzZs/Xwww/r9ddf18aNG3XrrbeqrKys3f8mjgxeB5ITrwM40ngtSE68FnQeyd0fhhb96Ec/Umpqqv7yl7/onnvuUX5+vv7rv/4r9OCwSy65RJs2bdKNN94or9er/v376+c//7kefvhhrVq1SlOnTk3wV2A/LpdLf/zjH/Xb3/5Wl156qerq6jR8+HA9/vjjbX6ewaRJk/Q///M/WrBggR5++GFNnTpVc+fO1dNPP92uNvXp00fPPPOM7rvvPl144YVyOp0aN26cnn76aeXm5kqSrrvuOqWmpuqOO+5QTU2NzjjjjGbLkSIxeB1IPrwO4GjgtSD58FrQeRgm/aoAAAAAEIGhdwAAAAAQhaAEAAAAAFEISgAAAAAQhaAEAAAAAFEISgAAAAAQhaAEAAAAAFEISgAAAAAQhaAEAAAAAFEISgAAAAAQhaAEAAAAAFEISgAAAAAQhaAEAAAAAFH+P2kpAo1Uu2riAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax = plt.subplots(1, figsize=(10, 10))\n", + "to_plot = []\n", + "labels = []\n", + "for i in range(3):\n", + " real = list(df[df['window']==window_sizes[i]]['vertices'])\n", + " shuffled = list(df[df['window']==window_sizes[i]]['vert_shuffled_mean'])\n", + " to_plot.append(real)\n", + " to_plot.append(shuffled)\n", + " labels.append(window_names[i]+ \"\\n real\")\n", + " labels.append(window_names[i]+ \"\\n shuffled\")\n", + "\n", + "sns.violinplot(data=to_plot)\n", + "ax.set_xticklabels(labels)\n", + "plt.show()" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Hourly number of users" + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [], + "source": [ + "views = g.rolling(window=3600, step=3600)\n", + "hour_vertices = [v.count_vertices() for v in views]" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [], + "source": [ + "timestamps = [dt.datetime.fromtimestamp(v.latest_time()) for v in views]\n", + "to_join = pd.DataFrame({\"time\": timestamps, \"window\":[3600 for k in range(len(timestamps))], \"vertices\": hour_vertices})\n", + "df = pd.concat([df,to_join],copy=False)" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [], + "source": [ + "df['week_days'] = df[\"time\"].apply(lambda x: x.weekday())\n", + "df['hour'] = df[\"time\"].apply(lambda x: x.hour)" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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OunChQvSV199JdWrV0/atm1bqfO0h4mNlZycHKlhw4bSZ599Zhm7ffu2FB0dLa1fv15x3LS0NKl///5STEyM1K5dO9WJzblz56SIiAgpNTXVMlZYWCg999xz0uzZs2XHu3XrljR8+HDpxIkTlrFjx45JERER0sGDB1XNVZIkaebMmVL37t2FJDaLFi2S2rdvr3pOklT0mj3zzDPStGnTbMZ79+4tLViwQMjvuHv3rvTMM8+4vLN0pEOHDtLUqVMtj+/cuSNFRERIW7ZsURTv6NGjUkREhPTzzz9bxrKysqRGjRpJa9eudSmGs8/1ggULpKefflrKy8uzjM2cOVNq27atqpgxMTFSfHy8yxtnZ/FGjBgh9ezZ0+bnv/32W+mJJ55wmtw5i7lmzRrp448/tvn5gQMHSv369VM8T7OrV69KzZo1kxISEkpNbJzFKywslGJiYqStW7c6jSEn5po1a6SYmBjp+vXrlrFffvlFevbZZ6U7d+4oilncypUrpaioKKd/vDmLZ04Krefzr3/9S4qLi1O83gMGDJA6d+5s8/Pz5s1z+P6Utu0eN26c9Nprr9k8Z/jw4VLv3r0dzs+V/YHc705pMbt06SKNGTPG5jlJSUlSq1atFMX797//LS1fvtzmOa+88oo0YcIEp/N0hKeirBw/fhx3795F8+bNLWMhISGoX78+9u7dqzju77//Dn9/f6xbtw5PPvmk6nmGhoZi0aJFaNCggWXMYDDAYDAgMzNTdrwHHngAM2fOREREBAAgPT0dy5YtQ1hYGGrXrq1qrnv37sVXX32FadOmqYpjduLECYSHhwuJdfbsWfz5559o3769zfjixYvRv39/Ib9jwYIFuH//Pt59911VcSpXroz//d//xaVLl1BQUICvvvoKAQEBqFu3rqJ4586dAwA0atTIMlahQgU8+uij+O2331yK4exznZqaiiZNmsBkMlnGmjVrhnPnzuHGjRuKYv7www+YOnWqrNfSWbzevXuXiGU0GpGXl4esrCxFMV999VUMGzYMQNFp7V9//RV79+7FU089pXieACBJEkaNGoVXXnkFTZo0cRqrtHgXLlzAvXv38Pjjj5cax9WYO3fuRLNmzVClShXLWIsWLfDDDz8gODhYUUxr6enpmD17NgYMGOB03s7iVapUCQDwxRdfoKCgAJcuXcLPP/9c6jbZWczz58/j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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "ax = sns.boxplot(data=df[df['window']==3600], x='hour', y='vertices')" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Temporal motifs" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "from raphtory.algorithms import all_local_motifs\n", + "counts = all_local_motifs(g,3600)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "df = pd.DataFrame(counts).transpose()" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
0123456789...30313233343536373839
208333000200050...0000000000
342390000000000...0000000000
1826490510001580...01244400371
881630000000000...0000000000
423250000000000...0000000000
..................................................................
697030000000000...0000000000
196030000000000...0000000000
179770000000000...0000000000
385780000000000...0000000000
388380000000000...0000000000
\n", + "

24818 rows × 40 columns

\n", + "
" + ], + "text/plain": [ + " 0 1 2 3 4 5 6 7 8 9 ... 30 31 32 33 34 35 \n", + "20833 3 0 0 0 2 0 0 0 5 0 ... 0 0 0 0 0 0 \\\n", + "34239 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 \n", + "1826 49 0 5 1 0 0 0 1 58 0 ... 0 12 4 4 4 0 \n", + "88163 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 \n", + "42325 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 \n", + "... .. .. .. .. .. .. .. .. .. .. ... .. .. .. .. .. .. \n", + "69703 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 \n", + "19603 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 \n", + "17977 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 \n", + "38578 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 \n", + "38838 0 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 \n", + "\n", + " 36 37 38 39 \n", + "20833 0 0 0 0 \n", + "34239 0 0 0 0 \n", + "1826 0 3 7 1 \n", + "88163 0 0 0 0 \n", + "42325 0 0 0 0 \n", + "... .. .. .. .. \n", + "69703 0 0 0 0 \n", + "19603 0 0 0 0 \n", + "17977 0 0 0 0 \n", + "38578 0 0 0 0 \n", + "38838 0 0 0 0 \n", + "\n", + "[24818 rows x 40 columns]" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# display(df)\n", + "display(df)" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[[17595 3826 2553 501 6088 22084]\n", + " [ 2012 1621 898 197 3543 996]\n", + " [ 2045 2676 2044 5964 2635 2027]\n", + " [ 4395 1615 8820 1687 1109 651]\n", + " [ 836 4088 1477 3219 2323 4780]\n", + " [16637 1930 9213 5081 3317 21493]]\n" + ] + } + ], + "source": [ + "motifs = np.array(df.sum(axis=0))\n", + "motifs[32:] = motifs[32:]/3\n", + "motifs_3d = to_3d_heatmap(motifs)\n", + "print(motifs_3d)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax = plt.subplots()\n", + "\n", + "labels = get_labels(motifs_3d)\n", + "ax = sns.heatmap(motifs_3d,square=True, cmap=\"YlGnBu\", cbar=True, annot=labels,annot_kws={\"size\":13}, fmt='', cbar_kws={\"shrink\": 1.0})\n", + "\n", + "for i in range(6):\n", + " offset_image(\"x\",i,i,ax)\n", + " offset_image(\"y\",i,i,ax)\n", + "\n", + "ax.tick_params(axis='x', which='major', pad=50)\n", + "ax.tick_params(axis='y', which='major', pad=50)\n", + "plt.setp(ax.get_xticklabels(), visible=False)\n", + "plt.setp(ax.get_yticklabels(), visible=False)\n", + "plt.tight_layout()\n", + "plt.savefig(\"sx-motifs-all.png\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "base", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.10" + }, + "orig_nbformat": 4 + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/py/math_overflow/motif-pics/motifs.pptx b/examples/py/math_overflow/motif-pics/motifs.pptx new file mode 100644 index 0000000000..6007bb0709 Binary files /dev/null and b/examples/py/math_overflow/motif-pics/motifs.pptx differ diff --git a/examples/py/math_overflow/motif-pics/x0.png b/examples/py/math_overflow/motif-pics/x0.png new file mode 100644 index 0000000000..16e313620c Binary files /dev/null and b/examples/py/math_overflow/motif-pics/x0.png differ diff --git a/examples/py/math_overflow/motif-pics/x1.png b/examples/py/math_overflow/motif-pics/x1.png new file mode 100644 index 0000000000..899b1a58a2 Binary files /dev/null and b/examples/py/math_overflow/motif-pics/x1.png differ diff --git a/examples/py/math_overflow/motif-pics/x2.png b/examples/py/math_overflow/motif-pics/x2.png new file mode 100644 index 0000000000..52135d3fe4 Binary files /dev/null and b/examples/py/math_overflow/motif-pics/x2.png differ diff --git a/examples/py/math_overflow/motif-pics/x3.png b/examples/py/math_overflow/motif-pics/x3.png new file mode 100644 index 0000000000..8629200e6c Binary files /dev/null and b/examples/py/math_overflow/motif-pics/x3.png differ diff --git a/examples/py/math_overflow/motif-pics/x4.png b/examples/py/math_overflow/motif-pics/x4.png new file mode 100644 index 0000000000..b10c2081ca Binary files /dev/null and b/examples/py/math_overflow/motif-pics/x4.png differ diff --git a/examples/py/math_overflow/motif-pics/x5.png b/examples/py/math_overflow/motif-pics/x5.png new file mode 100644 index 0000000000..4969a69ccd Binary files /dev/null and b/examples/py/math_overflow/motif-pics/x5.png differ diff --git a/examples/py/math_overflow/motif-pics/y0.png b/examples/py/math_overflow/motif-pics/y0.png new file mode 100644 index 0000000000..c62c237c5d Binary files /dev/null and b/examples/py/math_overflow/motif-pics/y0.png differ diff --git a/examples/py/math_overflow/motif-pics/y1.png b/examples/py/math_overflow/motif-pics/y1.png new file mode 100644 index 0000000000..32c039c523 Binary files /dev/null and b/examples/py/math_overflow/motif-pics/y1.png differ diff --git a/examples/py/math_overflow/motif-pics/y2.png b/examples/py/math_overflow/motif-pics/y2.png new file mode 100644 index 0000000000..e3b283e26f Binary files /dev/null and b/examples/py/math_overflow/motif-pics/y2.png differ diff --git a/examples/py/math_overflow/motif-pics/y3.png b/examples/py/math_overflow/motif-pics/y3.png new file mode 100644 index 0000000000..8e15974413 Binary files /dev/null and b/examples/py/math_overflow/motif-pics/y3.png differ diff --git a/examples/py/math_overflow/motif-pics/y4.png b/examples/py/math_overflow/motif-pics/y4.png new file mode 100644 index 0000000000..693a0f5b58 Binary files /dev/null and b/examples/py/math_overflow/motif-pics/y4.png differ diff --git a/examples/py/math_overflow/motif-pics/y5.png b/examples/py/math_overflow/motif-pics/y5.png new file mode 100644 index 0000000000..d4aba40ae6 Binary files /dev/null and b/examples/py/math_overflow/motif-pics/y5.png differ diff --git a/examples/py/math_overflow/new-existing-users.png b/examples/py/math_overflow/new-existing-users.png new file mode 100644 index 0000000000..e7ad71affd Binary files /dev/null and b/examples/py/math_overflow/new-existing-users.png differ diff --git a/examples/py/math_overflow/plotting_utils.py b/examples/py/math_overflow/plotting_utils.py new file mode 100644 index 0000000000..42bc922bd2 --- /dev/null +++ b/examples/py/math_overflow/plotting_utils.py @@ -0,0 +1,121 @@ +import matplotlib.pyplot as plt +import numpy as np +import distinctipy + +from matplotlib.offsetbox import OffsetImage,AnnotationBbox + +# Mapping different motifs to their place in the heatmap. + +mapper = {0:(5,5), + 1:(5,4), + 2:(4,5), + 3:(4,4), + 4:(4,3), + 5:(4,2), + 6:(5,3), + 7:(5,2), + 8:(0,0), + 9:(0,1), + 10:(1,0), + 11:(1,1), + 12:(2,1), + 13:(2,0), + 14:(3,1), + 15:(3,0), + 16:(0,5), + 17:(0,4), + 18:(1,5), + 19:(1,4), + 20:(2,3), + 21:(2,2), + 22:(3,3), + 23:(3,2), + 24:(5,0), + 25:(5,1), + 26:(4,0), + 27:(4,1), + 28:(4,1), + 29:(4,0), + 30:(5,1), + 31:(5,0), + 32:(0,2), + 33:(2,4), + 34:(1,2), + 35:(1,3), + 36:(0,3), + 37:(2,5), + 38:(3,4), + 39:(3,5)} + +def to_3d_heatmap(motif_flat, data_type=int): + motif_3d = np.zeros((6,6),dtype=data_type) + for i in list(range(24))+list(range(31,40)): + motif_3d[mapper[i]]=motif_flat[i] + for i in range(4): + motif_3d[mapper[24+i]] = (motif_flat[24+i] + motif_flat[31-i])/2 + + return motif_3d + +def human_format(num): + magnitude = 0 + while abs(num) >= 1000: + magnitude += 1 + num /= 1000.0 + # add more suffixes if you need them + return '%.1f%s' % (num, ['', 'K', 'M', 'B', 'T', 'P'][magnitude]) + +def get_labels(motif_map): + return np.vectorize(human_format)(motif_map) + +def get_motif(xory:str,y:int): + path = "motif-pics/"+xory+str(y)+".png" + return plt.imread(path) + +def offset_image(xory, coord, name, ax): + img = get_motif(xory, name) + im = OffsetImage(img,zoom=0.04) + im.image.axes = ax + + if(xory=="x"): + ab = AnnotationBbox(im, (coord+0.5, 5.5), xybox=(0., -40.), frameon=False, + xycoords='data', boxcoords="offset points", box_alignment=(0.5,0.5), pad=0) + + else: + ab = AnnotationBbox(im, (0, coord), xybox=(0., -40.), frameon=False, + xycoords='data', boxcoords="offset points", box_alignment=(1.0,0.0), pad=0) + + ax.add_artist(ab) + +# For making CDFs and CCDFs + +def cdf(listlike, normalised=True): + data = np.array(listlike) + N = len(listlike) + + x = np.sort(data) + if (normalised): + y = np.arange(N)/float(N-1) + else: + y = np.arange(N) + return x, y + +def ccdf(listlike, normalised=True): + x, y = cdf(listlike,normalised) + if normalised: + return x, 1.0-y + else: + return x, len(listlike)-y + +def lorenz(listlike): + tmp_arr = np.array(sorted(listlike)) + # print(tmp_arr[0]) + x= np.arange(listlike.size)/(listlike.size -1) + y = tmp_arr.cumsum() / tmp_arr.sum() + return x,y + +def get_ordinal_number(num): + if 10 < num % 100 < 20: + ordinal = str(num) + "th" + else: + ordinal = str(num) + {1: "st", 2: "nd", 3: "rd"}.get(num % 10, "th") + return ordinal diff --git a/examples/py/math_overflow/sx-motifs-all.png b/examples/py/math_overflow/sx-motifs-all.png new file mode 100644 index 0000000000..52ca79e954 Binary files /dev/null and b/examples/py/math_overflow/sx-motifs-all.png differ diff --git a/examples/py/reddit/demo.ipynb b/examples/py/reddit/demo.ipynb index 20f63c1b8b..8413604f12 100644 --- a/examples/py/reddit/demo.ipynb +++ b/examples/py/reddit/demo.ipynb @@ -9,7 +9,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 31, "metadata": {}, "outputs": [], "source": [ @@ -25,47 +25,119 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 32, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "Graph(number_of_edges=0, number_of_vertices=0, earliest_time=0, latest_time=0)" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + " DateTime Name Recipient Type Layer\n", + "0 2023-09-06 10:00:00 Alice David Email 1\n", + "1 2023-09-06 11:30:00 Bob Eve Message 2\n", + "2 2023-09-06 13:45:00 Charlie Frank Call 3\n", + "3 2023-09-06 13:50:00 Alice David Message 4\n" + ] } ], "source": [ - "g = Graph()\n", - "g" + "import pandas as pd\n", + "\n", + "# Sample data for demonstration\n", + "data = {\n", + " 'DateTime': ['2023-09-06 10:00:00', '2023-09-06 11:30:00', '2023-09-06 13:45:00', '2023-09-06 13:50:00'],\n", + " 'Name': ['Alice', 'Bob', 'Charlie', 'Alice'],\n", + " 'Recipient': ['David', 'Eve', 'Frank', 'David'],\n", + " 'Type': ['Email', 'Message', 'Call', 'Message'],\n", + " \"Layer\": [1, 2, 3, 4],\n", + "}\n", + "\n", + "# Create the DataFrame\n", + "df = pd.DataFrame(data)\n", + "\n", + "# Print the DataFrame\n", + "print(df)" ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 33, + "metadata": {}, + "outputs": [], + "source": [ + "df[\"DateTime\"] = pd.to_datetime(df[\"DateTime\"]).astype(\"datetime64[ms]\")" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "Loading edges: 100%|██████████| 4.00/4.00 [00:00<00:00, 18.7Kit/s]0:00, 3.19Kit/s]" + ] + } + ], + "source": [ + "g=Graph()\n", + "g.load_edges_from_pandas(edge_df=df, src_col=\"Name\", dst_col=\"Recipient\", time_col=\"DateTime\", props=[\"Type\"], layer_in_df=\"Layer\")\n", + "df.dropna(axis=0, inplace=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 35, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "Total vertices in the graph: 3\n", - "Total vertices at 2021-01-01 12:40:00: 2\n", - "1\n" + "Edge(source=Alice, target=David, earliest_time=1693994400000, latest_time=1694008200000, properties={Type: Message})\n" + ] + } + ], + "source": [ + "e= g.edge(\"Alice\",\"David\")\n", + "print(e)" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Vertex(name=Ben, earliest_time=\"1560419400000\", latest_time=\"1560419400000\", properties={type: person, _id: Ben})\n", + "Vertex(name=Hamza, earliest_time=\"1560419400000\", latest_time=\"1560419400000\", properties={type: person, _id: Hamza})\n", + "Edge(source=Ben, target=Hamza, earliest_time=1560419400000, latest_time=1560419400000, properties={type: friend})\n", + "Total vertices in the graph: 2\n", + "Total vertices at 2021-01-01 12:40:00: 2\n" ] } ], "source": [ - "from raphtory import Graph\n", "g = Graph()\n", - "g.add_edge(\"2021-01-01 12:32:00\", \"Ben\", \"Hamza\", {\"type\": \"friend\"})\n", - "g.add_edge(\"2021-01-02 14:15:36\", \"Hamza\", \"Haaroon\", {\"type\": \"friend\"})\n", - "print(\"Total vertices in the graph:\", g.num_vertices())\n", - "print(\"Total vertices at 2021-01-01 12:40:00:\", g.at(\"2021-01-01 12:40:00\").num_vertices())\n" + "# e = g.add_edge(\"2021-01-01 12:32:00\", \"Ben\", \"Hamza\", {\"type\": \"friend\"})\n", + "\n", + "# g.add_edge(\"2021-01-02 14:15:36\", \"Hamza\", \"Haaroon\", {\"type\": \"friend\"})\n", + "vertex = g.add_vertex(\"2019-06-13 09:50:00\", \"Ben\", {\"type\": \"person\"})\n", + "vertex2 = g.add_vertex(\"2019-06-13 09:50:00\", \"Hamza\", {\"type\": \"person\"})\n", + "edge = g.add_edge(\"2019-06-13 09:50:00\", \"Ben\", \"Hamza\", {\"type\": \"friend\"})\n", + "\n", + "# edge = g.add_edge(1, \"Ben\", \"Hamza\", {\"type\": \"friend\"})\n", + "# edge2 = g.add_edge(2, \"Ben\", \"Hamza\", {\"type\": \"friend\"})\n", + "print(vertex)\n", + "print(vertex2)\n", + "print(edge)\n", + "print(\"Total vertices in the graph:\", g.count_vertices())\n", + "print(\"Total vertices at 2021-01-01 12:40:00:\", g.at(\"2021-01-01 12:40:00\").count_vertices())\n" ] }, { @@ -77,29 +149,30 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 37, "metadata": {}, "outputs": [ { - "data": { - "text/plain": [ - "[(1, 1), (2, 2)]" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" + "name": "stdout", + "output_type": "stream", + "text": [ + "[1, 2]\n", + "123\n" + ] } ], "source": [ + "g = Graph()\n", + "\n", "\n", - "g.add_vertex(timestamp=1, id=\"ben\", properties={\"property 1\": 1, \"property 3\": \"hi\", \"property 4\": True})\n", + "v=g.add_vertex(timestamp=1, id=\"ben\", properties={\"property 1\": 1, \"property 3\": \"hi\", \"property 4\": True})\n", "g.add_vertex(timestamp=2, id=\"ben\", properties={\"property 1\": 2, \"property 2\": 0.6, \"property 4\": False})\n", "g.add_vertex(timestamp=3, id=\"ben\", properties={\"property 2\": 0.9, \"property 3\": \"hello\", \"property 4\": True})\n", "\n", - "g.add_vertex_properties(id=\"ben\", properties={\"static property\": 123})\n", + "v.add_constant_properties(properties={\"unchanging property\": 123})\n", "\n", - "g.vertex(\"ben\").property_history(\"property 1\")" + "print(v.properties.temporal.get(\"property 1\").values())\n", + "print(v.properties.constant.get(\"unchanging property\"))\n" ] }, { @@ -111,16 +184,16 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 38, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "Edge(source=ben, target=hamza, earliest_time=1, latest_time=3, properties={property 2 : 0.9, static property : 123, property 1 : 2, property 3 : hello, property 4 : true})" + "Edge(source=ben, target=hamza, earliest_time=1, latest_time=3, properties={property 1: 2, property 4: true, property 3: hello, property 2: 0.9, static property: 123})" ] }, - "execution_count": 5, + "execution_count": 38, "metadata": {}, "output_type": "execute_result" } @@ -128,11 +201,11 @@ "source": [ "\n", "g.add_vertex(timestamp=1,id=\"hamza\")\n", - "g.add_edge(timestamp=1, src=\"ben\", dst=\"hamza\", properties={\"property 1\": 1, \"property 3\": \"hi\", \"property 4\": True})\n", + "e=g.add_edge(timestamp=1, src=\"ben\", dst=\"hamza\", properties={\"property 1\": 1, \"property 3\": \"hi\", \"property 4\": True})\n", "g.add_edge(timestamp=2, src=\"ben\", dst=\"hamza\", properties={\"property 1\": 2, \"property 2\": 0.6, \"property 4\": False})\n", "g.add_edge(timestamp=3, src=\"ben\", dst=\"hamza\", properties={\"property 2\": 0.9, \"property 3\": \"hello\", \"property 4\": True})\n", "\n", - "g.add_edge_properties(src=\"ben\", dst=\"hamza\", properties={\"static property\": 123})\n", + "e.add_constant_properties(properties={\"static property\": 123})\n", "\n", "g.edge(\"ben\",\"hamza\")" ] @@ -144,42 +217,121 @@ "## Loading a real graph" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In the cell below we are pulling the subreddit to subreddit hyperlink graph from the [SNAP data repository](http://snap.stanford.edu/data/soc-RedditHyperlinks.html). This builds a web of the references between different communities on reddit with NLP analysis on each post (edge/link) providing properties such as sentiment." + ] + }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 39, "metadata": {}, "outputs": [], "source": [ - "reddit_graph = graph_loader.reddit_hyperlink_graph()" + "import requests\n", + "import pandas as pd\n", + "import os\n", + "\n", + "url = \"http://snap.stanford.edu/data/soc-redditHyperlinks-title.tsv\"\n", + "file_path = \"soc-redditHyperlinks-title.tsv\"\n", + "\n", + "if not os.path.exists(file_path):\n", + " response = requests.get(url, stream=True)\n", + " total_size = int(response.headers.get('content-length', 0))\n", + " block_size = 1024 \n", + " downloaded_size = 0\n", + "\n", + " with open(file_path, \"wb\") as f:\n", + " for data in response.iter_content(block_size):\n", + " f.write(data)\n", + " downloaded_size += len(data)\n", + " progress = (downloaded_size / total_size) * 100\n", + " print(f\"Downloaded {downloaded_size}/{total_size} bytes ({progress:.2f}%)\", end='\\r')\n", + "\n", + " print(\"\\nFile downloaded successfully.\")\n", + "\n", + "if not os.path.exists(\"reddit.pkl\"):\n", + "\n", + " #Next we label all the nlp features from the 'properties' column and make them there own columns which we can reference in the Raphtory Pandas loader \n", + " df = pd.read_csv(file_path, sep='\\t')\n", + "\n", + " #Define all the features as per the spec on the SNAP website\n", + " features = [\n", + " \"number_of_characters\", \"number_of_characters_without_counting_whitespace\", \"fraction_of_alphabetical_characters\", \n", + " \"fraction_of_digits\", \"fraction_of_uppercase_characters\", \"fraction_of_white_spaces\", \"fraction_of_special_characters\", \n", + " \"number_of_words\", \"number_of_unique_words\", \"number_of_long_words_at_least_6_characters\", \"average_word_length\", \n", + " \"number_of_unique_stopwords\", \"fraction_of_stopwords\", \"number_of_sentences\", \"number_of_long_sentences_at_least_10_words\", \n", + " \"average_number_of_characters_per_sentence\", \"average_number_of_words_per_sentence\", \"automated_readability_index\", \n", + " \"positive_sentiment\", \"negative_sentiment\", \"compound_sentiment\", \n", + " \"liwc_funct\", \"liwc_pronoun\", \"liwc_ppron\", \"liwc_i\", \"liwc_we\", \"liwc_you\", \"liwc_she_he\", \"liwc_they\", \n", + " \"liwc_ipron\", \"liwc_article\", \"liwc_verbs\", \"liwc_aux_vb\", \"liwc_past\", \"liwc_present\", \"liwc_future\", \n", + " \"liwc_adverbs\", \"liwc_prep\", \"liwc_conj\", \"liwc_negate\", \"liwc_quant\", \"liwc_numbers\", \"liwc_swear\", \n", + " \"liwc_social\", \"liwc_family\", \"liwc_friends\", \"liwc_humans\", \"liwc_affect\", \"liwc_posemo\", \"liwc_negemo\", \n", + " \"liwc_anx\", \"liwc_anger\", \"liwc_sad\", \"liwc_cog_mech\", \"liwc_insight\", \"liwc_cause\", \"liwc_discrep\", \n", + " \"liwc_tentat\", \"liwc_certain\", \"liwc_inhib\", \"liwc_incl\", \"liwc_excl\", \"liwc_percept\", \"liwc_see\", \n", + " \"liwc_hear\", \"liwc_feel\", \"liwc_bio\", \"liwc_body\", \"liwc_health\", \"liwc_sexual\", \"liwc_ingest\", \n", + " \"liwc_relativ\", \"liwc_motion\", \"liwc_space\", \"liwc_time\", \"liwc_work\", \"liwc_achiev\", \"liwc_leisure\", \n", + " \"liwc_home\", \"liwc_money\", \"liwc_relig\", \"liwc_death\", \"liwc_assent\", \"liwc_dissent\", \"liwc_nonflu\", \"liwc_filler\"\n", + " ]\n", + "\n", + " # Convert the property string to arrays of integers\n", + " df['PROPERTIES'] = df['PROPERTIES'].str.split(',').apply(lambda x: [float(i) for i in x])\n", + " # Convert the 'properties' column into a DataFrame with individual columns\n", + " properties_df = df['PROPERTIES'].apply(pd.Series)\n", + " # Rename the columns using the features array\n", + " properties_df.columns = features\n", + " # Concatenate the original DataFrame and the properties DataFrame\n", + " df = pd.concat([df, properties_df], axis=1)\n", + " # Drop the original 'properties' column\n", + " df = df.drop(columns=['PROPERTIES'])\n", + " #Convert the datestrings to datetimes\n", + " df['TIMESTAMP'] = pd.to_datetime(df['TIMESTAMP'])\n", + " # Convert datetime to millisecond precision (datetime64[ms])\n", + " df['TIMESTAMP'] = df['TIMESTAMP'].astype('datetime64[ms]')\n", + " #Save the data so we don't have to parse it again\n", + " df.to_pickle('reddit.pkl')\n", + " df.head()\n", + "\n", + "else:\n", + " df = pd.read_pickle(\"reddit.pkl\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now let's load this data into a Raphtory graph - we won't use all the features, just grab a couple for demo purposes, but obviously you can edit the code below to choose the ones you are interested in." ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 40, "metadata": {}, "outputs": [ { - "name": "stdout", + "name": "stderr", "output_type": "stream", "text": [ - "Graph(number_of_edges=234792, number_of_vertices=54075, earliest_time=1388506820000, latest_time=1493570870000)\n" + "Loading edges: 100%|██████████| 572K/572K [00:25<00:00, 22.7Kit/s]s]08, 726it/s]es: 24%|██▄ | 137K/572K [00:05<00:18, 24.0Kit/s]" ] } ], "source": [ - "print(reddit_graph)" + "reddit_graph = Graph.load_from_pandas(edges_df=df,src=\"SOURCE_SUBREDDIT\",dst=\"TARGET_SUBREDDIT\",time=\"TIMESTAMP\",props=[\"number_of_unique_words\", \"average_word_length\", \"number_of_unique_stopwords\", \"number_of_sentences\", \"automated_readability_index\", \"positive_sentiment\", \"negative_sentiment\", \"compound_sentiment\"])\n" ] }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 41, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "property names: ['post_label', 'post_id', 'word_count', 'long_words', 'sentences', 'readability', 'positive_sentiment', 'negative_sentiment', 'compound_sentiment']\n", + "property names: ['number_of_unique_words', 'average_word_length', 'number_of_unique_stopwords', 'number_of_sentences', 'automated_readability_index', 'positive_sentiment', 'negative_sentiment', 'compound_sentiment']\n", "sentiment history: [(1404952792000, 0.2732), (1414676601000, 0.2344), (1418639874000, 0.0), (1420030124000, -0.2481), (1426362695000, 0.0), (1427198700000, -0.2023), (1427894452000, 0.0), (1429187302000, 0.5562), (1429644527000, 0.296), (1431911341000, -0.3595), (1433653011000, -0.34), (1435258425000, 0.0772), (1439068701000, -0.5574), (1439249314000, 0.0), (1440456620000, 0.4019), (1446586617000, 0.0), (1447125847000, -0.7042), (1447422296000, 0.0387), (1450563607000, 0.8192), (1450658697000, 0.6841), (1450863723000, 0.2732), (1451249771000, -0.4767), (1455119795000, 0.0), (1455295188000, -0.4939), (1456505801000, -0.6908), (1456949833000, 0.0), (1458061181000, 0.3384), (1460498612000, -0.5267), (1461879765000, -0.6136), (1463741129000, 0.0), (1466862772000, -0.1779), (1471350739000, -0.6808), (1479538764000, 0.0), (1480262620000, 0.25), (1482314435000, 0.6908), (1483690844000, 0.0), (1487778524000, -0.4404), (1488873853000, 0.0), (1488966224000, 0.0), (1489359306000, -0.5789), (1489999301000, -0.2263), (1491144265000, -0.34), (1492064865000, 0.8149), (1492451552000, 0.0)]\n", "Most recent sentiment on 2014-10-30 13:45:00 - 0.2344\n" ] @@ -187,24 +339,24 @@ { "data": { "text/plain": [ - "PathFromVertex(Vertex(name=cancer, properties={_id : cancer}), Vertex(name=soccer, properties={_id : soccer}), Vertex(name=pics, properties={_id : pics}), Vertex(name=funny, properties={_id : funny}), Vertex(name=bitcoin, properties={_id : bitcoin}), Vertex(name=propaganda, properties={_id : propaganda}), Vertex(name=conspiracy, properties={_id : conspiracy}), Vertex(name=askreddit, properties={_id : askreddit}), Vertex(name=trees, properties={_id : trees}), Vertex(name=cricket, properties={_id : cricket}), ...)" + "PyPropHistValueList([[0.2732], [0.6249, 0.7957, 0], [0], [0.1779], [0.2003], [0.34], [0.4215], [-0.3182], [-0.4767], [0.296], ...])" ] }, - "execution_count": 8, + "execution_count": 41, "metadata": {}, "output_type": "execute_result" } ], "source": [ "edge = reddit_graph.edge(\"conspiracy\",\"documentaries\")\n", - "print(\"property names:\",edge.property_names())\n", - "print(\"sentiment history:\",edge.property_history(\"compound_sentiment\"))\n", + "print(\"property names:\",edge.properties.keys())\n", + "print(\"sentiment history:\",edge.properties.temporal.get(\"compound_sentiment\").items())\n", "\n", "date=\"2014-10-30 13:45:00\"\n", "edge_perspective = reddit_graph.at(date).edge(\"conspiracy\",\"documentaries\")\n", "print(\"Most recent sentiment on\",date,\"-\",edge_perspective[\"compound_sentiment\"])\n", "\n", - "reddit_graph.vertex(\"conspiracy\").out_neighbours().out_neighbours().out_neighbours()" + "reddit_graph.vertex(\"conspiracy\").out_edges.dst.out_edges.properties.temporal.get(\"compound_sentiment\").values()" ] }, { @@ -216,22 +368,29 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": 42, "metadata": {}, "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "1217it [01:06, 18.30it/s]\n" + ] + }, { "data": { "text/plain": [ "" ] }, - "execution_count": 13, + "execution_count": 42, "metadata": {}, "output_type": "execute_result" }, { "data": { - "image/png": "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DaeffjpuuOEGLF++PLz/7bffjk8//RQPP/wwnn32WWzatAlz58618B3pY00QERGR9SztE7R161Z89tlneOGFF3DQQQcBAP74xz/iv//9L/71r39h7969GD16NK6++moAwIgRI7B27VosWrQI06dPx+7du/HGG29g4cKFmDx5MgDg/vvvx/HHH481a9bggAMOsOy9aUl1nyAiIiIyztIkqLi4GE8++STGjx8ffkySJEiShMbGRqxevRozZ86M2GfatGmYN28ehBD48ssvw4+FDBs2DOXl5Vi1alVCSZDHk7pKMrdLSunxk8XtdkX81ymcGDdjTg8nxgw4L26nxRvixLgZc2IsTYIKCwtx5JFHRjz2zjvvYOvWrbjpppvw+uuvo6KiIuL5srIytLW1oa6uDrt370ZxcTGysrK6bVNVVWU6LpdLQnFxnun9o5JaAAAFBdkozLb+wzeqsDDH6hBMcWLcjDk9nBgz4Ly4nRZviBPjZszm2GqI/FdffYU//OEPOO644zBjxgx4vV5kZmZGbBP6t9/vR1tbW7fnASArKws+n890HLIs0NjYanr/aELNYc3NXgTb7N9N2u12obAwB42NbQgGnbPOhxPjZszp4cSYAefF7bR4Q5wYN2PuVFiYE3ftkm2SoGXLluG6667DgQceiPnz5wNQkhm/3x+xXejfOTk5yM7O7vY8oIwYy8lJLMMMBJJ/MQmhfDiyLBAQzrhYASAYlFNyPlLNiXEz5vRwYsyA8+J2WrwhToybMZtjizaZ5557DldeeSWOOuooLFy4MNy81b9/f1RXV0dsW11djdzcXBQUFKCiogL19fXdEqHq6mqUl5enLf54sV80ERGR9SxPgl544QXceeedOOecc3D//fdHNG9NnjwZX3zxRcT2K1aswIEHHgiXy4WDDjoIsiyHO0gDwObNm7F7925MmTIlbe/BKPs3gBEREfUeliZBmzdvxt13341jjz0Wc+bMQU1NDfbs2YM9e/agqakJ5557Lr799lvMnz8fP/30ExYvXoz/+7//w8UXXwwAKC8vx89//nPccsstWLlyJb799ltcc801mDp1KiZNmmTlW4uJNUFERETWs7RP0DvvvIP29na89957eO+99yKemzVrFu6991489thj+Mtf/oJnn30WgwYNwl/+8hdMnz49vN2dd96Ju+++G1dccQUA4IgjjsAtt9yS1vdhFOcJIiIisg9JCMFWmi6CQRm1tS1JP+7/bXZBADh2GOCB/TuweTwuFBfnoa6uxfLOa/FwYtyMOT2cGDPgvLidFm+IE+NmzJ1KSvLiHh1meZ+g3oQVQURERPbBJIiIiIh6JSZBaRLR6MiqICIiIssxCbIAcyAiIiLrMQlKE1YEERER2QuToDThEDwiIiJ7YRKULqosSGJVEBERkeWYBBEREVGvxCQoTdgniIiIyF6YBKUJ+wQRERHZC5MgC7BPEBERkfWYBKULq4KIiIhshUlQmrBPEBERkb0wCUoTVgQRERHZC5MgC7BPEBERkfWYBKVLR1UQ8x8iIiJ7YBKUJuHmMGZBREREtsAkKE1CSRBzICIiIntgEkRERES9EpOgNGOnaCIiInvwWB1Ab5HtBsrzgJJ8D4CA1eEQERH1ekyC0kSSgKkDgOLiLNTVMQkiIiKyGpvDiIiIqFdiEkRERES9EpMgIiIi6pWYBBEREVGvxCSIiIiIeiUmQURERNQrMQkiIiKiXolJEBEREfVKTIKIiIioV2ISRERERL0SkyAiIiLqlZgEERERUa/EJIiIiIh6JSZBRERE1CtJQghhdRB2I4SALKfmtLjdLgSDckqOnQpOizfEiXEz5vRwYsyA8+J2WrwhToybMStcLgmSJMW1D5MgIiIi6pXYHEZERES9EpMgIiIi6pWYBBEREVGvxCSIiIiIeiUmQURERNQrMQkiIiKiXolJEBEREfVKTIKIiIioV2ISRERERL0SkyAiIiLqlZgEERERUa/EJIiIiIh6JSZBRERE1CsxCaKUkWXZ6hDIpnhtUCyBQMDqEMimkn1tMAmipHv11Vfh9/vhcjnz8mIBnTpOvzYAXh+ptGTJEvj9fng8HqtDMY3XR2qk6tpw7p2oF1i3bh28Xq/VYcTl7rvvxl133YU9e/ZYHYphmzdvxrfffov169dDlmXHFNBOuz6ceG0Azrw+vvzyS1RWVlodRlzuvfde3Hvvvdi1a5fVocTlu+++w/Lly/Hf//7XMQm+066PVF4bzk23ezBZlrF582acfvrpuOOOO3DCCScgOzvb6rB03X333Xj99dfx4osvYuDAgeECQwgBAJAkyeIIu7v//vvx6aefYvfu3SgqKkJWVhYWLFiA4cOHWx1aVE68Ppx4bQDOuz5kWcauXbtwxRVX4Oyzz8YZZ5yB8vJyq8PSdffdd+ONN97AG2+8gaFDh0Ykm0II214f8+fPx8cffwyfzwcA8Pl8uPPOOzF16lRbfiedeH2k/NoQZEter1eMGzdOTJ06VbzyyivC5/NZHVJM999/vxg/frzYsmWL1aEY9vLLL4tp06aJr7/+WmzdulV88cUX4vTTTxeHH364WLFihQgGg1aHGJWTrg8nXhtCOPP6kGVZCCHEoYceKg444ADx17/+VezZs8fiqGJ76KGHxNixY8X27dutDiUub775ppg+fbr47rvvxJ49e8TOnTvFnDlzxOTJk8XSpUtFU1OT1SF247TrIx3XBmuCbCoYDKJ///4oLS3FrbfeimAwiFmzZiEzM9Pq0LrZtm0bli1bhtNOOw0DBw4EoPziWLRoESorK9HU1ITzzjsP++23n63i37BhA4444ghMnDgRADBkyBDccsstOOecc3Dbbbfhz3/+MyZOnGjLJhCnXB9OvTYAZ14foV/Fffr0wfDhw/Hoo48iEAjg3HPPRd++fS2Orrs9e/bgm2++wYwZM1BQUABAubb/+te/YseOHdizZw/OPvtsTJ06FSUlJRZHG2nr1q0YN24cxo0bF37srrvuwimnnIKHH34Y2dnZOOGEEyyMsDsnXR/pujbs8c2lbj7//HMEAgEsWrQIl1xyCe6880688cYb8Pv9VofWzZAhQ3DyySfjk08+wf/+9z8AwNlnn42PPvoIO3fuxI4dO3D++edj2bJlABBuArHazp07sWnTpvC/hRAYPnw4Jk6ciNzcXMydOxdNTU0RzTZ24ZTrw6nXBuDc62PFihVobW3F3/72N9x+++144okn8Pe//x01NTVWh9ZNv379cOKJJ6Kqqgoff/wx/H4/zjvvPHz55ZfIzMxEdnY2br755nCHejud56qqKmzevDn8b7/fj+LiYuy///6oqKjAHXfcgS1btgCw13XtlOsjbddGyuqYyJRQFfsHH3wgfvOb34SrVP/85z+LsWPHipdeesm2TR+XX365OOaYY8QTTzwhfv/734v6+nrh9/uFEELccMMN4rDDDrNNFbEsy+LZZ58VM2fOFP/617/Cj2/btk3MmjVLfPPNN+KUU04RN910k4VRdufU68NJ14YQzrw+Qk0dq1evFnPnzhW1tbVCCCGeeeYZMXr0aHH//ffbtunjtttuE9OnTxcPPPCAuP7660VjY2P4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8u3vYYLBIJqamnDOOefggQceQGNjIx5//HG0trYiGAxin332wT333AOXy2WboeSMObVCnS0B4Nxzz4Xf78dLL72EuXPnoqWlBccffzwyMjKwZMkStLW14c0330RWVlZER2/G23PjBpRE7Wc/+xlOO+00XHrppZg7dy68Xm84UVu8eDHa2trw6quvoqCggDH3org3bNgAt9uN4cOHhzsTL126FI899hiWLFmCwYMHhx+3A6fEyyHySRYa+ud2u9GnTx/su++++PrrrzF58mRcdtllqKysxPr163HggQfC7Xbb4mbAmNPD5XLB7/cDUFYt3717N0499VQUFhbinnvuwemnn45TTjkFCxcuRHt7O5599lkA0Yd1M15tTo07tKr3EUccgZUrV+K9996DEAI33HADTjrpJJx88sl44oknEAwG8fjjjzPmBDgx7n333RfDhw8HgHDicPTRR6OlpQUvv/xyxON24JR4mQQlwf/93//htddeA4DwgnQhWVlZ+OabbwAA//znPxEMBrHffvvhzTffxMcff2xJvABjTid13JmZmZBlGWVlZbjuuuuwYcMG7Nq1K2LBwr59+6K8vByNjY2MNw5OjFsdc2iI9llnnYWtW7fir3/9K3bs2BFeVy0YDKJv374YNmwYmpubGXOcnBi3OmYgcg4jWZZRXFyM8847D5999hnWrVtnRYgRnBYvwCQoIUIIBAIBvPrqq/jb3/6G9957D4DyiyEQCAAAZsyYAY/Hg5tuugmff/45Xn75ZVx11VVob28PV8ens0WSMadPtLhDTXMHH3wwzjvvPFx//fURK5F7PB4UFhaGC+x0xe20eJ0cd7SYAWDUqFG4//77sWPHDvzwww/h59xuN9xuN3JycsL93Bhzz4w71j0vFEeoyfeQQw7B3r178eGHH6Y9TqfGq8aO0QkIzXKZn5+PL7/8Es899xza29txwgknhDt99evXDy+99BKGDBmChQsXYsCAARgwYACuueYaDB06FDk5OYy5B8asF3co5quvvhputxsbNmzAjz/+iNGjR+PNN9/E119/jZtuuglA+qrhnRavk+PWi/mAAw7As88+iyuuuAKLFi3Cjz/+iHHjxmHVqlX47LPPcNVVVzHmHhx3rJi7Tgg7adIkXHjhhViwYAHGjx+Pww47LG1xOjVeNXaMToLZs2cjKysLfr8fwWAQs2fPDn/B6uvrsXTpUhx11FEYPnx4RKdNKzHm9IkVd3t7OwKBAObOnYvly5dj4MCByMrKwp///Gfst99+jLeHxx0rZgBYv349nn/+eSxfvhzZ2dkoKSnBjTfeGB5tw5iNc2LcsWIOFd2hGa6vuOIKfPvtt3j//fctW5/PafGGAiOTgsGg2LVrlzjllFPEN998IzZu3Chmz54tzj77bPHWW2+Ft/P5fBZGGYkxp4/RuIVQJmX7/vvvxffff2/ZqvBOizfEiXHHE3MgEBBer1c0NzeLtrY2iyJ2ZsxCODNuozGr5y/atm2bZbNuOy1eNXv8VHYol8uFzMxMzJw5E0VFRRgxYgRuvPFGuFwuPP/88/jPf/4DoLOTph0w5vQxGjcAlJeXY+zYsRg7dixKSkoYbxycGHc8MQshkJWVhby8PEsW6XRyzIAz4zYac2h9MwAYPHgwKioqGG+c2BwWhzfffBObN28GoEz6dOyxxwIAmpubkZ+fH57z4Pvvv8e9994LWZZx7rnn4vjjj2fMPTxmwHlxOy3eECfGzZjTx4lxOy1mp8UbC5MggxYsWIBXXnkFU6dOxZYtW+D1ejF8+HA8+uijcLvdET3cJUnC999/j/nz56Ompga/+93vMHPmTMbcQ2N2YtxOi9fJcTNmxt2TYnZavLrS3f7mRD/++KOYOXOmWL58uRBCiNbWVvHWW2+JI488Upx55pmioaFBCCHCq5aH1kD5+uuvxZw5c8SOHTsYcw+N2YlxOy1eJ8fNmBl3T4rZafEawSTIgC+++EIceuihoqamJvxYe3u7WLNmjTj22GPFr371q/DjoY5foQ/fqs66jDl9nBa30+INcWLcjDl9nBi302J2WrxGsGO0AYMHD0ZmZiY++OCD8GMejweTJk3CPffcg8rKSlxzzTUAOieECg33y8jISH/AYMzp5LS4nRZviBPjZszp48S4nRaz0+I1gpMlRvHee++hsrISbW1tmDBhAkaPHo1PPvkEY8eOxbhx48LbjR8/HldeeSWeffZZfP/99xHPAemdYIsxp4/T4nZavE6OmzGnjxPjdlrMTos3XqwJ0jB//nzccccd+OSTT7BkyRI8+eST6NevH1avXo3Fixdjy5Yt4W0zMzNx+OGHo7KyEhs3bmTMPTxmwHlxOy3eECfGzZjTx4lxOy1mp8VrBmuCunjrrbfw9ttvY9GiRRgzZgxaW1tx3nnnwefz4d5778Xll18OWZZxwQUXYMKECQAQXsU8Pz+fMffgmJ0Yt9PidXLcjJlx96SYnRavWUyCuti0aRP23XdfjB49Gu3t7cjNzcUll1yCa665BjfffDOeeuop3HjjjWhoaMChhx6K8ePH4/3338fWrVstm16dMTPunhKvk+NmzIy7J8XstHjNYhLUQXQs8LZnzx7s3bsXkiSFO3IVFRUhEAigsrIS06dPx6OPPoqXX34Zzz33HDIyMpCTk4PFixdj4MCBjLkHxuzEuJ0Wr5PjZsyMuyfF7LR4E8UkqEOo09axxx6Lr7/+Gtu3b8fgwYMBKB+82+2G3++HEAL7778/9t9/fzQ1NYVnxiwoKGDMPTRmJ8bttHidHDdjZtw9KWanxZsoJkFdHH744dh3331RWloafqy5uTmc5YY8++yzyMzMxFlnnWVFmBEYc/o4LW6nxRvixLgZc/o4MW6nxey0eM3i6DANFRUVEXMa7N69G4FAAAUFBZAkCQ899BD+/Oc/Y/LkyRZGGYkxp4/T4nZavCFOjJsxp48T43ZazE6L1wwmQQa0t7fD7XYjPz8fjz76KBYvXoyXX34Z++67r9WhRcWY08dpcTst3hAnxs2Y08eJcTstZqfFawSbw2IIdRDLyspCYWEhbrnlFixbtgwvvvgi9t9/f6vD08SY08dpcTst3hAnxs2Y08eJcTstZqfFG5eUL8zRA6xdu1aMHj1aTJgwQaxdu9bqcAxhzOnjtLidFm+IE+NmzOnjxLidFrPT4jWCSZABbW1t4k9/+pPYuHGj1aEYxpjTx2lxOy3eECfGzZjTx4lxOy1mp8VrhCSEEFbXRjlBe3u7bReAi4Yxp4/T4nZavCFOjJsxp48T43ZazE6LVw+TICIiIuqVODqMiIiIeiUmQURERNQrMQkiIiKiXolJEBEREfVKTIKIiIioV2ISRERERL0Sl80gIse48cYb8frrr8fcZuDAgdi5cyfef/99DBo0KE2REZETcZ4gInKMbdu2oba2Nvzvxx57DGvXrsUjjzwSfszv9yMzMxNjx45FZmamFWESkUOwJoiIHGPIkCEYMmRI+N8lJSXIzMzEpEmTrAuKiByLfYKIqEd57bXXMHr0aOzYsQOA0oR20UUX4aWXXsLMmTMxYcIE/OpXv8LmzZvx4Ycf4qSTTsLEiRNx+umn44cffog41urVqzF79mxMnDgRU6dOxe9///uImigicjbWBBFRj7dmzRpUV1fjxhtvhM/nw+23345LLrkEkiRh7ty5yMnJwW233YbrrrsOb731FgBg1apVuOCCCzBt2jQ8+OCDaGhowEMPPYTzzjsPr776KrKzsy1+V0SUKCZBRNTjtbS04MEHH8SIESMAAF988QVefPFFLFmyBNOnTwcAbN26FX/+85/R2NiIwsJCLFiwAMOGDcMTTzwBt9sNAJg4cSJ+/vOfY+nSpTjnnHMsez9ElBxsDiOiHq+oqCicAAFA3759AShJTUifPn0AAI2NjWhra8M333yDI488EkIIBAIBBAIBDB48GCNGjMBnn32W1viJKDVYE0REPV5+fr7m47m5uZqPNzY2QpZlPPXUU3jqqae6PZ+VlZXU+IjIGkyCiIi6yMvLgyRJOP/88/Hzn/+82/M5OTkWREVEycYkiIioi/z8fIwdOxabNm3C+PHjw497vV7MnTsXRx55JEaOHGlhhESUDOwTRESk4ZprrsGnn36Ka6+9Fh9//DE++OADXHzxxVi+fDnGjRtndXhElARMgoiINBx22GF4+umnUVVVhblz5+KGG26A2+3GM888w8kZiXoILptBREREvRJrgoiIiKhXYhJEREREvRKTICIiIuqVmAQRERFRr8QkiIiIiHolJkFERETUKzEJIiIiol6JSRARERH1SkyCiIiIqFdiEkRERES9EpMgIiIi6pX+P0X+ZQOusDUMAAAAAElFTkSuQmCC", + "image/png": "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", "text/plain": [ "
" ] @@ -241,14 +400,15 @@ } ], "source": [ + "from tqdm import tqdm\n", "views = reddit_graph.rolling(window=\"1 day\") \n", "\n", "timestamps = []\n", "edge_count = []\n", "\n", - "for view in views:\n", - " timestamps.append(view.latest_date_time())\n", - " edge_count.append(view.num_edges()) \n", + "for view in tqdm(views):\n", + " timestamps.append(view.latest_date_time)\n", + " edge_count.append(view.count_edges()) \n", "\n", "sns.set_context()\n", "ax = plt.gca()\n", @@ -267,22 +427,22 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 50, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "" + "" ] }, - "execution_count": 15, + "execution_count": 50, "metadata": {}, "output_type": "execute_result" }, { "data": { - "image/png": "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Ridp+0YEVUUN9YwSFKXGz0WVLaim5IgXqE4IQkhZO7n2T/aCiCZI1RpoOTnZLFjwz7rPrzaEKqkErfL3bu3a1cyd+yBer161d/bZ57JkyRcNM7c48cwz8/nii88ifz/66OPceuudjTgjePvt13n77dfjeo6cnGz++tfZEWO3oTEMJAODeqBJJlTZGlWIrSz+1C64O54BgH3/91hyt8RjehEsWfto8eWHmPfvjut5DKpGdLuwHtDf/+Le/QEBQVVBLRNm1TQIBsv/p4moohVNEytuq8V/mmBGFa2gUvV+lRgcqjkZJalFtQZa27btWLLkQ3777ZcYv2sVKafFU0mI+tj+6xaLlYyMFvGeVlw59mNxOlOw2xuoGbamVnpdRNXnXtPq9WAW1TniSNMPEhoYNCKubmP1f9Thi+zJHIDod5GUsxrH7qWopiQCKR1jPEMd86GDCEqQpHWroEsnjGejhid5+0YETcPXsg3B9JZonmJAz0PSJAk0jdSflmHKP9pocwyktaBg6KjKk3GrSdAdM2Yc69evY968h3n99XdJSkqudL/i4mIWLPgnK1cuJxAI0KvXCdx00yx69+4T2Wfp0i947bV/kZ2dRbduPRg9eiz//OcTfP/9KgB27drBSwueYv3GjXi8Plq2bM3EiZOZMmUqq1evYtasGwCYPPkC7rnnAQAeffQhvv9+FXPnPsiePbtZuPC1yPlycrKZPPkCnnrqOYYMOZUNG9bx4ovPsXnzJlJTUzn99JHccMPNJCdXbpDk5+fx5JOPsWbNKjweL7169WLGjJsZMGCQ/p4GAixc+AJLl36O211Mly7duPbaGzjllNMAPQT42muLuPLKa3jttUUcPnyILl26cdttd3LSSSczd+6DfP75pwAMHz6Y779fxcyZM2jTpi1/+9uDkeOnTLmCxYtfobCwgKFDT+e22+bw/PPPsHLlt9jtDq655nrGj78Q0A2Pt95azMcff0heXi4dOnTissuuYPRo/X62evUqZs++mXnznuSF5+azP+sgbdq05cYbZzFixJksWvQSr766MDKn99//L23atC33vojeQjZuWMMLi99i67atyLLM6aeP5OabbyUlJRXQw7Njx47nmmuujxw3YcJ5jB07nnHjzmfy5AsAmDXrBqZPv67cfg2BcZc0MIgFdWzi6O4wAm96TwRNxbnjU6SSIzGemI5U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/0jHs5JNchUiFxehiSL+1lG25RBFigYMJe37pchuF471v1I0cFhiPDSKMp7MAbU+TC7Mx3LooO496l477xFQqfc2kai1geRyubj++uu5/PLLWbNmDQUFBTidTvr3718uaTtaVFXlueee4/3338flcjFkyBDuv/9+OnSo3ipXVZUZM2bQv39/brnllnKvv/LKK7z//vscOnSIdu3acdVVVzF58uRazy0RCWsgaRZL01BIFiUCqemY845gystNGANJ8PswFegGkK76XfEXR25CLUZA94gBVXadBwi06An2ZMhPXNmCYEoq0mEPclF+zA0kX1o33G1PRZMt1X6u8UrQrg/lBCMTzBubSDSEWGTYe+RvmVmrEKxmsVI08HRSf/oGS84BbLu24ulWdS++RCdp+x9A2HuUGPf2WFLrX9hx48bx2WefYbfbGTFiBOeffz5nnHFGnYwj0GUD3nrrLR5++GHeeecdVFXl2muvxe/3V3mM3+/nnnvuYeXKlRW2vfTSS7z00kvceuut/Pe//2XatGk8+OCDfPzxx3WaX6LRZBK0yxAMN9xMoETtstVAUhU5HX5nB1ydz6m2n1bCoKpInpAGUlUepCZCXCvZRJmS9kPxZA6seh8liFSs52MkTIiNY7yxofkZVCRiIMVLLFLTSqvX2lQenq+OYFoGxX11b03ylvUJIaYrefL0NiNRKLuHkQvzsBzKQkOode5RWWzZv+Pc9l+kBMxDqrWB5Pf7SUuLzU3D7/fzyiuvMGvWLM4880x69+7N/PnzycnJYenSpZUes3r1aiZOnMiqVatwOitarG+//TZXX30148aNo2PHjlxyySVceOGFvP9+3eXTE4mmaCAlYqJ22ZtSVQaSak3F2+pE/GndGmpadUb0liBoGpooolpraEFzdA/JWz7BlrO6YSZXS+JRySa5j0Td4V0uKkRAQzVbEut7JkoEQp3RE+m7lGiUE4uMQzGCXFSA7C5GEyX8rdvWaQxvx25423VGQMO5+kdEb+OWudtyVpO26V1sOWuiPiYpnHvUrmO9IgPmgl1YCnYhFyeeoHCtDaRp06bx9NNPs2bNGjye+imDbtmyBbfbzdChQyOvOZ1O+vTpw2+//VbpMStWrGDEiBF8/PHHOBzls+1VVeWxxx7joosuKve6KIrNRn6gSWggHUM4UVt2F0cq8Bobc1kDqcQNCTKv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YqeUE7e1UGsYziQQCMiOjo7Uv9vb2+XnPvc5+aY3vWlEjUMikcjF8tKK+h0Xt60z4ZgtpuNVSikfeughuXz5cvnss8+Oeu7555+Xy5cvl8eOHUv756oIUhr4yEc+wurVq7nlllvYuXMnNpst5blWVVVRX19fMCHrs1EsdsLEbS3kNuHhlJeXU11djZQSwzCoqanhH//xH1mzZg3PPfcc9913H0CqdboQkONERWbi71jZOjOP2fFsnYnH61gk7f/IRz7CzTffzMc//nF+//vfj/heampqmD17dkZ+x8pBmgTj/VgbGxv5+Mc/ztVXX8373vc+nn76aTo6OojFYmzcuBGn05nllU6PYrETistWGNveZDoiOflb13WklKmT7tKlS9m2bRs9PT3ZXu60GO9COBP3rbK1eGydicfr8PPS8L8n06U2m4277rqL973vfdx5551897vfZe/evfT19fH0008jpczIcGkhx7tCKFKcPHmSxsZGgBEqpIZhoOs6nZ2dPPXUU1x11VU8/vjj/OxnP6OiogKPx0NbWxvf+973WLp0aS5NmBDFYicUl61wbnvb2tr4zW9+w7ve9S48Hs+I7To7OwGorq7OzeInyYMPPkhpaSm33377iMdn4r5VthanrTPpeAUIh8PYbLZUFMg0TTRNG2HvI488wh133MEvfvELfvzjHxONRvH7/QQCAb7zne9kpgA97Um7GcaTTz4pb7jhBrlp06bUY6ZppnK7LS0tcs2aNfKBBx5IPb9lyxb561//Wj755JPyxIkTWV/zVCgWO6UsLlulnLi9X/3qV0e9ttDqNb7whS/IlStXygMHDox4PNn6O5P2rbK1uG2dCcerlFI+8sgj8gMf+ID8u7/7O/npT3869fhwe9euXSu/+MUvpp47ePCg/Otf/yr/9Kc/ydbW1oytTTlI5+B//ud/5OLFi+W73/1u+eKLL454rq2tTV5++eVy/fr1o4TlCo1isVPK4rJVyonbW4gn1+F88YtflGvWrEkV6Z7JyZMnZ8y+VbaeplhtLfTjVUopv/71r8vLLrtMfuc735Hr16+XX/va10Y839bWJi+99FL5mc98Jif7VjlI45D88W3cuFGuXr1a/sM//IN817veJf/yl7+ktvnJT34iv/71rxf0QVksdkpZXLZKWVz2PvbYY3Lp0qVyx44dIx4PBALy5MmTUkpLbfkb3/iGsrWAULbOTFullLKjo0O+7W1vk7///e9HPB4Oh+Xx48ellFK+/PLL8rvf/W7O7FWz2MYhWaOxYMECmpqauOaaa/jjH//Igw8+iMPhYPXq1dx0003TFqLKNcViJxSXrVBc9ra3t1NfX58qxI3FYqxfv579+/fT3d3NwoUL+bd/+zfe8pa3FKzmTxJlq7K10G0Fq+7o1KlTqVqpeDzOPffcw4EDB+jr62P+/Pl8+tOf5tJLL83ZGlWR9lkwDIP+/n5uueUWHnzwwdQU6FAohGEYzJkzhy9/+ctomlZQraNnUix2QnHZCjPf3mQxJ8D73vc+YrEY//u//8u6desYHBzkDW94A3a7nccee4xwOMyvf/1rnE7niEL1QkHZqmwtdFuHE4vFuOmmm3jHO97BHXfcwbp164hEIikH8NFHHyUcDvPzn/8cj8eTE3tVm/8YmKYJWBoSPp+PhQsXsn37di6++GL+v//v/+PUqVPs37+flStXout6wf5Ii8VOKC5boXjs1TQtNZ38gQceoL29nb/927/F6/Xy5S9/mXe+853cfPPN/Nd//RfxeJzvf//7QBoUdnOAslXZWui2JkkkEgghuOqqq9i4cSPPPvssUko++clP8pa3vIW3vvWtfPvb38YwDL71rW8BubFXOUhD/O53v+MXv/gFwIhZLmBJtr/66qsA/OpXv8IwDJYuXcqvf/1rXnjhhZysd6oUi51QXLZCcdk73FaHw4FpmtTU1PCJT3yCAwcO0Nrais/nS21fVVVFbW0twWAwRyueOspWZSsUtq0w0t5kS/973/tejh07xje+8Q1aWlqYNWsWYEW+q6qqmDt3LgMDAzlbc9HXIMkhJdKf//zndHV14fF4eN3rXocQgkQigc1m45prrmHDhg3827/9G5s2beJnP/sZJ06c4IEHHuCxxx5jzZo1uFyuvPboi8VOKC5bobjsHc/WZIrikksu4bbbbuPNb34zDocj9TqbzYbX601dcAohPaFsVbYWuq0wvr0AixYt4j/+4z/4h3/4B8LhMM8++yxvetObUgrgbrc7VSOZC3uL3kFKqnSWlZXxyiuv8MMf/pB4PM4b3/hGbDbr66muruZ///d/mTVrFv/1X/9FQ0MDDQ0NfPzjH2f27Nm43e4cW3FuisVOKC5bobjsPZutYNl51113oes6Bw4cYN++fSxevJhf//rXbN++nX/7t38DCiM9oWxVtha6rXBuey+66CK+//3v87GPfYzvfve77Nu3j+XLl7N582b++te/cueddwK5sVcVaQ9x66234nQ6icViGIbBrbfemtqBgUCAJ554gmuvvZZ58+aNKKorNIrFTiguW6G47D2brfF4nEQiwbp163j55ZdpbGzE6XRy3333FYyS8nCUrcrWQrcVzm4vwP79+/nRj37Eyy+/jMvloqKignvuuYclS5bkbtEZFxLIcwzDkK2trfLmm2+Wr776qjx48KC89dZb5d/93d/J3/zmN6ntotFoDlc5fYrFTimLy1Ypi8veidoqpSUyt2vXLrlr166CnFivbFW2FrqtUk7O3kQiISORiBwYGJDhcDhHKz5N4d5CpglN03A4HNxwww2Ul5czf/587rnnHjRN40c/+hFPP/00cLqIrlApFjuhuGyF4rJ3orYC1NbWsmzZMpYtW0ZFRUUOVz01lK3K1kK3FSZnr5QSp9NJaWlpRobPTpaiS7H9+te/5siRIwAsW7YsVSw2MDBAWVlZajjerl27+MpXvoJpmrzvfe/jDW94Qy6XPWmKxU4oLluhuOxVtipbla2FxUyyt6gcpK997Ws8/vjjrFmzhqNHjxKJRJg3bx7f/OY30XV9RFu0EIJdu3bxwAMP0NXVxT//8z9zww035HD1E6dY7ITishWKy15lq7JV2Vo4tsIMtDcXeb1csG/fPnnDDTfIl19+WUopZSgUkr/5zW/k1VdfLd/97nfLvr4+KaVMTThPzrHavn27/MhHPiJbWlpys/BJUix2SllctkpZXPYqW5WtUipbC4mZaG/ROEibNm2Sl19+uezq6ko9Fo/H5bZt2+TrXvc6+Z73vCf1eHIwXnIHFlJxa7HYKWVx2SplcdmrbFW2KlsLi/+/vfsJiWrv4zj+HibHxkaN/kBgI4kNU+M44yIiKXFTK2kZFIYkhZtoFlOUUFCtSihQyKCmJoUWZmUraVEqSRGUUC2qTVj2Z6ikQPtj6uA8ix6H29x7n3zundu5c36f127ODPJ9M5sv5xzP2LHXmJu0vV4vLpeL/v7+9LF58+ZRVVXFsWPHSCQSRKNRgPS/P88+dyEvL+/XD/wXmdIJZrWCWb1qVatac4sde239oMgbN26QSCSYmJggFArh9/sZHBwkEAhQUVGR/lxlZSV79uyhs7OTx48f//Ae/PsfyGVKJ5jVCmb1qlWtav0uF1rB/r22PYN04sQJjh49yuDgIB0dHZw9e5alS5cyNDREPB7nxYsX6c+6XC5qampIJBI8e/bMuqH/AlM6waxWMKtXrWpVa24xodeWZ5B6e3u5fv06586dY9WqVXz9+pWGhgYmJyc5fvw4u3fvZmZmhsbGRkKhEED6F889Ho/F08+dKZ1gViuY1atWtao1t5jSa8sFaXh4GJ/Ph9/vZ3p6moKCApqamohGoxw8eJBYLEZzczNjY2OsX7+eyspK+vr6GBkZsfax5v8nUzrBrFYwq1etalVrbjGl11YLUuq/v/Y7OjrKhw8fcDgc6Zu/iouLSSaTJBIJqquraW9vp7u7m4sXL5KXl4fb7SYej1NSUmJxxc+Z0glmtYJZvWpVq1pzpxXM67XVgjR7o9emTZt4+PAhr169wuv1At+/PKfTydTUFKlUimAwSDAY5NOnT+knexYWFlo5/pyZ0glmtYJZvWpVq1pzpxXM67XVgjSrpqYGn8/H4sWL08c+f/6c3mJndXZ24nK52LZtmxVj/m2mdIJZrWBWr1rVqtbcYkqvbf+LbdmyZT88W+Hdu3ckk0kKCwtxOBy0tbXR0tLCmjVrLJzy7zOlE8xqBbN61arWXGdSK5jRa9sFKdP09DROpxOPx0N7ezvxeJzu7m58Pp/Vo2WVKZ1gViuY1atWteY6k1rBnr22vMT2W7M3leXn51NUVMShQ4e4efMmXV1dBINBq8fLGlM6waxWMKtXrWrNdSa1gr17bb8gzd5UtmLFCkZHRxkYGODy5cusXr3a4smyy5ROMKsVzOpVq1pznUmtYO9eYy6xlZWVUV9fT09Pjy2+uD9jSieY1Qpm9arVntRqX3bsdaRSqZTVQ/wq09PT/9ofxcsmUzrBrFYwq1et9qRW+7Jbr1ELkoiIiMhcGHOJTURERGSutCCJiIiIZNCCJCIiIpJBC5KIiIhIBi1IIiIiIhm0IImIiIhksP2TtEXEvpqbm7l27dr//ExJSQlv3ryhr6+P5cuX/6LJRCTX6TlIIpKzXr58ycePH9OvT58+zZMnTzh16lT62NTUFC6Xi0AggMvlsmJMEclBOoMkIjmrtLSU0tLS9OtFixbhcrmoqqqybigRsQXdgyQittbT04Pf7+f169fA98tyO3fu5NKlS2zcuJFQKMTWrVt5/vw5AwMDbN68mXA4zJYtW3j69OkPf2toaIjt27cTDodZu3YtBw4c+OEMlojYh84giYhxHjx4wPv372lubmZycpIjR47Q1NSEw+EgEongdrs5fPgw+/bto7e3F4D79+/T2NjIunXraG1tZWxsjLa2NhoaGrhy5Qrz58+3uEpEskkLkogY58uXL7S2tlJeXg7AvXv36OrqoqOjg+rqagBGRkZoaWlhfHycoqIiTp48SVlZGWfOnMHpdAIQDoepq6vj6tWr1NfXW9YjItmnS2wiYpzi4uL0cgSwZMkS4PvCM2vhwoUAjI+PMzExwaNHj6itrSWVSpFMJkkmk3i9XsrLy7lz584vnV9E/nk6gyQixvF4PH94vKCg4A+Pj4+PMzMzQywWIxaL/e79/Pz8rM4nItbTgiQi8hMLFizA4XCwY8cO6urqfve+2+22YCoR+SdpQRIR+QmPx0MgEGB4eJjKysr08W/fvhGJRKitrWXlypUWTigi2aZ7kERE5iAajXL79m327t3LrVu36O/vZ9euXdy9e5eKigqrxxORLNOCJCIyBxs2bOD8+fO8ffuWSCTC/v37cTqdXLhwQQ+mFLEh/dSIiIiISAadQRIRERHJoAVJREREJIMWJBEREZEMWpBEREREMmhBEhEREcmgBUlEREQkgxYkERERkQxakEREREQyaEESERERyaAFSURERCSDFiQRERGRDP8BUdljrxhE02UAAAAASUVORK5CYII=", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAk4AAAHYCAYAAAC7jmc2AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOydd5gT5fbHPzOTupvt9N5BRVAEFQVBQLwoqBfBglgQxYYoXrFdxcJFuYjiRUWFKxZULD8LiqhYsF4boggivfeFremZ8vtjNtkNm91Nssludp3P8/jIZmbeOe8kmZw557zfI2iapmFgYGBgYGBgYFAjYn0bYGBgYGBgYGDQUDAcJwMDAwMDAwODKDEcJwMDAwMDAwODKDEcJwMDAwMDAwODKDEcJwMDAwMDAwODKDEcJwMDAwMDAwODKDEcJwMDAwMDAwODKDEcJwMDAwMDAwODKDHVtwGNBU3TUFVdS1QUhdC/GzN/lXmCMdfGyl9lrn+VeYIx18ZMMuYrigKCIMR0jOE4JQhV1SgocGEyieTkpFNS4kaW1fo2K2n8VeYJxlwbK3+Vuf5V5gnGXBszyZpvbm46khSb42Sk6gwMDAwMDAwMosRwnAwMDAwMDAwMosRwnAwMDAwMDAwMosRwnAwMDAwMDAwMosRwnAwMDAwMDAwMosRYVWdgYGCQ4qiqiqLIcR4r4PVK+P0+FKVxL1035tp4iWe+kmRCFBMfHzIcJwMDA4MURdM0SkoK8HictRrn8GERVW38S9bBmGtjJp752u0OMjNzY9Zqqg7DcTIwMDBIUYJOk8ORg8VijfvmL0nCXyIqAcZcGzOxzFfTNPx+H05nIQBZWXkJs8NwnAwMDAxSEFVVQk6Tw5FZq7FMJvEvIZIIxlwbM7HO12KxAuB0FpKRkZOwtJ1RHG5gYGCQgiiKApTf/A0MDGIn+P2Jt0YwEobjZGBgYJDCJLI2w8Dgr0Yyvj+G42RgYGBgYGBgECWG42RgYGBgYGBgECWG42RgYGDQ2NHqd+XVmDGjGDCgb+i/gQP7MXz4ICZPnsRvv61O2HlmznyAG264NvT377//xpo1vwGwf/8+Bgzoy+rVqxJ2vrpGlmXeeOPV0N8LFz7LmDGj6tEiffXaRx8to7CwIOL2xnDdj0bQtHr+RjUSFEWloMCFySSSk5NOYaGrUa92+KvME4y5NlZSfa6BgJ8jR/aTl9cSs9kS9ziivxQx4Eax56KJ5gRaGD1jxoxi8OChXHrpeED340pKinjuuadZteonXn31bVq0aFHr8zidTgRBIz09A4ABA/pyzz33c845o1AUhaKiQjIzszCb6+c61JaPPlrGzJkP8O23uhPi93txuTzk5OTUm02//voLN998HW+99T4tW7aqtD2R1z2eVYQ1fY9yc9ORpNhiSEbEycDAwKCBIatV/6dU/F3RNFS/B1kTUL3O6veNZdw4sNvt5OU1IS+vCU2aNKFTpy5Mm3YPPp+Pr79eWbvBy3A4HGRlZUXcJkkSeXlNGqzTBHp0pyJpaWn16jRBZZuOpjFc96MxdJwMDAwMGhif7qz6mbepXaNvi7IfM01hRX4zlCqekXNtGqe0LP/h+3K3QECNvAopy6JxWuvEJigkSQLAYtF/VH0+Ly+//AIrVnzMkSP5tGvXgauumsjgwUMBPXrx3HNP89lnn1BYWEDLlq246KJLueCCMYCeqjtwYD9PPvkcAwb0BeDhhx/k119/4eqrJzF27HnMm/csBw7sZ86cR1i69BMyMjJC9lx00fkMG3Y2kybdSH7+IZ56ai4//vg9oihx/PG9mDx5Km3btos4l5psA/jww/d57bWX2b9/Py1btuT88y9kzJiLEUWR/fv3MXbsefzrX//m1VdfZsuWTeTlNeHyyydw/vmjWb78Ax5++EFAj6TNm/csa9as5sMPP+D//u+D0PEPPDCTV199iZ07d9CxY2emT5/BypWf8fbbbyLLMsOGnc1tt90RWm323Xff8Pzzz7Fjx3aaNm3KsGFnc+WVE7FYLKFz3XXXfXz66SesXbuGjAwHF1wwhgkTrmX16lVMmXI9AGPHnheK7lUkaNe8ec/Sp09fJk+exHHHHU9RUSFfffUFqqpx+ukDmTbtbtLS0mvxaao76j3ipKoq8+bNY+DAgZxwwglce+217N69u8r9N2/ezKRJkzjllFPo378/U6ZMYd++faHtiqLQq1cvunfvHvbfk08+Gdpnz549XHfddfTp04cBAwbwxBNPhDRTDAwMDBoLguKvbxOqJD//EI8/Phu73c6ppw4A4IEH/slHHy1j6tRpvPjiEgYOHMR9993F119/CcC7777FypWf8+CDD7NkyTtceOFFzJkzK1THVJGlSz8GYMqUf3DLLbeHbTvzzGFIkomvvvo89NratWvYt28v55wzCo/Hw803XwfAk08u4KmnniMrK5tJk64iP/9QxPnUZNvSpe/w9NP/YcKEa1m8+A2uvfYGXn31RZ599smwcebNe5wrr7yaV155i9NOG8Bjj81i3769DB16FlOm/CM0t+OP7x3RjgUL5jNlyj9YsOAlSktLuP76q9m9eydPPbWA6667kXfffYvvvvsGgB9++B/Tp9/Feef9ncWL3+Af/7iLL774lBkzpoeN+dRTT3DOOSN55ZU3ufDCi3n++ef47bfVHH98b2bOnA3AwoUvMXToWRFtOpo333yN3Nw8Fi58menTH+Kbb77kjTdei+rYVKDeI07z58/ntddeY9asWbRo0YJHH32Ua665hg8++CDk8QYpLCxkwoQJ9OnTh8WLF+P3+5k1axbXXHMN7777LlarlR07duDz+Vi6dCl5eeUS62lpaQAEAgEmTpxIhw4deP3119m1axf//Oc/EUWRKVOm1OncDQwMDOLhrPZV580qxosEJcDwpofAko5sqvw0f3RsaXBbDYgcVaqtGs7ixS/w+uuvAPoDrt/vp0OHjjz0kH7v37FjO9988xX//vdcTjtNd6QmTryOLVs2s3jxIs44YzB79+7FbrfRsmVrmjRpwoUXXky7dh1o165yFCgvrwmgp+8cDgelpSWhbXa7nTPPHMqKFR8zcuQFAKxYoTsjbdq0Zdmy93A6S7nvvhmYTPrP5F133cevv/7C+++/y8SJ11U6X022vfTS81x11USGDTsbgNat2+ByuXjssX8zceL1oXEuueQyBgwYBMCkSTfxzjtv8ccfaznrrL/hcDjC5haJSy+9nBNPPAmAQYOG8NZbS5g27Z/YbDbat+/A888vYPv2rQwYcAYvv7yI884bzQUXXBiyadq0e5gy5Xr2798XqlkaMWIkZ599DgBXXHE1r722mLVr13DCCX3IyNBV7bOzc7BabVXaVZEOHTpy3XU3AdC2bTv69TuVtWvXRHVsKlCvjpPf72fRokXcfvvtDB48GIC5c+cycOBAVqxYwciRI8P2/+yzz3C73cyePRubTX+DHn30UQYPHszq1avp378/GzduxOFw0KNHj4jn/OSTT9i3bx9vvvkmWVlZdOvWjSNHjjB79myuv/76Ss6agYGBQaphiiZXoGkIqh+ToIHJhKB60QQJpKprTaIaN04uuOBCxoy5BABR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P2PHnsfjjz9Fv36nsHbtGp599in+/HM92dnZnH76GVx//U2kVxFxLSws4LHH/s2vv67C4/HSvXt3Jk26iRNPPEm/roEACxc+w4oVH+FyOenYsTPXXHM9J598KqCnEl966XmuvHIiL730PIcOHaRjx8784x93cNxxvZg58wE++kjv4zpgQF++/XYVkydPomXLVvzznw+Ejr/00st5+eVFFBcX0b//6dx66zTmz5/HN998icORwcSJ1zFy5Pllb6XGa6+9zHvvvUNBwWHatm3PuHGXM3y43qps9epVTJ16E7NmPcb8+fPYs2c3LVu24oYbbmbgwME8//xzvPDCwpBNb731Pi1btqp0bdat+50FC+azceOfmEwmTj/9DG666RaysrIBPc07YsRIJk68LnRM8LVzzhnF2LHnATBlyvVMmHBt2H7JwnjUM6gTLPn7kcoa+vqbtYy4Tyqm6iR3mRKx261HR2rAVFGKoLYIIu7WpxDIap+y0SaIrb4JAEEIrb4z0nU1oOqRJVWsGL1J3Ua2IadJEEGo/OBw9933UVpaypNPPhHxeE3TmDZtCvv27eXf/36CBQte4rjjjueGGyayadMGQK9nmTnzfs4993xefHEJ5547imeeeVIfQBTxer3ccvfdZGZm8eyzi1i8+E3OPHMoTz/9BJs3b+T443szc6beIH7hwpcYOvSsMBvOOWcUf/75B3v37gm9tmLFRzRt2oyTTurHli2bufXWGznllP689NIS7r9/Jhs3/snUqZOrjH7MmfMIfr+PJ59cwMsvv07btu25++5/4PF4AL3O6+eff2D69BksWvQqQ4YM4447buV///s2NMbBgwd47723ue++GTz//CvY7XZmzJiOpmnccsvtDBlyFj179qoy5XjgwH5WrvycOXP+w7/+NZtvvvmKyy+/mG7duvP884s59dTTeOyxWRQXFwGwYMF83nvvbaZOncbLL7/B2LGXMGfOLN55563QmIqiMH/+PG69Vd+nU6fO/Otf9+N2u7n00su55JLxNGvWnKVLP6ZZs8oraNevX8fNN19Hx46deO65F5kx49+sX7+OqVMno0QRVW3WrHnIwZ05czaXXnp5jcckAiPiZFAnVGzoW1UkJhWLwyVXmS1a2Wo/S/UChMGIkxxHj7qGSqwRp+C+5pIiw3GqDk0LpZxC9U2CoEd8YkzVmUwicgyp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vHkzKioqEBMTgxkzZiA0NBQ5OTl44IEHMGfOHDz22GMAgIsXL2LWrFn45ptvYDQa0b9/f/zjH/+A0WgEAFgsFixfvhypqanIy8tDaGgonnnmGQwbNszxmn/0HEREREQ3onjjREREROQqlD/nRUREROQi2DgREREROYmNExEREZGT2DgREREROYmNExEREZGT2DgREREROYmNExEREZGT2DgREREROYmNExEREZGT2DgREREROYmNExEREZGT2DgR3Sbl5eVKl0BETuBYpVvBxokUYbPZlC7htnr//fexfft2AIDW7qOttbxqx7GqXlrLW1fYON0Bjh8/joqKCqXLuO3Wrl2LlJQUAIBOp1PtAXnx4sV48803HQdjtTty5Aj27duH48ePw2azQZIk1R6QOVbVhWNVvWP1wIEDyM3NvS3P7XZbnpWcYrPZcPr0aQwdOhSzZs3CwIED4e7urnRZdU4IgZKSEnz22Wc4duwYPD098cQTTzgOyDqdevr3119/HVu3bkW/fv1QUlICAJAkSeGqbp/58+djx44duHz5Mpo0aQIfHx8kJSUhLCxM6dLqFMcqx6qr09JYPX/+PJ5//nk8+eSTGDZsGIKCgur0NdQzClyQTqdDaGgohBCYN28e/vWvf8FsNitdVp2TJAleXl6IiYmBv78/lixZgrVr1wJQ11+zb775Jj777DNs2rQJY8aMwX//+1+cPn1a6bJumy1btiA1NRVz587F+vXrMXXqVEiShOHDhyM9PV3p8uoUxyrHqivT0liVJAkhISHQ6/VYs2YN1q9fj8LCwjp9DTZOCrNarQgODkbLli0xY8YMpKamqvKADADu7u7w8/PDoEGDsHLlSrz//vsA7Adki8WicHW189Zbb2Hjxo348MMPERoaCoPBALPZjIKCAqVLu23y8/MRExODrl27omXLlujRoweWLVuGTp06YeLEicjMzASgnnkUHKscq65KS2NVPmvYuHFjREdHY+nSpfjggw/qtHli46Sw7777DhaLBatWrcL48ePx2muv4bPPPlPlATkuLg7h4eEYOXIkevfujRUrViA1NRXvvPMODh065LJ/zZaWlqKoqAiffvop2rZtCyEEOnTogI4dO2LDhg0wm82qOCBdq6ioCMeOHXN8bbPZ4Ovri4ULF6Jz586YNGkSCgsLVTOPgmOVY9VVaW2sfv/99ygr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", "text/plain": [ "
" ] @@ -299,19 +459,12 @@ "negative_sentiment_in = []\n", "negative_sentiment_out = []\n", "\n", - "def edge_weight(edge,prop_name):\n", - " return sum(list(map(lambda e: e[1],edge.property_history(prop_name)))), len(edge.property_history(prop_name))\n", - "\n", - "def weighted_average_degree(vertex,prop_name,incoming):\n", - " edges = list(vertex.in_edges() if incoming else vertex.out_edges())\n", - " return sum(list(map(lambda e: edge_weight(e,prop_name)[0],edges)))/max(1,sum(list(map(lambda e: edge_weight(e,prop_name)[1],edges))))\n", - "\n", "for vertex in views:\n", - " timestamps.append(vertex.latest_date_time())\n", - " positive_sentiment_in.append(weighted_average_degree(vertex,\"positive_sentiment\",incoming=True))\n", - " positive_sentiment_out.append(weighted_average_degree(vertex,\"positive_sentiment\",incoming=False))\n", - " negative_sentiment_in.append(weighted_average_degree(vertex,\"negative_sentiment\",incoming=True))\n", - " negative_sentiment_out.append(weighted_average_degree(vertex,\"negative_sentiment\",incoming=False)) \n", + " timestamps.append(vertex.latest_date_time)\n", + " positive_sentiment_in.append(vertex.in_edges.properties.temporal.get(\"positive_sentiment\").values().sum().mean())\n", + " positive_sentiment_out.append(vertex.out_edges.properties.temporal.get(\"positive_sentiment\").values().sum().mean())\n", + " negative_sentiment_in.append(vertex.in_edges.properties.temporal.get(\"negative_sentiment\").values().sum().mean())\n", + " negative_sentiment_out.append(vertex.out_edges.properties.temporal.get(\"negative_sentiment\").values().sum().mean())\n", "\n", "sns.set()\n", "sns.set_palette(\"pastel\")\n", @@ -327,6 +480,89 @@ "ax.legend(loc=\"best\")\n" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Running some example algorithms" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Lets get the top 5 subreddits in the graph as per pagerank" + ] + }, + { + "cell_type": "code", + "execution_count": 51, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The conspiracy subreddit has a pagerank score of 0.0028709357281156145\n", + " Key Value\n", + "1746 askreddit 0.019556\n", + "295 iama 0.015616\n", + "3236 pics 0.009884\n", + "22085 funny 0.009283\n", + "40067 videos 0.006105\n", + "The top five ranked subreddits are [('askreddit', 0.019555592169738754), ('iama', 0.01561587791951029), ('pics', 0.009884204062062652), ('funny', 0.009282589235120708), ('videos', 0.006105153065518092)]\n" + ] + } + ], + "source": [ + "from raphtory import algorithms as algos\n", + "\n", + "#First lets run the algorithm and get our result set\n", + "result_set=algos.pagerank(reddit_graph)\n", + "\n", + "#We can then have a look at the values of specific nodes\n", + "print(\"The conspiracy subreddit has a pagerank score of\",result_set.get('conspiracy'))\n", + "\n", + "#Convert the results to a dataframe\n", + "print(result_set.to_df().sort_values(by='Value',ascending=False).head())\n", + "\n", + "#get the top 5 most import users via the intial top_k function\n", + "print(\"The top five ranked subreddits are \",result_set.top_k(5))\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Getting the largest connected component" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "The largest component has 52468 nodes out of a total of 54075\n" + ] + } + ], + "source": [ + "#First lets run the algorithm and group by the component_id\n", + "components=algos.weakly_connected_components(reddit_graph).group_by()\n", + "\n", + "#Map the returned dict so that we have the size of the components instead of the vertex names\n", + "component_sizes = {key: len(value) for key, value in components.items()}\n", + "#Get the component id with the greatest number of nodes\n", + "component_with_biggest_size = max(component_sizes,key=component_sizes.get)\n", + "#Get the value of this component\n", + "lcc=component_sizes[component_with_biggest_size]\n", + "print(\"The largest component has\",lcc,\"nodes out of a total of\",reddit_graph.count_vertices())" + ] + }, { "cell_type": "code", "execution_count": null, diff --git a/examples/rust/Cargo.toml b/examples/rust/Cargo.toml index 7a062f7116..99817d23ab 100644 --- a/examples/rust/Cargo.toml +++ b/examples/rust/Cargo.toml @@ -7,8 +7,7 @@ keywords = ["graph", "temporal-graph", "temporal", "examples"] # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -raphtory = {path = "../../raphtory"} -raphtory-io = {path = "../../raphtory-io"} +raphtory = {path = "../../raphtory", features=["io"]} chrono = "0.4" regex = "1" serde = { version = "1", features = ["derive", "rc"] } @@ -25,9 +24,6 @@ name = "bench" [[bin]] name = "lotr" -[[bin]] -name = "healthcheck" - [[bin]] name = "hulongbay" diff --git a/examples/rust/src/bin/bench/main.rs b/examples/rust/src/bin/bench/main.rs index 2978b92d77..166c08b8fd 100644 --- a/examples/rust/src/bin/bench/main.rs +++ b/examples/rust/src/bin/bench/main.rs @@ -1,13 +1,13 @@ -use itertools::Itertools; -use raphtory::core::utils; -use raphtory::core::Prop; -use raphtory::db::graph::Graph; -use raphtory::db::view_api::*; -use raphtory_io::graph_loader::source::csv_loader::CsvLoader; +use raphtory::{ + algorithms::pagerank::unweighted_page_rank, graph_loader::source::csv_loader::CsvLoader, + prelude::*, +}; use serde::Deserialize; -use std::path::PathBuf; -use std::{env, path::Path, time::Instant}; -use raphtory::algorithms::pagerank::unweighted_page_rank; +use std::{ + env, + path::{Path, PathBuf}, + time::Instant, +}; #[derive(Deserialize, std::fmt::Debug)] pub struct Benchr { @@ -43,40 +43,26 @@ fn main() { println!( "Loaded graph from encoded data files {} with {} vertices, {} edges which took {} seconds", encoded_data_dir.to_str().unwrap(), - g.num_vertices(), - g.num_edges(), + g.count_vertices(), + g.count_edges(), now.elapsed().as_secs() ); g } else { - let g = Graph::new(8); + let g = Graph::new(); let now = Instant::now(); - CsvLoader::new(data_dir, ) + CsvLoader::new(data_dir) .set_delimiter("\t") .load_into_graph(&g, |lotr: Benchr, g: &Graph| { - g.add_vertex( - 1, - lotr.src_id.clone(), - &vec![], - ) + g.add_vertex(1, lotr.src_id.clone(), NO_PROPS) .expect("Failed to add vertex"); - g.add_vertex( - 1, - lotr.dst_id.clone(), - &vec![], - ) + g.add_vertex(1, lotr.dst_id.clone(), NO_PROPS) .expect("Failed to add vertex"); - g.add_edge( - 1, - lotr.src_id.clone(), - lotr.dst_id.clone(), - &vec![], - None, - ) + g.add_edge(1, lotr.src_id.clone(), lotr.dst_id.clone(), NO_PROPS, None) .expect("Failed to add edge"); }) .expect("Failed to load graph from CSV data files"); @@ -84,8 +70,8 @@ fn main() { println!( "Loaded graph from CSV data files {} with {} vertices, {} edges which took {} seconds", encoded_data_dir.to_str().unwrap(), - g.num_vertices(), - g.num_edges(), + g.count_vertices(), + g.count_edges(), now.elapsed().as_secs() ); @@ -95,6 +81,6 @@ fn main() { g }; println!("Data loaded\nPageRanking"); - let r = unweighted_page_rank(&graph, 25, Some(8), None,true); + unweighted_page_rank(&graph, 25, Some(8), None, true); println!("Done PR"); } diff --git a/examples/rust/src/bin/btc/main.rs b/examples/rust/src/bin/btc/main.rs index 71cd352a54..339a9fd889 100644 --- a/examples/rust/src/bin/btc/main.rs +++ b/examples/rust/src/bin/btc/main.rs @@ -1,23 +1,21 @@ #![allow(unused_imports)] -use std::collections::HashMap; -use std::marker::PhantomData; -use std::path::{Path, PathBuf}; -use std::thread::JoinHandle; -use std::{env, thread}; +#![allow(dead_code)] use chrono::{DateTime, Utc}; -use raphtory::core::tgraph::TemporalGraph; -use raphtory::core::utils; -use raphtory::core::{Direction, Prop}; -use raphtory_io::graph_loader::source::csv_loader::CsvLoader; +use raphtory::{core::utils::hashing, graph_loader::source::csv_loader::CsvLoader, prelude::*}; use regex::Regex; use serde::Deserialize; -use std::fs::File; -use std::io::{prelude::*, BufReader, LineWriter}; -use std::time::Instant; - -use raphtory::db::graph::Graph; -use raphtory::db::view_api::*; +use std::{ + collections::HashMap, + env, + fs::File, + io::{prelude::*, BufReader, LineWriter}, + marker::PhantomData, + path::{Path, PathBuf}, + thread, + thread::JoinHandle, + time::Instant, +}; #[derive(Deserialize, std::fmt::Debug)] pub struct Sent { @@ -56,7 +54,7 @@ fn main() { panic!("Missing data dir = {}", data_dir.to_str().unwrap()) } - let test_v = utils::calculate_hash(&"139eeGkMGR6F9EuJQ3qYoXebfkBbNAsLtV:btc"); + let test_v = hashing::calculate_hash(&"139eeGkMGR6F9EuJQ3qYoXebfkBbNAsLtV:btc"); // If data_dir/graphdb.bincode exists, use bincode to load the graph from binary encoded data files // otherwise load the graph from csv data files @@ -70,22 +68,22 @@ fn main() { println!( "Loaded graph from path {} with {} vertices, {} edges, took {} seconds", encoded_data_dir.to_str().unwrap(), - g.num_vertices(), - g.num_edges(), + g.count_vertices(), + g.count_edges(), now.elapsed().as_secs() ); g } else { - let g = Graph::new(16); + let g = Graph::new(); let now = Instant::now(); CsvLoader::new(data_dir) .with_filter(Regex::new(r".+(sent|received)").unwrap()) .load_into_graph(&g, |sent: Sent, g: &Graph| { - let src = utils::calculate_hash(&sent.addr); - let dst = utils::calculate_hash(&sent.txn); + let src = hashing::calculate_hash(&sent.addr); + let dst = hashing::calculate_hash(&sent.txn); let time = sent.time.timestamp(); if src == test_v || dst == test_v { @@ -96,18 +94,18 @@ fn main() { time, src, dst, - &vec![("amount".to_string(), Prop::U64(sent.amount_btc))], + [("amount".to_string(), Prop::U64(sent.amount_btc))], None, ) - .unwrap() + .unwrap(); }) .expect("Failed to load graph from CSV data files"); println!( "Loaded graph from CSV data files {} with {} vertices, {} edges which took {} seconds", encoded_data_dir.to_str().unwrap(), - g.num_vertices(), - g.num_edges(), + g.count_vertices(), + g.count_edges(), now.elapsed().as_secs() ); @@ -117,8 +115,8 @@ fn main() { g }; - assert_eq!(graph.num_vertices(), 9132396); - assert_eq!(graph.num_edges(), 5087223); + assert_eq!(graph.count_vertices(), 9132396); + assert_eq!(graph.count_edges(), 5087223); let windowed_graph = graph.window(0, i64::MAX); diff --git a/examples/rust/src/bin/crypto/main.rs b/examples/rust/src/bin/crypto/main.rs index c5d35fc7a6..fb635a2d10 100644 --- a/examples/rust/src/bin/crypto/main.rs +++ b/examples/rust/src/bin/crypto/main.rs @@ -1,27 +1,12 @@ -use chrono::NaiveDateTime; use itertools::Itertools; -use raphtory::algorithms::generic_taint::generic_taint; -use raphtory::algorithms::pagerank::unweighted_page_rank; -use raphtory::core::time::TryIntoTime; -use raphtory::db::view_api::internal::GraphViewInternalOps; -use raphtory::db::view_api::layer::LayerOps; -use raphtory::db::view_api::time::WindowSet; -use raphtory::db::view_api::*; -use raphtory_io::graph_loader::example::stable_coins::stable_coin_graph; -use serde::Deserialize; -use std::env; -use std::time::Instant; - -#[derive(Deserialize, std::fmt::Debug)] -pub struct StableCoin { - block_number: String, - transaction_index: u32, - from_address: String, - to_address: String, - time_stamp: i64, - contract_address: String, - value: f64, -} +use raphtory::{ + algorithms::{ + pagerank::unweighted_page_rank, temporal_reachability::temporally_reachable_nodes, + }, + db::api::view::*, + graph_loader::example::stable_coins::stable_coin_graph, +}; +use std::{env, time::Instant}; fn main() { let args: Vec = env::args().collect(); @@ -32,13 +17,13 @@ fn main() { Some(args.get(1).unwrap().to_string()) }; - let g = stable_coin_graph(data_dir,true, 1); + let g = stable_coin_graph(data_dir, true); - assert_eq!(g.num_vertices(), 1523333); - assert_eq!(g.num_edges(), 2814155); + assert_eq!(g.count_vertices(), 1523333); + assert_eq!(g.count_edges(), 2814155); assert_eq!( - g.get_unique_layers().into_iter().sorted().collect_vec(), + g.unique_layers().into_iter().sorted().collect_vec(), vec!["Dai", "LUNC", "USD", "USDP", "USDT", "USTC"] ); @@ -49,35 +34,22 @@ fn main() { let now = Instant::now(); - let _ = unweighted_page_rank( - &g, - 20, - None, - None, - true, - ); + let _ = unweighted_page_rank(&g, 20, None, None, true); println!("Time taken: {} secs", now.elapsed().as_secs()); let now = Instant::now(); - let _ = unweighted_page_rank( - &g.layer("USDT") - .unwrap(), - 20, - None, - None, - true - ); + let _ = unweighted_page_rank(&g.layer("USDT").unwrap(), 20, None, None, true); println!("Time taken: {} secs", now.elapsed().as_secs()); println!("Generic taint"); let now = Instant::now(); - let _ = generic_taint( + let _ = temporally_reachable_nodes( &g.layer("USDT").unwrap(), None, 20, 1651105815, vec!["0xd30b438df65f4f788563b2b3611bd6059bff4ad9"], - vec![], + None, ); println!("Time taken: {} secs", now.elapsed().as_secs()); } diff --git a/examples/rust/src/bin/healthcheck/main.rs b/examples/rust/src/bin/healthcheck/main.rs deleted file mode 100644 index c143017ec6..0000000000 --- a/examples/rust/src/bin/healthcheck/main.rs +++ /dev/null @@ -1,208 +0,0 @@ -fn main() {} - -#[cfg(test)] -mod test { - use std::{ - fmt::Debug, - path::{Path, PathBuf}, - }; - - use itertools::Itertools; - use raphtory::algorithms::connected_components::weakly_connected_components; - use raphtory::core::Direction; - use raphtory::db::{ - graph::Graph, - view_api::*, - view_api::{internal::GraphViewInternalOps, GraphViewOps}, - }; - use raphtory_io::graph_loader::source::csv_loader::CsvLoader; - use serde::de::DeserializeOwned; - - trait TestEdge { - fn src(&self) -> u64; - fn dst(&self) -> u64; - fn t(&self) -> i64; - } - - fn load(g1: &Graph, gn: &Graph, p: PathBuf) { - CsvLoader::new(p) - .set_delimiter(" ") - .load_into_graph(&(g1, gn), |pair: REC, (g1, gn)| { - g1.add_edge(pair.t(), pair.src(), pair.dst(), &vec![], None); - gn.add_edge(pair.t(), pair.src(), pair.dst(), &vec![], None); - }) - .expect("Failed to load graph from CSV files"); - } - - fn test_graph_sanity(p: P, n_parts: usize) - where - P: Into, - { - let path: PathBuf = p.into(); - let g1 = Graph::new(1); - let gn = Graph::new(n_parts); - - load::(&g1, &gn, path); - - fn check_graphs(g1: &G, gn: &G, n_parts: usize) { - assert_eq!(g1.num_vertices(), gn.num_vertices()); - // NON-TEMPORAL TESTS HERE! - - let mut expect_1 = g1.vertices().id().collect::>(); - let mut expect_n = gn.vertices().id().collect::>(); - - expect_1.sort(); - expect_n.sort(); - - assert_eq!(expect_1, expect_n, "Graphs are not equal {n_parts}"); - - for v_ref in g1.vertices().id() { - let v1 = g1.vertex(v_ref).unwrap().id(); - let vn = gn.vertex(v_ref).unwrap().id(); - assert_eq!(v1, vn, "Graphs are not equal {n_parts}"); - - let v_id = v1; - let v1 = g1.vertex(v_id).unwrap(); - let vn = gn.vertex(v_id).unwrap(); - let mut expect_1 = v1.neighbours().id().collect_vec(); - let mut expect_n = vn.neighbours().id().collect_vec(); - expect_1.sort(); - expect_n.sort(); - assert_eq!(expect_1, expect_n, "Graphs are not equal {n_parts}"); - - let mut expect_1 = v1.in_neighbours().id().collect_vec(); - let mut expect_n = vn.in_neighbours().id().collect_vec(); - expect_1.sort(); - expect_n.sort(); - assert_eq!(expect_1, expect_n, "Graphs are not equal {n_parts}"); - - let mut expect_1 = v1.out_neighbours().id().collect_vec(); - let mut expect_n = vn.out_neighbours().id().collect_vec(); - expect_1.sort(); - expect_n.sort(); - assert_eq!(expect_1, expect_n, "Graphs are not equal {n_parts}"); - - // now we test degrees - let expect_1 = v1.degree(); - let expect_n = vn.degree(); - assert_eq!(expect_1, expect_n, "Graphs are not equal {n_parts}"); - - let expect_1 = v1.in_degree(); - let expect_n = vn.in_degree(); - assert_eq!(expect_1, expect_n, "Graphs are not equal {n_parts}"); - - let expect_1 = v1.out_degree(); - let expect_n = vn.out_degree(); - assert_eq!(expect_1, expect_n, "Graphs are not equal {n_parts}"); - } - } - - check_graphs(&g1, &gn, n_parts); - - // TEMPORAL TESTS HERE! - let t_start = 0; - let t_end = 100; - let g1_w = g1.window(t_start, t_end); - let gn_w = gn.window(t_start, t_end); - - check_graphs(&g1_w, &gn_w, n_parts); - } - - #[derive(serde::Deserialize, Debug)] - struct Pair { - src: u64, - dst: u64, - t: i64, - } - - impl TestEdge for Pair { - fn src(&self) -> u64 { - self.src - } - - fn dst(&self) -> u64 { - self.dst - } - - fn t(&self) -> i64 { - self.t - } - } - - #[test] - fn load_graph_from_cargo_path() { - let csv_path: PathBuf = [env!("CARGO_MANIFEST_DIR"), "../../resource/", "test2.csv"] - .iter() - .collect(); - - let p = Path::new(&csv_path); - assert!(p.exists()); - - for n_parts in 1..33 { - test_graph_sanity::<&Path, Pair>(p, n_parts); - } - } - - #[derive(serde::Deserialize, Debug)] - struct PairNoTime { - src: u64, - dst: u64, - } - - impl TestEdge for PairNoTime { - fn src(&self) -> u64 { - self.src - } - - fn dst(&self) -> u64 { - self.dst - } - - fn t(&self) -> i64 { - 1 - } - } - - #[test] - fn connected_components() { - let csv_path: PathBuf = [env!("CARGO_MANIFEST_DIR"), "../../resource/", "test3.csv"] - .iter() - .collect(); - - let p = Path::new(&csv_path); - assert!(p.exists()); - let window = -100..100; - - for n_parts in 2..3 { - let g1 = Graph::new(1); - let gn = Graph::new(n_parts); - load::(&g1, &gn, csv_path.clone()); - - let iter_count = 50; - let cc1 = weakly_connected_components(&g1, iter_count, None); - let ccn = weakly_connected_components(&gn, iter_count, None); - - // get LCC - let counts = cc1.iter().counts_by(|(_, cc)| cc); - let max_1 = counts - .into_iter() - .sorted_by(|l, r| l.1.cmp(&r.1)) - .rev() - .take(1) - .next(); - - // get LCC - let counts = ccn.iter().counts_by(|(_, cc)| cc); - let max_n = counts - .into_iter() - .sorted_by(|l, r| l.1.cmp(&r.1)) - .rev() - .take(1) - .next(); - - assert_eq!(max_1, Some((&6, 1039))); - assert_eq!(max_1, max_n); - println!("{:?}", max_1); - } - } -} diff --git a/examples/rust/src/bin/hulongbay/main.rs b/examples/rust/src/bin/hulongbay/main.rs index cfce358570..a80ffab8e0 100644 --- a/examples/rust/src/bin/hulongbay/main.rs +++ b/examples/rust/src/bin/hulongbay/main.rs @@ -1,25 +1,22 @@ // #![allow(unused_imports)] -use std::env; -use std::error::Error; -use std::fmt::{Debug, Display, Formatter}; -use std::path::Path; - +#![allow(dead_code)] use itertools::Itertools; use raphtory::{ algorithms::{ - connected_components::weakly_connected_components, - motifs::three_node_temporal_motifs::{ - global_temporal_three_node_motif, global_temporal_three_node_motif_from_local, - temporal_three_node_motif, - }, - triangle_count::triangle_count, + connected_components::weakly_connected_components, triangle_count::triangle_count, }, graph_loader::source::csv_loader::CsvLoader, - prelude::{AdditionOps, EdgeListOps, Graph, GraphViewOps, Prop, TimeOps, VertexViewOps}, + prelude::*, }; use regex::Regex; use serde::Deserialize; -use std::time::Instant; +use std::{ + env, + error::Error, + fmt::{Debug, Display, Formatter}, + path::Path, + time::Instant, +}; #[derive(Deserialize, Debug)] pub struct Edge { @@ -67,14 +64,14 @@ pub fn loader(data_dir: &Path) -> Result> { println!( "Loaded graph from path {} with {} vertices, {} edges, took {} seconds", encoded_data_dir.display(), - g.num_vertices(), - g.num_edges(), + g.count_vertices(), + g.count_edges(), now.elapsed().as_secs() ); Ok(g) } else { - let g = Graph::new(16); + let g = Graph::new(); let now = Instant::now(); @@ -89,17 +86,17 @@ pub fn loader(data_dir: &Path) -> Result> { time, src, dst, - &vec![("amount".to_owned(), Prop::U64(sent.amount_usd))], + [("amount".to_owned(), Prop::U64(sent.amount_usd))], None, ) - .unwrap() + .unwrap(); })?; println!( "Loaded graph from CSV data files {} with {} vertices, {} edges which took {} seconds", encoded_data_dir.display(), - g.num_vertices(), - g.num_edges(), + g.count_vertices(), + g.count_edges(), now.elapsed().as_secs() ); @@ -113,10 +110,6 @@ fn try_main() -> Result<(), Box> { let data_dir = Path::new(args.get(1).ok_or(MissingArgumentError)?); let graph = loader(data_dir)?; - - let min_time = graph.start().ok_or(GraphEmptyError)?; - let max_time = graph.end().ok_or(GraphEmptyError)?; - let mid_time = (min_time + max_time) / 2; let now = Instant::now(); let motifs = global_temporal_three_node_motif( @@ -181,6 +174,13 @@ fn try_main() -> Result<(), Box> { // now.elapsed().as_secs() // ); + let now = Instant::now(); + let num_windowed_edges2 = window.count_edges(); + println!( + "Window num_edges returned {} in {} seconds", + num_windowed_edges2, + now.elapsed().as_secs() + ); // let now = Instant::now(); // let num_windowed_edges2 = window.num_edges(); // println!( @@ -210,39 +210,18 @@ fn try_main_bm() -> Result<(), Box> { println!("graph time range: {}-{}", earliest_time, latest_time); let now = Instant::now(); - let num_edges2 = graph.num_edges(); + let num_edges2 = graph.count_edges(); println!( "num_edges returned {} in {} milliseconds", num_edges2, now.elapsed().as_millis() ); - println!("\n Immutable graph metrics:"); - - let graph = graph.freeze(); - let now = Instant::now(); - let num_edges: usize = graph - .vertices() - .map(|v| graph.degree(v, Direction::OUT)) - .sum(); - + let num_exploded_edges = graph.edges().explode().count(); println!( - "Counting edges by summing degrees returned {} in {} milliseconds", - num_edges, - now.elapsed().as_millis() - ); - - let earliest_time = graph.earliest_time().ok_or(GraphEmptyError)?; - let latest_time = graph.latest_time().ok_or(GraphEmptyError)?; - - println!("graph time range: {}-{}", earliest_time, latest_time); - - let now = Instant::now(); - let num_edges2 = graph.num_edges(); - println!( - "num_edges returned {} in {} milliseconds", - num_edges2, + "counted {} exploded edges in {} milliseconds", + num_exploded_edges, now.elapsed().as_millis() ); diff --git a/examples/rust/src/bin/lotr/main.rs b/examples/rust/src/bin/lotr/main.rs index ef9cfd8c4f..f89412cf16 100644 --- a/examples/rust/src/bin/lotr/main.rs +++ b/examples/rust/src/bin/lotr/main.rs @@ -1,13 +1,14 @@ use itertools::Itertools; -use raphtory::algorithms::generic_taint::generic_taint; -use raphtory::core::utils; -use raphtory::core::Prop; -use raphtory::db::graph::Graph; -use raphtory::db::view_api::*; -use raphtory_io::graph_loader::source::csv_loader::CsvLoader; +use raphtory::{ + algorithms::temporal_reachability::temporally_reachable_nodes, core::utils::hashing, + graph_loader::source::csv_loader::CsvLoader, prelude::*, +}; use serde::Deserialize; -use std::path::PathBuf; -use std::{env, path::Path, time::Instant}; +use std::{ + env, + path::{Path, PathBuf}, + time::Instant, +}; #[derive(Deserialize, std::fmt::Debug)] pub struct Lotr { @@ -43,14 +44,14 @@ fn main() { println!( "Loaded graph from encoded data files {} with {} vertices, {} edges which took {} seconds", encoded_data_dir.to_str().unwrap(), - g.num_vertices(), - g.num_edges(), + g.count_vertices(), + g.count_edges(), now.elapsed().as_secs() ); g } else { - let g = Graph::new(2); + let g = Graph::new(); let now = Instant::now(); CsvLoader::new(data_dir) @@ -58,14 +59,14 @@ fn main() { g.add_vertex( lotr.time, lotr.src_id.clone(), - &vec![("type".to_string(), Prop::Str("Character".to_string()))], + [("type", Prop::str("Character"))], ) .expect("Failed to add vertex"); g.add_vertex( lotr.time, lotr.dst_id.clone(), - &vec![("type".to_string(), Prop::Str("Character".to_string()))], + [("type", Prop::str("Character"))], ) .expect("Failed to add vertex"); @@ -73,10 +74,7 @@ fn main() { lotr.time, lotr.src_id.clone(), lotr.dst_id.clone(), - &vec![( - "type".to_string(), - Prop::Str("Character Co-occurrence".to_string()), - )], + [("type", Prop::str("Character Co-occurrence"))], None, ) .expect("Failed to add edge"); @@ -86,8 +84,8 @@ fn main() { println!( "Loaded graph from CSV data files {} with {} vertices, {} edges which took {} seconds", encoded_data_dir.to_str().unwrap(), - g.num_vertices(), - g.num_edges(), + g.count_vertices(), + g.count_edges(), now.elapsed().as_secs() ); @@ -97,18 +95,18 @@ fn main() { g }; - assert_eq!(graph.num_vertices(), 139); - assert_eq!(graph.num_edges(), 701); + assert_eq!(graph.count_vertices(), 139); + assert_eq!(graph.count_edges(), 701); - let gandalf = utils::calculate_hash(&"Gandalf"); + let gandalf = hashing::calculate_hash(&"Gandalf"); assert_eq!(gandalf, 2760374808085341115); assert!(graph.has_vertex(gandalf)); assert_eq!(graph.vertex(gandalf).unwrap().name(), "Gandalf"); - let r = generic_taint(&graph, None, 20, 31930, vec!["Gandalf"], vec![]); + let r = temporally_reachable_nodes(&graph, None, 20, 31930, vec!["Gandalf"], None); assert_eq!( - r.keys().sorted().collect_vec(), + r.result.keys().sorted().collect_vec(), vec!["Gandalf", "Saruman", "Wormtongue"] ) } diff --git a/examples/rust/src/bin/pokec/main.rs b/examples/rust/src/bin/pokec/main.rs index c42adc0656..1d3cd55023 100644 --- a/examples/rust/src/bin/pokec/main.rs +++ b/examples/rust/src/bin/pokec/main.rs @@ -1,12 +1,16 @@ -use std::{time::Instant, env, path::Path}; - use raphtory::{ - algorithms::pagerank::unweighted_page_rank, - db::{graph::Graph, view_api::GraphViewOps}, + algorithms::{ + connected_components::weakly_connected_components, pagerank::unweighted_page_rank, + }, + db::{ + api::{mutation::AdditionOps, view::GraphViewOps}, + graph::graph::Graph, + }, + graph_loader::source::csv_loader::CsvLoader, + prelude::NO_PROPS, }; -use raphtory_io::graph_loader::source::csv_loader::CsvLoader; use serde::Deserialize; -use raphtory::algorithms::connected_components::weakly_connected_components; +use std::{env, path::Path, time::Instant}; #[derive(Deserialize, std::fmt::Debug)] struct Edge { @@ -15,52 +19,49 @@ struct Edge { } fn main() { - let shards = 2; - let now = Instant::now(); let args: Vec = env::args().collect(); - //let data_dir = Path::new(args.get(1).expect("No data directory provided")); - let data_dir = Path::new("/tmp/soc-pokec-relationships.txt"); + let data_dir = Path::new(args.get(1).expect("No data directory provided")); let g = if std::path::Path::new("/tmp/pokec").exists() { Graph::load_from_file("/tmp/pokec").unwrap() - } - else{ - let g = Graph::new(shards); + } else { + let g = Graph::new(); CsvLoader::new(data_dir) .set_delimiter("\t") .set_header(false) .load_into_graph(&g, |e: Edge, g| { - g.add_edge(0, e.src, e.dst, &vec![], None) + g.add_edge(0, e.src, e.dst, NO_PROPS, None) .expect("Failed to add edge"); }) .expect("Failed to load graph from encoded data files"); - g.save_to_file("/tmp/pokec"); + g.save_to_file("/tmp/pokec") + .expect("Failed to save graph to file"); g }; - println!( "Loaded graph from encoded data files {} with {} vertices, {} edges which took {} seconds", - "/tmp/soc-pokec-relationships.txt", - g.num_vertices(), - g.num_edges(), + data_dir.to_str().unwrap(), + g.count_vertices(), + g.count_edges(), now.elapsed().as_secs() ); - let frozen = g.freeze(); - let now = Instant::now(); - unweighted_page_rank(&frozen, 100, None , Some(0.00000001), true); + unweighted_page_rank(&g, 100, None, Some(0.00000001), true); println!("PageRank took {} millis", now.elapsed().as_millis()); let now = Instant::now(); - weakly_connected_components(&frozen, 100, None); + weakly_connected_components(&g, 100, None); - println!("Connected Components took {} millis", now.elapsed().as_millis()); + println!( + "Connected Components took {} millis", + now.elapsed().as_millis() + ); } diff --git a/js-raphtory/Cargo.toml b/js-raphtory/Cargo.toml index 62041cb5af..5d2b6f8aed 100644 --- a/js-raphtory/Cargo.toml +++ b/js-raphtory/Cargo.toml @@ -20,6 +20,8 @@ default = ["console_error_panic_hook"] [dependencies] wasm-bindgen = "0.2.63" +serde = { version = "1.0", features = ["derive"] } +serde-wasm-bindgen = "0.5.0" js-sys = "0.3" chrono = "0.4" raphtory = { path = "../raphtory" } diff --git a/js-raphtory/src/graph/edge.rs b/js-raphtory/src/graph/edge.rs index bd2c22731f..d368d7456c 100644 --- a/js-raphtory/src/graph/edge.rs +++ b/js-raphtory/src/graph/edge.rs @@ -1,13 +1,27 @@ -use raphtory::db::edge::EdgeView; -use raphtory::db::view_api::*; -use wasm_bindgen::prelude::*; - use super::Graph; -use crate::graph::{misc::JsProp, vertex::Vertex}; +use crate::graph::{misc::JsProp, vertex::Vertex, UnderGraph}; +use raphtory::db::{ + api::view::*, + graph::{edge::EdgeView, graph::Graph as TGraph}, +}; +use std::sync::Arc; +use wasm_bindgen::prelude::*; #[wasm_bindgen] pub struct Edge(pub(crate) EdgeView); +impl From> for Edge { + fn from(value: EdgeView) -> Self { + let graph = value.graph; + let eref = value.edge; + let js_graph = Graph(UnderGraph::TGraph(Arc::new(graph))); + Edge(EdgeView { + graph: js_graph, + edge: eref, + }) + } +} + #[wasm_bindgen] impl Edge { #[wasm_bindgen(js_name = source)] @@ -22,16 +36,17 @@ impl Edge { #[wasm_bindgen(js_name = properties)] pub fn properties(&self) -> js_sys::Map { + let t_props = self.0.properties(); let obj = js_sys::Map::new(); - for (k, v) in self.0.properties(true) { - obj.set(&k.into(), &JsProp(v).into()); + for (k, v) in t_props.iter() { + obj.set(&k.to_string().into(), &JsProp(v).into()); } obj } #[wasm_bindgen(js_name = getProperty)] pub fn get_property(&self, name: String) -> JsValue { - if let Some(prop) = self.0.property(name, true).map(JsProp) { + if let Some(prop) = self.0.properties().get(&name).map(JsProp) { prop.into() } else { JsValue::NULL diff --git a/js-raphtory/src/graph/graph_view_impl.rs b/js-raphtory/src/graph/graph_view_impl.rs index 42cbfe92e8..ca6e794334 100644 --- a/js-raphtory/src/graph/graph_view_impl.rs +++ b/js-raphtory/src/graph/graph_view_impl.rs @@ -1,14 +1,15 @@ -use raphtory::db::view_api::internal::{GraphViewInternalOps, WrappedGraph}; - use super::{Graph, UnderGraph}; +use raphtory::db::api::view::internal::{Base, BoxableGraphView, InheritViewOps}; -impl WrappedGraph for Graph { - type Internal = dyn GraphViewInternalOps + Send + Sync + 'static; +impl Base for Graph { + type Base = dyn BoxableGraphView + Send + Sync + 'static; - fn as_graph(&self) -> &(dyn GraphViewInternalOps + Send + Sync + 'static) { + fn base(&self) -> &(dyn BoxableGraphView + Send + Sync + 'static) { match &self.0 { UnderGraph::TGraph(g) => g.as_ref(), UnderGraph::WindowedGraph(g) => g.as_ref(), } } } + +impl InheritViewOps for Graph {} diff --git a/js-raphtory/src/graph/misc.rs b/js-raphtory/src/graph/misc.rs index 5290467cc3..29a57f2e33 100644 --- a/js-raphtory/src/graph/misc.rs +++ b/js-raphtory/src/graph/misc.rs @@ -1,10 +1,9 @@ -use std::ops::Deref; -use std::sync::Arc; - use crate::graph::{Graph, UnderGraph}; use chrono::{Datelike, Timelike}; use js_sys::Array; -use raphtory::core::{tgraph_shard::errors::GraphError, Prop}; +use raphtory::core::{utils::errors::GraphError, Prop}; +use serde::{Deserialize, Serialize}; +use std::{ops::Deref, sync::Arc}; use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; #[wasm_bindgen] @@ -13,13 +12,16 @@ pub struct JSError(pub(crate) GraphError); pub(crate) struct JsObjectEntry(pub(crate) JsValue); +#[derive(Serialize, Deserialize)] #[repr(transparent)] pub(crate) struct JsProp(pub(crate) Prop); -impl Into for JsProp { - fn into(self) -> JsValue { - match self.0 { - raphtory::core::Prop::Str(v) => v.into(), +impl From for JsValue { + fn from(value: JsProp) -> JsValue { + match value.0 { + raphtory::core::Prop::U8(v) => v.into(), + raphtory::core::Prop::U16(v) => v.into(), + raphtory::core::Prop::Str(v) => v.to_string().into(), raphtory::core::Prop::I32(v) => v.into(), raphtory::core::Prop::I64(v) => v.into(), raphtory::core::Prop::U32(v) => v.into(), @@ -40,6 +42,14 @@ impl Into for JsProp { .into() } Prop::Graph(v) => Graph(UnderGraph::TGraph(Arc::new(v))).into(), + Prop::List(v) => { + let v: Array = v.iter().map(|v| JsValue::from(JsProp(v.clone()))).collect(); + v.into() + } + Prop::Map(v) => { + let v = v.deref().clone(); + serde_wasm_bindgen::to_value(&v).unwrap() + } } } } @@ -58,6 +68,6 @@ impl From for Option<(String, Prop)> { let key = arr.at(0).as_string().unwrap(); let value = arr.at(1).as_string().unwrap(); - Some((key, Prop::Str(value))) + Some((key, Prop::str(value))) } } diff --git a/js-raphtory/src/graph/mod.rs b/js-raphtory/src/graph/mod.rs index e49ccdf13e..65bec2841c 100644 --- a/js-raphtory/src/graph/mod.rs +++ b/js-raphtory/src/graph/mod.rs @@ -1,27 +1,29 @@ +#![allow(dead_code)] #[cfg(feature = "console_error_panic_hook")] extern crate console_error_panic_hook; use core::panic; -use std::convert::TryFrom; -use std::sync::Arc; - use js_sys::Object; -use raphtory::core::tgraph_shard::errors::GraphError; -use raphtory::core::Prop; -use raphtory::db::graph::Graph as TGraph; -use raphtory::db::graph_window::WindowedGraph; -use raphtory::db::view_api::internal::GraphViewInternalOps; -use raphtory::db::view_api::GraphViewOps; -use raphtory::db::view_api::TimeOps; -use wasm_bindgen::prelude::*; -use wasm_bindgen::JsCast; - -use crate::graph::misc::JSError; -use crate::graph::misc::JsObjectEntry; -use crate::graph::vertex::JsVertex; -use crate::graph::vertex::Vertex; -use crate::log; -use crate::utils::set_panic_hook; +use raphtory::{ + core::utils::errors::GraphError, + db::{ + api::view::{internal::BoxableGraphView, GraphViewOps, TimeOps}, + graph::{graph::Graph as TGraph, views::window_graph::WindowedGraph}, + }, + prelude::*, +}; +use std::{convert::TryFrom, sync::Arc}; +use wasm_bindgen::{prelude::*, JsCast}; + +use crate::{ + graph::{ + edge::Edge, + misc::{JSError, JsObjectEntry}, + vertex::{JsVertex, Vertex}, + }, + log, + utils::set_panic_hook, +}; mod edge; mod graph_view_impl; @@ -48,7 +50,7 @@ impl UnderGraph { } // a bit heavy but might work - pub fn graph(&self) -> Box> { + pub fn graph(&self) -> Box> { match self { UnderGraph::TGraph(g) => Box::new(g.clone()), UnderGraph::WindowedGraph(g) => Box::new(g.clone()), @@ -61,7 +63,7 @@ impl Graph { #[wasm_bindgen(constructor)] pub fn new() -> Self { set_panic_hook(); - Graph(UnderGraph::TGraph(Arc::new(TGraph::new(1)))) + Graph(UnderGraph::TGraph(Arc::new(TGraph::new()))) } #[wasm_bindgen(js_name = window)] @@ -95,9 +97,9 @@ impl Graph { } #[wasm_bindgen(js_name = addVertex)] - pub fn add_vertex_js(&self, t: i64, id: JsValue, js_props: Object) -> Result<(), JSError> { + pub fn add_vertex_js(&self, t: i64, id: JsValue, js_props: Object) -> Result { let rust_props = if js_props.is_string() { - vec![("name".to_string(), Prop::Str(js_props.as_string().unwrap()))] + vec![("name".to_string(), Prop::str(js_props.as_string().unwrap()))] } else if js_props.is_object() { Object::entries(&js_props) .iter() @@ -113,11 +115,13 @@ impl Graph { match JsVertex::try_from(id)? { JsVertex::Str(vertex) => self .mutable_graph() - .add_vertex(t, vertex, &rust_props) + .add_vertex(t, vertex, rust_props) + .map(|v| v.into()) .map_err(JSError), JsVertex::Number(vertex) => self .mutable_graph() - .add_vertex(t, vertex, &rust_props) + .add_vertex(t, vertex, rust_props) + .map(|v| v.into()) .map_err(JSError), } } @@ -129,7 +133,7 @@ impl Graph { src: JsValue, dst: JsValue, js_props: Object, - ) -> Result<(), JSError> { + ) -> Result { js_props.dyn_ref::().map(|bigint| { log(&format!("bigint: {:?}", bigint)); }); @@ -151,12 +155,14 @@ impl Graph { match (JsVertex::try_from(src)?, JsVertex::try_from(dst)?) { (JsVertex::Str(src), JsVertex::Str(dst)) => self .mutable_graph() - .add_edge(t, src, dst, &props, None) - .map_err(JSError), + .add_edge(t, src, dst, props, None) + .map_err(JSError) + .map(|e| e.into()), (JsVertex::Number(src), JsVertex::Number(dst)) => self .mutable_graph() - .add_edge(t, src, dst, &props, None) - .map_err(JSError), + .add_edge(t, src, dst, props, None) + .map_err(JSError) + .map(|e| e.into()), _ => Err(JSError(GraphError::VertexIdNotStringOrNumber)), } } diff --git a/js-raphtory/src/graph/vertex.rs b/js-raphtory/src/graph/vertex.rs index a6881db01f..932d13cde6 100644 --- a/js-raphtory/src/graph/vertex.rs +++ b/js-raphtory/src/graph/vertex.rs @@ -1,18 +1,27 @@ -use std::convert::TryFrom; - +use super::{misc::JSError, Graph}; +use crate::graph::{edge::Edge, misc::JsProp, UnderGraph}; use raphtory::{ - core::tgraph_shard::errors::GraphError, - db::{vertex::VertexView, view_api::VertexViewOps}, + core::utils::errors::GraphError, + db::{ + api::view::VertexViewOps, + graph::{graph::Graph as TGraph, vertex::VertexView}, + }, }; +use std::{convert::TryFrom, sync::Arc}; use wasm_bindgen::prelude::*; -use crate::graph::{edge::Edge, misc::JsProp}; - -use super::{misc::JSError, Graph}; - #[wasm_bindgen] pub struct Vertex(pub(crate) VertexView); +impl From> for Vertex { + fn from(value: VertexView) -> Self { + let vid = value.vertex; + let graph = value.graph; + let js_graph = Graph(UnderGraph::TGraph(Arc::new(graph))); + Vertex(VertexView::new_internal(js_graph, vid)) + } +} + #[wasm_bindgen] impl Vertex { #[wasm_bindgen(js_name = id)] @@ -90,19 +99,19 @@ impl Vertex { #[wasm_bindgen(js_name = properties)] pub fn properties(&self) -> js_sys::Map { let obj = js_sys::Map::new(); - for (k, v) in self.0.properties(true) { - obj.set(&k.into(), &JsProp(v).into()); + for (k, v) in self.0.properties() { + obj.set(&k.to_string().into(), &JsProp(v).into()); } obj } #[wasm_bindgen(js_name = getProperty)] pub fn get_property(&self, name: String) -> JsValue { - if let Some(prop) = self.0.property(name, true).map(JsProp) { - prop.into() - } else { - JsValue::NULL - } + self.0 + .properties() + .get(&name) + .map(|v| JsProp(v).into()) + .unwrap_or(JsValue::NULL) } } diff --git a/paper/joss-raphtory.bib b/paper/joss-raphtory.bib index 152299de89..ac98224a00 100644 --- a/paper/joss-raphtory.bib +++ b/paper/joss-raphtory.bib @@ -6,7 +6,8 @@ @article{gauvin2022randomized number={4}, pages={763--830}, year={2022}, - publisher={SIAM} + publisher={SIAM}, + doi = {10.1137/19M1242252} } @article{bovet2021centralities, @@ -21,7 +22,8 @@ @inproceedings{tang2010analysing author={Tang, John and Musolesi, Mirco and Mascolo, Cecilia and Latora, Vito and Nicosia, Vincenzo}, booktitle={Proceedings of the 3rd Workshop on Social Network Systems}, pages={1--6}, - year={2010} + year={2010}, + doi={10.1145/1852658.1852661} } @article{goh2008burstiness, @@ -32,7 +34,8 @@ @article{goh2008burstiness number={4}, pages={48002}, year={2008}, - publisher={IOP Publishing} + publisher={IOP Publishing}, + doi = {10.1209/0295-5075/81/48002} } @article{pfitzner2013betweenness, title={Betweenness preference: Quantifying correlations in the topological dynamics of temporal networks}, @@ -42,7 +45,8 @@ @article{pfitzner2013betweenness number={19}, pages={198701}, year={2013}, - publisher={APS} + publisher={APS}, + doi = {10.1103/physrevlett.110.198701} } @article{steer2020raphtory, @@ -52,7 +56,8 @@ @article{steer2020raphtory volume={102}, pages={453--464}, year={2020}, - publisher={Elsevier} + publisher={Elsevier}, + doi = {10.1016/j.future.2019.08.022}, } @article{yousaf2023non, @@ -69,7 +74,8 @@ @article{badie2023reticula volume={21}, pages={101301}, year={2023}, - publisher={Elsevier} + publisher={Elsevier}, + doi = {10.1016/j.softx.2022.101301}, } @inproceedings{hackl2021analysis, @@ -77,7 +83,8 @@ @inproceedings{hackl2021analysis author={Hackl, J{\"u}rgen and Scholtes, Ingo and Petrovi{\'c}, Luka V and Perri, Vincenzo and Verginer, Luca and Gote, Christoph}, booktitle={Companion Proceedings of the Web Conference 2021}, pages={530--532}, - year={2021} + year={2021}, + doi = {10.1145/3442442.3452052} } @article{csardi2006igraph, @@ -87,7 +94,8 @@ @article{csardi2006igraph volume={1695}, number={5}, pages={1--9}, - year={2006} + year={2006}, + doi = {10.5281/zenodo.7682609} } @article{zhang2015dynamic, @@ -102,6 +110,7 @@ @article{peixoto2014graph author={Peixoto, Tiago P}, journal={figshare}, year={2014} + doi = {10.6084/m9.figshare.1164194.v14} } @techreport{hagberg2008exploring, @@ -119,7 +128,8 @@ @article{starnini2012random number={5}, pages={056115}, year={2012}, - publisher={APS} + publisher={APS}, + doi = {10.1103/PhysRevE.85.056115} } @article{delvenne2010stability, @@ -130,7 +140,8 @@ @article{delvenne2010stability number={29}, pages={12755--12760}, year={2010}, - publisher={National Acad Sciences} + publisher={National Acad Sciences}, + doi = {10.1073/pnas.0903215107} } @@ -138,14 +149,16 @@ @book{langville2006google title={Google's PageRank and beyond: The science of search engine rankings}, author={Langville, Amy N and Meyer, Carl D}, year={2006}, - publisher={Princeton university press} + publisher={Princeton university press}, + doi = {10.1063/1.2711640}, } @inproceedings{qiu2018network, title={Network embedding as matrix factorization: Unifying deepwalk, line, pte, and node2vec}, author={Qiu, Jiezhong and Dong, Yuxiao and Ma, Hao and Li, Jian and Wang, Kuansan and Tang, Jie}, booktitle={Proceedings of the eleventh ACM international conference on web search and data mining}, pages={459--467}, - year={2018} + year={2018}, + doi = {10.1145/3159652.3159706} } @article{karsai2012universal, @@ -156,7 +169,8 @@ @article{karsai2012universal number={1}, pages={397}, year={2012}, - publisher={Nature Publishing Group UK London} + publisher={Nature Publishing Group UK London}, + doi = {10.1038/srep00397}, } @article{donnat2018tracking, @@ -167,7 +181,8 @@ @article{donnat2018tracking number={2}, pages={971--1012}, year={2018}, - publisher={JSTOR} + publisher={JSTOR}, + doi = {10.1214/18-aoas1176} } @article{malmgren2008poissonian, title={A Poissonian explanation for heavy tails in e-mail communication}, @@ -177,7 +192,8 @@ @article{malmgren2008poissonian number={47}, pages={18153--18158}, year={2008}, - publisher={National Acad Sciences} + publisher={National Acad Sciences}, + doi = {10.1073/pnas.0800332105} } @article{lambiotte2019networks, @@ -188,14 +204,16 @@ @article{lambiotte2019networks number={4}, pages={313--320}, year={2019}, - publisher={Nature Publishing Group UK London} + publisher={Nature Publishing Group UK London}, + doi = {10.1038/s41567-019-0459-y} } @book{newman2018networks, title={Networks}, author={Newman, Mark}, year={2018}, - publisher={Oxford University Press} + publisher={Oxford University Press}, + doi = {10.1093/oso/9780198805090.001.0001} } @article{holme2012temporal, @@ -205,14 +223,16 @@ @article{holme2012temporal volume={519}, number={3}, pages={97--125}, - year={2012} + year={2012}, + doi = {10.1007/978-3-642-36461-7} } @book{masuda2016guide, title={A guide to temporal networks}, author={Masuda, Naoki and Lambiotte, Renaud}, year={2016}, - publisher={World Scientific} + publisher={World Scientific}, + doi = {10.1142/q0033} } @article{arnold2021moving, @@ -220,7 +240,8 @@ @article{arnold2021moving author={Arnold, Naomi A and Steer, Benjamin and Hafnaoui, Imane and Parada G, Hugo A and Mondrag{\'o}n, Raul J and Cuadrado, F{\'e}lix and Clegg, Richard G}, journal={Proceedings of the ACM on Human-Computer Interaction}, number={CSCW}, - year={2021} + year={2021}, + doi = {10.1145/3479591} } @inproceedings{paranjape2017motifs, @@ -228,14 +249,16 @@ @inproceedings{paranjape2017motifs author={Paranjape, Ashwin and Benson, Austin R and Leskovec, Jure}, booktitle={Proceedings of the tenth ACM international conference on web search and data mining}, pages={601--610}, - year={2017} + year={2017}, + doi = {10.1145/3018661.3018731} } @inproceedings{Chronograph, author = {Erb, Benjamin and Mei\ss{}ner, Dominik and Pietron, Jakob and Kargl, Frank}, title = {Chronograph: A Distributed Processing Platform for Online and Batch Computations on Event-Sourced Graphs}, year = {2017}, - booktitle = {Proceedings of the 11th ACM International Conference on Distributed and Event-Based Systems} + booktitle = {Proceedings of the 11th ACM International Conference on Distributed and Event-Based Systems}, + doi = {10.1145/3093742.3093913} } @misc{RecallGraph, @@ -268,5 +291,6 @@ @article{lucas2023inferring volume={3}, number={2}, year={2023}, - publisher={Elsevier} + publisher={Elsevier}, + doi = {10.1101/2021.03.26.437187} } \ No newline at end of file diff --git a/py-raphtory/.gitignore b/py-raphtory/.gitignore deleted file mode 100644 index af3ca5ef1c..0000000000 --- a/py-raphtory/.gitignore +++ /dev/null @@ -1,72 +0,0 @@ -/target - -# Byte-compiled / optimized / DLL files -__pycache__/ -.pytest_cache/ -*.py[cod] - -# C extensions -*.so - -# Distribution / packaging -.Python -.venv/ -env/ -bin/ -build/ -develop-eggs/ -dist/ -eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -include/ -man/ -venv/ -*.egg-info/ -.installed.cfg -*.egg - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt -pip-selfcheck.json - -# Unit test / coverage reports -htmlcov/ -.tox/ -.coverage -.cache -nosetests.xml -coverage.xml - -# Translations -*.mo - -# Mr Developer -.mr.developer.cfg -.project -.pydevproject - -# Rope -.ropeproject - -# Django stuff: -*.log -*.pot - -.DS_Store - -# Sphinx documentation -docs/_build/ - -# PyCharm -.idea/ - -# VSCode -.vscode/ - -# Pyenv -.python-version \ No newline at end of file diff --git a/py-raphtory/Cargo.toml b/py-raphtory/Cargo.toml deleted file mode 100644 index e418cc1d40..0000000000 --- a/py-raphtory/Cargo.toml +++ /dev/null @@ -1,36 +0,0 @@ -[package] -name = "py-raphtory" -description = "Raphtory python bindings allowing custom rust algorithms compatible with the python client" -version.workspace = true -edition.workspace = true -rust-version.workspace = true -keywords.workspace = true -authors.workspace = true -documentation.workspace = true -repository.workspace = true -license.workspace = true -readme.workspace = true -homepage.workspace = true -doc = false -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -pyo3 = {version="0.18.1", features=["multiple-pymethods", "chrono"]} -raphtory = {path = "../raphtory", version = "0.4.0" } -raphtory-io = {path = "../raphtory-io", version = "0.4.0" } -rustc-hash = "1.1.0" -parking_lot = { version = "0.12" , features = ["serde"] } -flume = "0.10" -futures = {version = "0.3", features = ["thread-pool"] } -replace_with = "0.1" -itertools="0.10" -csv = "1.1.6" -flate2 = "1.0" -regex = "1" -serde = { version = "1", features = ["derive", "rc"] } -rayon = "1" -chrono = "0.4" -bincode = "1" -display-error-chain = "0.1.1" -num = "0.4.0" -tokio = { version = "1.27.0", features = ["full"] } diff --git a/py-raphtory/src/algorithms.rs b/py-raphtory/src/algorithms.rs deleted file mode 100644 index cff6db0c62..0000000000 --- a/py-raphtory/src/algorithms.rs +++ /dev/null @@ -1,241 +0,0 @@ -/// Implementations of various graph algorithms that can be run on a graph. -/// -/// To run an algorithm simply import the module and call the function with the graph as the argument -/// -use crate::graph_view::PyGraphView; -use std::collections::HashMap; - -use crate::utils; -use crate::utils::{extract_input_vertex, InputVertexBox}; -use pyo3::prelude::*; -use raphtory::algorithms::connected_components; -use raphtory::algorithms::degree::{ - average_degree as average_degree_rs, max_in_degree as max_in_degree_rs, - max_out_degree as max_out_degree_rs, min_in_degree as min_in_degree_rs, - min_out_degree as min_out_degree_rs, -}; -use raphtory::algorithms::directed_graph_density::directed_graph_density as directed_graph_density_rs; -use raphtory::algorithms::generic_taint::generic_taint as generic_taint_rs; -use raphtory::algorithms::local_clustering_coefficient::local_clustering_coefficient as local_clustering_coefficient_rs; -use raphtory::algorithms::local_triangle_count::local_triangle_count as local_triangle_count_rs; -use raphtory::algorithms::motifs::three_node_local::global_temporal_three_node_motif as global_temporal_three_node_motif_rs; -use raphtory::algorithms::motifs::three_node_local::global_temporal_three_node_motif_from_local as global_temporal_three_node_motif_from_local_rs; -use raphtory::algorithms::motifs::three_node_local::temporal_three_node_motif as temporal_three_node_motif_rs; -use raphtory::algorithms::pagerank::unweighted_page_rank; -use raphtory::algorithms::reciprocity::{ - all_local_reciprocity as all_local_reciprocity_rs, global_reciprocity as global_reciprocity_rs, -}; - -/// Local triangle count - calculates the number of triangles (a cycle of length 3) for a node. -/// It measures the local clustering of a graph. -/// -/// This is useful for understanding the level of connectivity and the likelihood of information -/// or influence spreading through a network. -/// -/// For example, in a social network, the local triangle count of a user's profile can reveal the -/// number of mutual friends they have and the level of interconnectivity between those friends. -/// A high local triangle count for a user indicates that they are part of a tightly-knit group -/// of people, which can be useful for targeted advertising or identifying key influencers -/// within a network. -/// -/// Local triangle count can also be used in other domains such as biology, where it can be used -/// to analyze protein interaction networks, or in transportation networks, where it can be used -/// to identify critical junctions or potential traffic bottlenecks. -/// -#[pyfunction] -pub fn local_triangle_count(g: &PyGraphView, v: &PyAny) -> PyResult> { - let v = utils::extract_vertex_ref(v)?; - Ok(local_triangle_count_rs(&g.graph, v)) -} - -#[pyfunction] -pub fn weakly_connected_components( - g: &PyGraphView, - iter_count: usize, -) -> PyResult> { - Ok(connected_components::weakly_connected_components( - &g.graph, iter_count, None, - )) -} - -#[pyfunction] -pub fn pagerank( - g: &PyGraphView, - iter_count: usize, - max_diff: Option, -) -> PyResult> { - Ok(unweighted_page_rank(&g.graph, iter_count, None, max_diff, true)) -} - -#[pyfunction] -pub fn generic_taint( - g: &PyGraphView, - iter_count: usize, - start_time: i64, - infected_nodes: Vec<&PyAny>, - stop_nodes: Vec<&PyAny>, -) -> Result>, PyErr> { - let infected_nodes: PyResult> = infected_nodes - .into_iter() - .map(|v| extract_input_vertex(v)) - .collect(); - let stop_nodes: PyResult> = stop_nodes - .into_iter() - .map(|v| extract_input_vertex(v)) - .collect(); - - Ok(generic_taint_rs( - &g.graph, - None, - iter_count, - start_time, - infected_nodes?, - stop_nodes?, - )) -} - -/// Local Clustering coefficient - measures the degree to which nodes in a graph tend to cluster together. -/// -/// It is calculated by dividing the number of triangles (sets of three nodes that are all -/// connected to each other) in the graph by the total number of possible triangles. -/// The resulting value is a number between 0 and 1 that represents the density of -/// clustering in the graph. -/// -/// A high clustering coefficient indicates that nodes tend to be -/// connected to nodes that are themselves connected to each other, while a low clustering -/// coefficient indicates that nodes tend to be connected to nodes that are not connected -/// to each other. -/// -/// In a social network of a particular community, we can compute the clustering -/// coefficient of each node to get an idea of how strongly connected and cohesive -/// that node's neighborhood is. -/// -/// A high clustering coefficient for a node in a social network indicates that the -/// node's neighbors tend to be strongly connected with each other, forming a tightly-knit -/// group or community. In contrast, a low clustering coefficient for a node indicates that -/// its neighbors are relatively less connected with each other, suggesting a more fragmented -/// or diverse community. -#[pyfunction] -pub fn local_clustering_coefficient(g: &PyGraphView, v: &PyAny) -> PyResult> { - let v = utils::extract_vertex_ref(v)?; - Ok(local_clustering_coefficient_rs(&g.graph, v)) -} - -/// Graph density - measures how dense or sparse a graph is. -/// -/// It is defined as the ratio of the number of edges in the graph to the total number of possible -/// edges. A dense graph has a high edge-to-vertex ratio, while a sparse graph has a low -/// edge-to-vertex ratio. -/// -/// For example in social network analysis, a dense graph may indicate a highly interconnected -/// community, while a sparse graph may indicate more isolated individuals. -#[pyfunction] -pub fn directed_graph_density(g: &PyGraphView) -> f32 { - directed_graph_density_rs(&g.graph) -} - -/// The average degree of all vertices in the graph. -#[pyfunction] -pub fn average_degree(g: &PyGraphView) -> f64 { - average_degree_rs(&g.graph) -} - -/// The maximum out degree of any vertex in the graph. -#[pyfunction] -pub fn max_out_degree(g: &PyGraphView) -> usize { - max_out_degree_rs(&g.graph) -} - -/// The maximum in degree of any vertex in the graph. -#[pyfunction] -pub fn max_in_degree(g: &PyGraphView) -> usize { - max_in_degree_rs(&g.graph) -} - -/// The minimum out degree of any vertex in the graph. -#[pyfunction] -pub fn min_out_degree(g: &PyGraphView) -> usize { - min_out_degree_rs(&g.graph) -} - -/// The minimum in degree of any vertex in the graph. -#[pyfunction] -pub fn min_in_degree(g: &PyGraphView) -> usize { - min_in_degree_rs(&g.graph) -} - -/// Reciprocity - measure of the symmetry of relationships in a graph, the global reciprocity of -/// the entire graph. -/// This calculates the number of reciprocal connections (edges that go in both directions) in a -/// graph and normalizes it by the total number of edges. -/// -/// In a social network context, reciprocity measures the likelihood that if person A is linked -/// to person B, then person B is linked to person A. This algorithm can be used to determine the -/// level of symmetry or balance in a social network. It can also reveal the power dynamics in a -/// group or community. For example, if one person has many connections that are not reciprocated, -/// it could indicate that this person has more power or influence in the network than others. -/// -/// In a business context, reciprocity can be used to study customer behavior. For instance, in a -/// transactional network, if a customer tends to make a purchase from a seller and then the seller -/// makes a purchase from the same customer, it can indicate a strong reciprocal relationship -/// between them. On the other hand, if the seller does not make a purchase from the same customer, -/// it could imply a less reciprocal or more one-sided relationship. -#[pyfunction] -pub fn global_reciprocity(g: &PyGraphView) -> f64 { - global_reciprocity_rs(&g.graph, None) -} - -/// Reciprocity - measure of the symmetry of relationships in a graph. -/// the reciprocity of every vertex in the graph as a tuple of vector id and the reciprocity -/// This calculates the number of reciprocal connections (edges that go in both directions) in a -/// graph and normalizes it by the total number of edges. -/// -/// In a social network context, reciprocity measures the likelihood that if person A is linked -/// to person B, then person B is linked to person A. This algorithm can be used to determine the -/// level of symmetry or balance in a social network. It can also reveal the power dynamics in a -/// group or community. For example, if one person has many connections that are not reciprocated, -/// it could indicate that this person has more power or influence in the network than others. -/// -/// In a business context, reciprocity can be used to study customer behavior. For instance, in a -/// transactional network, if a customer tends to make a purchase from a seller and then the seller -/// makes a purchase from the same customer, it can indicate a strong reciprocal relationship -/// between them. On the other hand, if the seller does not make a purchase from the same customer, -/// it could imply a less reciprocal or more one-sided relationship. -/// -#[pyfunction] -pub fn all_local_reciprocity(g: &PyGraphView) -> HashMap { - all_local_reciprocity_rs(&g.graph, None) -} - -/// Computes the number of both open and closed triplets within a graph -/// -/// An open triplet, is one where a node has two neighbors, but no edge between them. -/// A closed triplet is one where a node has two neighbors, and an edge between them. -#[pyfunction] -pub fn triplet_count(g: &PyGraphView) -> usize { - raphtory::algorithms::triplet_count::triplet_count(&g.graph, None) -} - -/// Computes the global clustering coefficient of a graph. The global clustering coefficient is -/// defined as the number of triangles in the graph divided by the number of triplets in the graph. -#[pyfunction] -pub fn global_clustering_coefficient(g: &PyGraphView) -> f64 { - raphtory::algorithms::clustering_coefficient::clustering_coefficient(&g.graph) -} - -#[pyfunction] -pub fn temporal_three_node_motif(g: &PyGraphView, delta: i64) -> HashMap> { - temporal_three_node_motif_rs(&g.graph, None, delta) -} - -#[pyfunction] -pub fn global_temporal_three_node_motif(g: &PyGraphView, delta: i64) -> Vec { - global_temporal_three_node_motif_rs(&g.graph, None, delta) -} - -#[pyfunction] -pub fn global_temporal_three_node_motif_from_local( - counts: HashMap>, -) -> Vec { - global_temporal_three_node_motif_from_local_rs(counts) -} diff --git a/py-raphtory/src/dynamic.rs b/py-raphtory/src/dynamic.rs deleted file mode 100644 index a3dc3945ff..0000000000 --- a/py-raphtory/src/dynamic.rs +++ /dev/null @@ -1,44 +0,0 @@ -use raphtory::db::graph::Graph; -use raphtory::db::graph_layer::LayeredGraph; -use raphtory::db::graph_window::WindowedGraph; -use raphtory::db::view_api::internal::{GraphViewInternalOps, WrappedGraph}; -use raphtory::db::view_api::GraphViewOps; -use std::sync::Arc; - -#[derive(Clone)] -pub struct DynamicGraph(Arc); - -pub trait IntoDynamic { - fn into_dynamic(self) -> DynamicGraph; -} - -impl IntoDynamic for Graph { - fn into_dynamic(self) -> DynamicGraph { - DynamicGraph(self.as_arc()) - } -} - -impl IntoDynamic for WindowedGraph { - fn into_dynamic(self) -> DynamicGraph { - DynamicGraph(Arc::new(self)) - } -} - -impl IntoDynamic for LayeredGraph { - fn into_dynamic(self) -> DynamicGraph { - DynamicGraph(Arc::new(self)) - } -} - -impl IntoDynamic for DynamicGraph { - fn into_dynamic(self) -> DynamicGraph { - self - } -} - -impl WrappedGraph for DynamicGraph { - type Internal = dyn GraphViewInternalOps + Send + Sync + 'static; - fn as_graph(&self) -> &(dyn GraphViewInternalOps + Send + Sync + 'static) { - &*self.0 - } -} diff --git a/py-raphtory/src/edge.rs b/py-raphtory/src/edge.rs deleted file mode 100644 index 3a7c466273..0000000000 --- a/py-raphtory/src/edge.rs +++ /dev/null @@ -1,549 +0,0 @@ -//! The edge module contains the PyEdge class, which is used to represent edges in the graph and -//! provides access to the edge's properties and vertices. -//! -//! The PyEdge class also provides access to the perspective APIs, which allow the user to view the -//! edge as it existed at a particular point in time, or as it existed over a particular time range. -//! -use crate::dynamic::{DynamicGraph, IntoDynamic}; -use crate::types::repr::{iterator_repr, Repr}; -use crate::utils::*; -use crate::vertex::{PyVertex, PyVertexIterable}; -use crate::wrappers::iterators::{OptionI64Iterable, OptionPropIterable}; -use crate::wrappers::prop::Prop; -use chrono::NaiveDateTime; -use itertools::Itertools; -use pyo3::prelude::*; -use pyo3::{pyclass, pymethods, PyAny, PyRef, PyRefMut, PyResult}; -use raphtory::db::edge::EdgeView; -use raphtory::db::view_api::*; -use std::collections::HashMap; -use std::sync::Arc; - -/// PyEdge is a Python class that represents an edge in the graph. -/// An edge is a directed connection between two vertices. -#[pyclass(name = "Edge")] -pub struct PyEdge { - pub(crate) edge: EdgeView, -} - -impl From> for PyEdge { - fn from(value: EdgeView) -> Self { - Self { - edge: EdgeView { - graph: value.graph.clone().into_dynamic(), - edge: value.edge, - }, - } - } -} - -impl IntoPyObject for EdgeView { - fn into_py_object(self) -> PyObject { - let py_version: PyEdge = self.into(); - Python::with_gil(|py| py_version.into_py(py)) - } -} - -/// PyEdge is a Python class that represents an edge in the graph. -/// An edge is a directed connection between two vertices. -#[pymethods] -impl PyEdge { - pub fn __getitem__(&self, name: String) -> Option { - self.property(name, Some(true)) - } - - /// Returns the value of the property with the given name. - /// If the property is not found, None is returned. - /// If the property is found, the value of the property is returned. - /// - /// Arguments: - /// name (str): The name of the property to retrieve. - /// - /// Returns: - /// The value of the property with the given name. - #[pyo3(signature = (name, include_static = true))] - pub fn property(&self, name: String, include_static: Option) -> Option { - let include_static = include_static.unwrap_or(true); - self.edge - .property(name, include_static) - .map(|prop| prop.into()) - } - - /// Returns the value of the property with the given name all times. - /// If the property is not found, None is returned. - /// If the property is found, the value of the property is returned. - /// - /// Arguments: - /// name (str): The name of the property to retrieve. - /// - /// Returns: - /// The value of the property with the given name. - #[pyo3(signature = (name))] - pub fn property_history(&self, name: String) -> Vec<(i64, Prop)> { - self.edge - .property_history(name) - .into_iter() - .map(|(k, v)| (k, v.into())) - .collect() - } - - /// Returns a list of timestamps of when an edge is added or change to an edge is made. - /// - /// Returns: - /// A list of timestamps. - /// - - pub fn history(&self) -> Vec { - self.edge.history() - } - - /// Returns a dictionary of all properties on the edge. - /// - /// Arguments: - /// include_static (bool): Whether to include static properties in the result. - /// - /// Returns: - /// A dictionary of all properties on the edge. - #[pyo3(signature = (include_static = true))] - pub fn properties(&self, include_static: Option) -> HashMap { - let include_static = include_static.unwrap_or(true); - self.edge - .properties(include_static) - .into_iter() - .map(|(k, v)| (k, v.into())) - .collect() - } - - /// Returns a dictionary of all properties on the edge at all times. - /// - /// Returns: - /// A dictionary of all properties on the edge at all times. - pub fn property_histories(&self) -> HashMap> { - self.edge - .property_histories() - .into_iter() - .map(|(k, v)| (k, v.into_iter().map(|(t, p)| (t, p.into())).collect())) - .collect() - } - - /// Returns a list of all property names on the edge. - /// - /// Arguments: - /// include_static (bool): Whether to include static properties in the result. - /// - /// Returns: - /// A list of all property names on the edge. - #[pyo3(signature = (include_static = true))] - pub fn property_names(&self, include_static: Option) -> Vec { - let include_static = include_static.unwrap_or(true); - self.edge.property_names(include_static) - } - - /// Check if a property exists with the given name. - /// - /// Arguments: - /// name (str): The name of the property to check. - /// include_static (bool): Whether to include static properties in the result. - /// - /// Returns: - /// True if a property exists with the given name, False otherwise. - #[pyo3(signature = (name, include_static = true))] - pub fn has_property(&self, name: String, include_static: Option) -> bool { - let include_static = include_static.unwrap_or(true); - self.edge.has_property(name, include_static) - } - - /// Check if a static property exists with the given name. - /// - /// Arguments: - /// name (str): The name of the property to check. - /// - /// Returns: - /// True if a static property exists with the given name, False otherwise. - pub fn has_static_property(&self, name: String) -> bool { - self.edge.has_static_property(name) - } - - pub fn static_property(&self, name: String) -> Option { - self.edge.static_property(name).map(|prop| prop.into()) - } - - /// Get the source vertex of the Edge. - /// - /// Returns: - /// The source vertex of the Edge. - fn src(&self) -> PyVertex { - self.edge.src().into() - } - - /// Get the destination vertex of the Edge. - /// - /// Returns: - /// The destination vertex of the Edge. - fn dst(&self) -> PyVertex { - self.edge.dst().into() - } - - //****** Perspective APIS ******// - - /// Get the start time of the Edge. - /// - /// Returns: - /// The start time of the Edge. - pub fn start(&self) -> Option { - self.edge.start() - } - - /// Get the start datetime of the Edge. - /// - /// Returns: - /// the start datetime of the Edge. - pub fn start_date_time(&self) -> Option { - let start_time = self.edge.start()?; - Some(NaiveDateTime::from_timestamp_millis(start_time).unwrap()) - } - - /// Get the end time of the Edge. - /// - /// Returns: - /// The end time of the Edge. - pub fn end(&self) -> Option { - self.edge.end() - } - - /// Get the end datetime of the Edge. - /// - /// Returns: - /// The end datetime of the Edge - pub fn end_date_time(&self) -> Option { - let end_time = self.edge.end()?; - Some(NaiveDateTime::from_timestamp_millis(end_time).unwrap()) - } - - /// Get the duration of the Edge. - /// - /// Arguments: - /// step (int): The step size to use when calculating the duration. - /// - /// Returns: - /// A set of windows containing edges that fall in the time period - #[pyo3(signature = (step))] - fn expanding(&self, step: &PyAny) -> PyResult { - expanding_impl(&self.edge, step) - } - - /// Get a set of Edge windows for a given window size, step, start time - /// and end time using rolling window. - /// A rolling window is a window that moves forward by `step` size at each iteration. - /// - /// Arguments: - /// window (int): The size of the window. - /// step (int): The step size to use when calculating the duration. - /// start (int): The start time to use when calculating the duration. - /// end (int): The end time to use when calculating the duration. - /// - /// Returns: - /// A set of windows containing edges that fall in the time period - fn rolling(&self, window: &PyAny, step: Option<&PyAny>) -> PyResult { - rolling_impl(&self.edge, window, step) - } - - /// Get a new Edge with the properties of this Edge within the specified time window. - /// - /// Arguments: - /// t_start (int): The start time of the window. - /// t_end (int): The end time of the window. - /// - /// Returns: - /// A new Edge with the properties of this Edge within the specified time window. - #[pyo3(signature = (t_start = None, t_end = None))] - pub fn window(&self, t_start: Option<&PyAny>, t_end: Option<&PyAny>) -> PyResult { - window_impl(&self.edge, t_start, t_end).map(|e| e.into()) - } - - /// Get a new Edge with the properties of this Edge at a specified time. - /// - /// Arguments: - /// end (int): The time to get the properties at. - /// - /// Returns: - /// A new Edge with the properties of this Edge at a specified time. - #[pyo3(signature = (end))] - pub fn at(&self, end: &PyAny) -> PyResult { - at_impl(&self.edge, end).map(|e| e.into()) - } - - /// Explodes an Edge into a list of PyEdges. This is useful when you want to iterate over - /// the properties of an Edge at every single point in time. This will return a seperate edge - /// each time a property had been changed. - /// - /// Returns: - /// A list of PyEdges - pub fn explode(&self) -> PyEdges { - let edge = self.edge.clone(); - (move || edge.explode()).into() - } - - /// Gets the earliest time of an edge. - /// - /// Returns: - /// (int) The earliest time of an edge - pub fn earliest_time(&self) -> Option { - self.edge.earliest_time() - } - - /// Gets of earliest datetime of an edge. - /// - /// Returns: - /// the earliest datetime of an edge - pub fn earliest_date_time(&self) -> Option { - Some(NaiveDateTime::from_timestamp_millis(self.edge.earliest_time()?).unwrap()) - } - - /// Gets the latest time of an edge. - /// - /// Returns: - /// (int) The latest time of an edge - pub fn latest_time(&self) -> Option { - self.edge.latest_time() - } - - /// Gets of latest datetime of an edge. - /// - /// Returns: - /// the latest datetime of an edge - pub fn latest_date_time(&self) -> Option { - let latest_time = self.edge.latest_time()?; - Some(NaiveDateTime::from_timestamp_millis(latest_time).unwrap()) - } - - /// Gets the time of an exploded edge. - /// - /// Returns: - /// (int) The time of an exploded edge - pub fn time(&self) -> Option { - self.edge.time() - } - - /// Gets the name of the layer this edge belongs to - /// - /// Returns: - /// (str) The name of the layer - pub fn layer_name(&self) -> String { - self.edge.layer_name() - } - - /// Gets the datetime of an exploded edge. - /// - /// Returns: - /// (datetime) the datetime of an exploded edge - pub fn date_time(&self) -> Option { - let date_time = self.edge.time()?; - Some(NaiveDateTime::from_timestamp_millis(date_time).unwrap()) - } - - /// Displays the Edge as a string. - pub fn __repr__(&self) -> String { - self.repr() - } -} - -impl Repr for PyEdge { - fn repr(&self) -> String { - let properties = &self - .properties(Some(true)) - .iter() - .map(|(k, v)| k.to_string() + " : " + &v.to_string()) - .join(", "); - - let source = self.edge.src().name(); - let target = self.edge.dst().name(); - let earliest_time = self.edge.earliest_time(); - let latest_time = self.edge.latest_time(); - if properties.is_empty() { - format!( - "Edge(source={}, target={}, earliest_time={}, latest_time={})", - source.trim_matches('"'), - target.trim_matches('"'), - earliest_time.unwrap_or(0), - latest_time.unwrap_or(0), - ) - } else { - let property_string: String = "{".to_string() + &properties + "}"; - format!( - "Edge(source={}, target={}, earliest_time={}, latest_time={}, properties={})", - source.trim_matches('"'), - target.trim_matches('"'), - earliest_time.unwrap_or(0), - latest_time.unwrap_or(0), - property_string - ) - } - } -} - -py_iterator!(PyEdgeIter, EdgeView, PyEdge, "EdgeIter"); - -/// A list of edges that can be iterated over. -#[pyclass(name = "Edges")] -pub struct PyEdges { - builder: Arc BoxedIter> + Send + Sync + 'static>, -} - -impl PyEdges { - /// an iterable that can be used in rust - fn iter(&self) -> BoxedIter> { - (self.builder)() - } - - /// returns an iterable used in python - fn py_iter(&self) -> BoxedIter { - Box::new(self.iter().map(|e| e.into())) - } -} - -#[pymethods] -impl PyEdges { - fn __iter__(&self) -> PyEdgeIter { - PyEdgeIter { - iter: Box::new(self.py_iter()), - } - } - - fn __len__(&self) -> usize { - self.iter().count() - } - - fn src(&self) -> PyVertexIterable { - let builder = self.builder.clone(); - (move || builder().src()).into() - } - - fn dst(&self) -> PyVertexIterable { - let builder = self.builder.clone(); - (move || builder().dst()).into() - } - - /// Returns all edges as a list - fn collect(&self) -> Vec { - self.py_iter().collect() - } - - /// Returns the first edge - fn first(&self) -> Option { - self.py_iter().next() - } - - /// Returns the number of edges - fn count(&self) -> usize { - self.py_iter().count() - } - - /// Explodes the edges into a list of edges. This is useful when you want to iterate over - /// the properties of an Edge at every single point in time. This will return a seperate edge - /// each time a property had been changed. - fn explode(&self) -> PyEdges { - let builder = self.builder.clone(); - (move || { - let iter: BoxedIter> = - Box::new(builder().flat_map(|e| e.explode())); - iter - }) - .into() - } - - /// Returns the earliest time of the edges. - fn earliest_time(&self) -> OptionI64Iterable { - let edges: Arc< - dyn Fn() -> Box> + Send> + Send + Sync, - > = self.builder.clone(); - (move || edges().earliest_time()).into() - } - - /// Returns the latest time of the edges. - fn latest_time(&self) -> OptionI64Iterable { - let edges: Arc< - dyn Fn() -> Box> + Send> + Send + Sync, - > = self.builder.clone(); - (move || edges().latest_time()).into() - } - - fn property(&self, name: String, include_static: Option) -> OptionPropIterable { - let edges: Arc< - dyn Fn() -> Box> + Send> + Send + Sync, - > = self.builder.clone(); - (move || edges().property(name.clone(), include_static.unwrap_or(true))).into() - } - - fn __repr__(&self) -> String { - self.repr() - } -} - -impl Repr for PyEdges { - fn repr(&self) -> String { - format!("Edges({})", iterator_repr(self.__iter__().into_iter())) - } -} - -impl BoxedIter> + Send + Sync + 'static> From for PyEdges { - fn from(value: F) -> Self { - Self { - builder: Arc::new(value), - } - } -} - -py_iterator!( - PyNestedEdgeIter, - BoxedIter>, - PyEdgeIter, - "NestedEdgeIter" -); - -#[pyclass(name = "NestedEdges")] -pub struct PyNestedEdges { - builder: Arc BoxedIter>> + Send + Sync + 'static>, -} - -impl PyNestedEdges { - fn iter(&self) -> BoxedIter>> { - (self.builder)() - } -} - -#[pymethods] -impl PyNestedEdges { - fn __iter__(&self) -> PyNestedEdgeIter { - self.iter().into() - } - - fn collect(&self) -> Vec> { - self.iter() - .map(|e| e.map(|ee| ee.into()).collect()) - .collect() - } - - fn explode(&self) -> PyNestedEdges { - let builder = self.builder.clone(); - (move || { - let iter: BoxedIter>> = Box::new(builder().map(|e| { - let inner_box: BoxedIter> = - Box::new(e.flat_map(|e| e.explode())); - inner_box - })); - iter - }) - .into() - } -} - -impl BoxedIter>> + Send + Sync + 'static> From - for PyNestedEdges -{ - fn from(value: F) -> Self { - Self { - builder: Arc::new(value), - } - } -} diff --git a/py-raphtory/src/graph.rs b/py-raphtory/src/graph.rs deleted file mode 100644 index 765b12dbe6..0000000000 --- a/py-raphtory/src/graph.rs +++ /dev/null @@ -1,225 +0,0 @@ -//! Defines the `Graph` struct, which represents a raphtory graph in memory. -//! -//! This is the base class used to create a temporal graph, add vertices and edges, -//! create windows, and query the graph with a variety of algorithms. -//! It is a wrapper around a set of shards, which are the actual graph data structures. -//! In Python, this class wraps around the rust graph. - -use crate::graph_view::PyGraphView; -use crate::utils::{adapt_result, extract_input_vertex, extract_into_time, InputVertexBox}; -use crate::wrappers::prop::Prop; -use itertools::Itertools; -use pyo3::exceptions::PyException; -use pyo3::prelude::*; -use raphtory::core as dbc; -use raphtory::db::graph::Graph; -use std::collections::HashMap; -use std::fmt::{Debug, Formatter}; -use std::path::{Display, Path, PathBuf}; - -/// A temporal graph. -#[derive(Clone)] -#[pyclass(name="Graph", extends=PyGraphView)] -pub struct PyGraph { - pub(crate) graph: Graph, -} - -impl Debug for PyGraph { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.graph) - } -} - -impl From for PyGraph { - fn from(value: Graph) -> Self { - Self { graph: value } - } -} - -impl PyGraph { - pub fn py_from_db_graph(db_graph: Graph) -> PyResult> { - Python::with_gil(|py| { - Py::new( - py, - (PyGraph::from(db_graph.clone()), PyGraphView::from(db_graph)), - ) - }) - } -} - -/// A temporal graph. -#[pymethods] -impl PyGraph { - #[new] - #[pyo3(signature = (nr_shards=1))] - pub fn py_new(nr_shards: usize) -> (Self, PyGraphView) { - let graph = Graph::new(nr_shards); - ( - Self { - graph: graph.clone(), - }, - PyGraphView::from(graph), - ) - } - - /// Adds a new vertex with the given id and properties to the graph. - /// - /// Arguments: - /// timestamp (int, str, or datetime(utc)): The timestamp of the vertex. - /// id (str or int): The id of the vertex. - /// properties (dict): The properties of the vertex. - /// - /// Returns: - /// None - #[pyo3(signature = (timestamp, id, properties=None))] - pub fn add_vertex( - &self, - timestamp: &PyAny, - id: &PyAny, - properties: Option>, - ) -> PyResult<()> { - let time = extract_into_time(timestamp)?; - let v = Self::extract_id(id)?; - let result = self - .graph - .add_vertex(time, v, &Self::transform_props(properties)); - adapt_result(result) - } - - /// Adds properties to an existing vertex. - /// - /// Arguments: - /// id (str or int): The id of the vertex. - /// properties (dict): The properties of the vertex. - /// - /// Returns: - /// None - pub fn add_vertex_properties( - &self, - id: &PyAny, - properties: HashMap, - ) -> PyResult<()> { - let v = Self::extract_id(id)?; - let result = self - .graph - .add_vertex_properties(v, &Self::transform_props(Some(properties))); - adapt_result(result) - } - - /// Adds a new edge with the given source and destination vertices and properties to the graph. - /// - /// Arguments: - /// timestamp (int): The timestamp of the edge. - /// src (str or int): The id of the source vertex. - /// dst (str or int): The id of the destination vertex. - /// properties (dict): The properties of the edge, as a dict of string and properties - /// layer (str): The layer of the edge. - /// - /// Returns: - /// None - #[pyo3(signature = (timestamp, src, dst, properties=None, layer=None))] - pub fn add_edge( - &self, - timestamp: &PyAny, - src: &PyAny, - dst: &PyAny, - properties: Option>, - layer: Option<&str>, - ) -> PyResult<()> { - let time = extract_into_time(timestamp)?; - let src = Self::extract_id(src)?; - let dst = Self::extract_id(dst)?; - adapt_result( - self.graph - .add_edge(time, src, dst, &Self::transform_props(properties), layer), - ) - } - - /// Adds properties to an existing edge. - /// - /// Arguments: - /// src (str or int): The id of the source vertex. - /// dst (str or int): The id of the destination vertex. - /// properties (dict): The properties of the edge, as a dict of string and properties - /// layer (str): The layer of the edge. - /// - /// Returns: - /// None - #[pyo3(signature = (src, dst, properties, layer=None))] - pub fn add_edge_properties( - &self, - src: &PyAny, - dst: &PyAny, - properties: HashMap, - layer: Option<&str>, - ) -> PyResult<()> { - let src = Self::extract_id(src)?; - let dst = Self::extract_id(dst)?; - let result = self.graph.add_edge_properties( - src, - dst, - &Self::transform_props(Some(properties)), - layer, - ); - adapt_result(result) - } - - //****** Saving And Loading ******// - - // Alternative constructors are tricky, see: https://gist.github.com/redshiftzero/648e4feeff3843ffd9924f13625f839c - - /// Loads a graph from the given path. - /// - /// Arguments: - /// path (str): The path to the graph. - /// - /// Returns: - /// Graph: The loaded graph. - #[staticmethod] - pub fn load_from_file(path: String) -> PyResult> { - let file_path: PathBuf = [env!("CARGO_MANIFEST_DIR"), &path].iter().collect(); - - match Graph::load_from_file(file_path) { - Ok(g) => Self::py_from_db_graph(g), - Err(e) => Err(PyException::new_err(format!( - "Failed to load graph from the files. Reason: {}", - e - ))), - } - } - - /// Saves the graph to the given path. - /// - /// Arguments: - /// path (str): The path to the graph. - /// - /// Returns: - /// None - pub fn save_to_file(&self, path: String) -> PyResult<()> { - match self.graph.save_to_file(Path::new(&path)) { - Ok(()) => Ok(()), - Err(e) => Err(PyException::new_err(format!( - "Failed to save graph to the files. Reason: {}", - e - ))), - } - } -} - -impl PyGraph { - fn transform_props(props: Option>) -> Vec<(String, dbc::Prop)> { - props - .unwrap_or_default() - .into_iter() - .map(|(key, value)| (key, value.into())) - .collect_vec() - } - - /// Extracts the id from the given python vertex - /// - /// Arguments: - /// id (str or int): The id of the vertex. - pub(crate) fn extract_id(id: &PyAny) -> PyResult { - extract_input_vertex(id) - } -} diff --git a/py-raphtory/src/graph_view.rs b/py-raphtory/src/graph_view.rs deleted file mode 100644 index f7edf95fab..0000000000 --- a/py-raphtory/src/graph_view.rs +++ /dev/null @@ -1,302 +0,0 @@ -//! The API for querying a view of the graph in a read-only state -use crate::dynamic::{DynamicGraph, IntoDynamic}; -use crate::edge::{PyEdge, PyEdges}; -use crate::types::repr::Repr; -use crate::utils::{ - at_impl, expanding_impl, extract_vertex_ref, rolling_impl, window_impl, IntoPyObject, - PyWindowSet, -}; -use crate::vertex::{PyVertex, PyVertices}; -use chrono::prelude::*; -use pyo3::prelude::*; -use raphtory::db::view_api::layer::LayerOps; -use raphtory::db::view_api::*; -use raphtory::*; - -/// Graph view is a read-only version of a graph at a certain point in time. -#[pyclass(name = "GraphView", frozen, subclass)] -pub struct PyGraphView { - pub graph: DynamicGraph, -} - -/// Graph view is a read-only version of a graph at a certain point in time. -impl From for PyGraphView { - fn from(value: G) -> Self { - PyGraphView { - graph: value.into_dynamic(), - } - } -} - -impl IntoPyObject for G { - fn into_py_object(self) -> PyObject { - let py_version: PyGraphView = self.into(); - Python::with_gil(|py| py_version.into_py(py)) - } -} - -/// The API for querying a view of the graph in a read-only state -#[pymethods] -impl PyGraphView { - pub fn get_unique_layers(&self) -> Vec { - self.graph.get_unique_layers() - } - - //****** Metrics APIs ******// - - /// Timestamp of earliest activity in the graph - /// - /// Returns: - /// the timestamp of the earliest activity in the graph - pub fn earliest_time(&self) -> Option { - self.graph.earliest_time() - } - - /// DateTime of earliest activity in the graph - /// - /// Returns: - /// the datetime of the earliest activity in the graph - pub fn earliest_date_time(&self) -> Option { - let earliest_time = self.graph.earliest_time()?; - Some(NaiveDateTime::from_timestamp_millis(earliest_time).unwrap()) - } - - /// Timestamp of latest activity in the graph - /// - /// Returns: - /// the timestamp of the latest activity in the graph - pub fn latest_time(&self) -> Option { - self.graph.latest_time() - } - - /// DateTime of latest activity in the graph - /// - /// Returns: - /// the datetime of the latest activity in the graph - pub fn latest_date_time(&self) -> Option { - let latest_time = self.graph.latest_time()?; - Some(NaiveDateTime::from_timestamp_millis(latest_time).unwrap()) - } - - /// Number of edges in the graph - /// - /// Returns: - /// the number of edges in the graph - pub fn num_edges(&self) -> usize { - self.graph.num_edges() - } - - /// Number of vertices in the graph - /// - /// Returns: - /// the number of vertices in the graph - pub fn num_vertices(&self) -> usize { - self.graph.num_vertices() - } - - /// Returns true if the graph contains the specified vertex - /// - /// Arguments: - /// id (str or int): the vertex id - /// - /// Returns: - /// true if the graph contains the specified vertex, false otherwise - pub fn has_vertex(&self, id: &PyAny) -> PyResult { - let v = extract_vertex_ref(id)?; - Ok(self.graph.has_vertex(v)) - } - - /// Returns true if the graph contains the specified edge - /// - /// Arguments: - /// src (str or int): the source vertex id - /// dst (str or int): the destination vertex id - /// layer (str): the edge layer (optional) - /// - /// Returns: - /// true if the graph contains the specified edge, false otherwise - #[pyo3(signature = (src, dst, layer=None))] - pub fn has_edge(&self, src: &PyAny, dst: &PyAny, layer: Option<&str>) -> PyResult { - let src = extract_vertex_ref(src)?; - let dst = extract_vertex_ref(dst)?; - Ok(self.graph.has_edge(src, dst, layer)) - } - - //****** Getter APIs ******// - - /// Gets the vertex with the specified id - /// - /// Arguments: - /// id (str or int): the vertex id - /// - /// Returns: - /// the vertex with the specified id, or None if the vertex does not exist - pub fn vertex(&self, id: &PyAny) -> PyResult> { - let v = extract_vertex_ref(id)?; - Ok(self.graph.vertex(v).map(|v| v.into())) - } - - /// Gets the vertices in the graph - /// - /// Returns: - /// the vertices in the graph - #[getter] - pub fn vertices(&self) -> PyVertices { - self.graph.vertices().into() - } - - /// Gets the edge with the specified source and destination vertices - /// - /// Arguments: - /// src (str or int): the source vertex id - /// dst (str or int): the destination vertex id - /// layer (str): the edge layer (optional) - /// - /// Returns: - /// the edge with the specified source and destination vertices, or None if the edge does not exist - #[pyo3(signature = (src, dst, layer=None))] - pub fn edge(&self, src: &PyAny, dst: &PyAny, layer: Option<&str>) -> PyResult> { - let src = extract_vertex_ref(src)?; - let dst = extract_vertex_ref(dst)?; - Ok(self.graph.edge(src, dst, layer).map(|we| we.into())) - } - - /// Gets all edges in the graph - /// - /// Returns: - /// the edges in the graph - pub fn edges(&self) -> PyEdges { - let clone = self.graph.clone(); - (move || clone.edges()).into() - } - - //****** Perspective APIS ******// - - /// Returns the default start time for perspectives over the view - /// - /// Returns: - /// the default start time for perspectives over the view - pub fn start(&self) -> Option { - self.graph.start() - } - - /// Returns the default start datetime for perspectives over the view - /// - /// Returns: - /// the default start datetime for perspectives over the view - pub fn start_date_time(&self) -> Option { - let start_time = self.graph.start()?; - Some(NaiveDateTime::from_timestamp_millis(start_time).unwrap()) - } - - /// Returns the default end time for perspectives over the view - /// - /// Returns: - /// the default end time for perspectives over the view - pub fn end(&self) -> Option { - self.graph.end() - } - - #[doc = window_size_doc_string!()] - pub fn window_size(&self) -> Option { - self.graph.window_size() - } - - /// Returns the default end datetime for perspectives over the view - /// - /// Returns: - /// the default end datetime for perspectives over the view - pub fn end_date_time(&self) -> Option { - let end_time = self.graph.end()?; - Some(NaiveDateTime::from_timestamp_millis(end_time).unwrap()) - } - - /// Creates a `WindowSet` with the given `step` size and optional `start` and `end` times, - /// using an expanding window. - /// - /// An expanding window is a window that grows by `step` size at each iteration. - /// - /// Arguments: - /// step (int) : the size of the window - /// start (int): the start time of the window (optional) - /// end (int): the end time of the window (optional) - /// - /// Returns: - /// A `WindowSet` with the given `step` size and optional `start` and `end` times, - #[pyo3(signature = (step))] - fn expanding(&self, step: &PyAny) -> PyResult { - expanding_impl(&self.graph, step) - } - - /// Creates a `WindowSet` with the given `window` size and optional `step`, `start` and `end` times, - /// using a rolling window. - /// - /// A rolling window is a window that moves forward by `step` size at each iteration. - /// - /// Arguments: - /// window (int): the size of the window - /// step (int): the size of the step (optional) - /// start (int): the start time of the window (optional) - /// end: the end time of the window (optional) - /// - /// Returns: - /// a `WindowSet` with the given `window` size and optional `step`, `start` and `end` times, - fn rolling(&self, window: &PyAny, step: Option<&PyAny>) -> PyResult { - rolling_impl(&self.graph, window, step) - } - - /// Create a view including all events between `t_start` (inclusive) and `t_end` (exclusive) - /// - /// Arguments: - /// start (int): the start time of the window (optional) - /// end (int): the end time of the window (optional) - /// - /// Returns: - /// a view including all events between `t_start` (inclusive) and `t_end` (exclusive) - #[pyo3(signature = (start=None, end=None))] - pub fn window(&self, start: Option<&PyAny>, end: Option<&PyAny>) -> PyResult { - window_impl(&self.graph, start, end).map(|g| g.into()) - } - - /// Create a view including all events until `end` (inclusive) - /// - /// Arguments: - /// end (int) : the end time of the window - /// - /// Returns: - /// a view including all events until `end` (inclusive) - #[pyo3(signature = (end))] - pub fn at(&self, end: &PyAny) -> PyResult { - at_impl(&self.graph, end).map(|g| g.into()) - } - - #[doc = default_layer_doc_string!()] - pub fn default_layer(&self) -> PyGraphView { - self.graph.default_layer().into() - } - - #[doc = layer_doc_string!()] - #[pyo3(signature = (name))] - pub fn layer(&self, name: &str) -> Option { - self.graph.layer(name).map(|layer| layer.into()) - } - - /// Displays the graph - pub fn __repr__(&self) -> String { - self.repr() - } -} - -impl Repr for PyGraphView { - fn repr(&self) -> String { - let num_edges = self.graph.num_edges(); - let num_vertices = self.graph.num_vertices(); - let earliest_time = self.graph.earliest_time().unwrap_or_default(); - let latest_time = self.graph.latest_time().unwrap_or_default(); - - format!( - "Graph(number_of_edges={:?}, number_of_vertices={:?}, earliest_time={:?}, latest_time={:?})", - num_edges, num_vertices, earliest_time, latest_time - ) - } -} diff --git a/py-raphtory/src/lib.rs b/py-raphtory/src/lib.rs deleted file mode 100644 index b99c9e6218..0000000000 --- a/py-raphtory/src/lib.rs +++ /dev/null @@ -1,16 +0,0 @@ -extern crate core; - -#[macro_use] -mod macros; - -pub mod algorithms; -pub mod dynamic; -pub mod edge; -pub mod graph; -pub mod graph_gen; -pub mod graph_loader; -pub mod graph_view; -pub mod types; -pub mod utils; -pub mod vertex; -pub mod wrappers; diff --git a/py-raphtory/src/macros/iter.rs b/py-raphtory/src/macros/iter.rs deleted file mode 100644 index d1fe39a32a..0000000000 --- a/py-raphtory/src/macros/iter.rs +++ /dev/null @@ -1,69 +0,0 @@ -// Internal macro for generating the iterator struct (with or without name) -macro_rules! _py_iterator_struct { - ($name:ident, $pyitem:ty) => { - #[pyclass] - pub struct $name { - iter: Box + Send>, - } - }; - ($name:ident, $pyname:literal, $pyitem:ty) => { - #[pyclass(name=$pyname)] - pub struct $name { - iter: Box + Send>, - } - }; -} - -// internal macro for adding methods to iterators -macro_rules! _py_iterator_methods { - ($name:ident, $item:ty, $pyitem:ty) => { - #[pymethods] - impl $name { - fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { - slf - } - fn __next__(mut slf: PyRefMut<'_, Self>) -> Option<$pyitem> { - slf.iter.next() - } - } - - impl From + Send>> for $name { - fn from(value: Box + Send>) -> Self { - let iter = Box::new(value.map(|v| v.into())); - Self { iter } - } - } - - impl IntoIterator for $name { - type Item = $pyitem; - type IntoIter = Box + Send>; - - fn into_iter(self) -> Self::IntoIter { - self.iter - } - } - }; -} - -/// Construct a python Iterator struct -/// -/// # Arguments -/// -/// * `name` - The identifier for the new struct -/// * `item` - The type of `Item` for the wrapped iterator -/// * `pyitem` - The type of the python wrapper for `Item` (optional if `item` implements `IntoPy`) -/// * `pyname` - The python-side name for the iterator (optional, defaults to `name`) -macro_rules! py_iterator { - ($name:ident, $item:ty) => { - _py_iterator_struct!($name, $item); - _py_iterator_methods!($name, $item, $item); - }; - ($name:ident, $item:ty, $pyitem:ty) => { - _py_iterator_struct!($name, $pyitem); - _py_iterator_methods!($name, $item, $pyitem); - }; - ($name:ident, $item:ty, $pyitem:ty, $pyname:literal) => { - _py_iterator_struct!($name, $pyname, $pyitem); - _py_iterator_methods!($name, $item, $pyitem); - }; -} diff --git a/py-raphtory/src/macros/iterable.rs b/py-raphtory/src/macros/iterable.rs deleted file mode 100644 index f99e61acaa..0000000000 --- a/py-raphtory/src/macros/iterable.rs +++ /dev/null @@ -1,178 +0,0 @@ -// internal macro for sum and mean methods -macro_rules! _py_numeric_methods { - ($name:ident, $item:ty, $pyitem:ty) => { - #[pymethods] - impl $name { - pub fn sum(&self) -> $pyitem { - let v: $item = self.iter().sum(); - v.into() - } - - pub fn mean(&self) -> f64 { - use $crate::wrappers::iterators::MeanExt; - self.iter().mean() - } - } - }; -} - -// Internal macro defining max and min on ordered iterables -macro_rules! _py_ord_max_min_methods { - ($name:ident, $pyitem:ty) => { - #[pymethods] - impl $name { - pub fn max(&self) -> Option<$pyitem> { - self.iter().max().map(|v| v.into()) - } - - pub fn min(&self) -> Option<$pyitem> { - self.iter().min().map(|v| v.into()) - } - } - }; -} - -// Internal macro defining max and min on float iterables -macro_rules! _py_float_max_min_methods { - ($name:ident, $pyitem:ty) => { - #[pymethods] - impl $name { - pub fn max(&self) -> Option<$pyitem> { - self.iter().max_by(|a, b| a.total_cmp(b)).map(|v| v.into()) - } - pub fn min(&self) -> Option<$pyitem> { - self.iter().min_by(|a, b| a.total_cmp(b)).map(|v| v.into()) - } - } - }; -} - -// Internal macro for methods supported by all iterables (also used by nested iterables) -macro_rules! _py_iterable_base_methods { - ($name:ident, $iter:ty) => { - #[pymethods] - impl $name { - pub fn __iter__(&self) -> $iter { - self.iter().into() - } - - pub fn __len__(&self) -> usize { - self.iter().count() - } - - pub fn __repr__(&self) -> String { - self.repr() - } - } - }; -} - -// internal macro for the collect method (as it is different for nested iterables) -macro_rules! _py_iterable_collect_method { - ($name:ident, $pyitem:ty) => { - #[pymethods] - impl $name { - pub fn collect(&self) -> Vec<$pyitem> { - self.iter().map(|v| v.into()).collect() - } - } - }; -} - -/// Construct a python Iterable struct which wraps a closure that returns an iterator -/// -/// Has methods `__iter__`, `__len__`, `__repr__`, `collect` -/// -/// # Arguments -/// -/// * `name` - The identifier for the new struct -/// * `item` - The type of `Item` for the wrapped iterator builder -/// * `pyitem` - The type of the python wrapper for `Item` (optional if `item` implements `IntoPy`, need Into<`pyitem`> to be implemented for `item`) -/// * `pyiter` - The python iterator wrapper that should be returned when calling `__iter__` (needs to have the same `item` and `pyitem`) -macro_rules! py_iterable { - ($name:ident, $item:ty, $pyiter:ty) => { - py_iterable!($name, $item, $item, $pyiter); - }; - ($name:ident, $item:ty, $pyitem:ty, $pyiter:ty) => { - #[pyclass] - pub struct $name($crate::types::iterable::Iterable<$item, $pyitem>); - - impl std::ops::Deref for $name { - type Target = $crate::types::iterable::Iterable<$item, $pyitem>; - - fn deref(&self) -> &Self::Target { - &self.0 - } - } - - impl BoxedIter<$item> + Send + Sync + 'static> From for $name { - fn from(value: F) -> Self { - Self($crate::types::iterable::Iterable::new( - stringify!($name).to_string(), - value, - )) - } - } - _py_iterable_base_methods!($name, $pyiter); - _py_iterable_collect_method!($name, $pyitem); - }; -} - -/// Construct a python Iterable struct which wraps a closure that returns an iterator of ordered values -/// -/// additionally adds the `min` and `max` methods to those created by `py_iterable` -/// # Arguments -/// -/// * `name` - The identifier for the new struct -/// * `item` - The type of `Item` for the wrapped iterator builder -/// * `pyitem` - The type of the python wrapper for `Item` (optional if `item` implements `IntoPy`, need Into<`pyitem`> to be implemented for `item`) -/// * `pyiter` - The python iterator wrapper that should be returned when calling `__iter__` (needs to have the same `item` and `pyitem`) -macro_rules! py_ordered_iterable { - ($name:ident, $item:ty, $iter:ty) => { - py_ordered_iterable!($name, $item, $item, $iter); - }; - ($name:ident, $item:ty, $pyitem:ty, $iter:ty) => { - py_iterable!($name, $item, $pyitem, $iter); - _py_ord_max_min_methods!($name, $pyitem); - }; -} - -/// Construct a python Iterable struct which wraps a closure that returns an iterator of ordered and summable values -/// -/// additionally adds the `mean` and `sum` methods to those created by `py_ordered_iterable` -/// # Arguments -/// -/// * `name` - The identifier for the new struct -/// * `item` - The type of `Item` for the wrapped iterator builder -/// * `pyitem` - The type of the python wrapper for `Item` (optional if `item` implements `IntoPy`, need Into<`pyitem`> to be implemented for `item`) -/// * `pyiter` - The python iterator wrapper that should be returned when calling `__iter__` (needs to have the same `item` and `pyitem`) -macro_rules! py_numeric_iterable { - ($name:ident, $item:ty, $iter:ty) => { - py_numeric_iterable!($name, $item, $item, $iter); - }; - ($name:ident, $item:ty, $pyitem:ty, $iter:ty) => { - py_ordered_iterable!($name, $item, $pyitem, $iter); - _py_numeric_methods!($name, $item, $pyitem); - }; -} - -/// Construct a python Iterable struct which wraps a closure that returns an iterator of float values -/// -/// This acts the same as `py_numeric_iterable` but with special implementations of `max` and `min` for floats. -/// -/// # Arguments -/// -/// * `name` - The identifier for the new struct -/// * `item` - The type of `Item` for the wrapped iterator builder -/// * `pyitem` - The type of the python wrapper for `Item` (optional if `item` implements `IntoPy`, need Into<`pyitem`> to be implemented for `item`) -/// * `pyiter` - The python iterator wrapper that should be returned when calling `__iter__` (needs to have the same `item` and `pyitem`) -macro_rules! py_float_iterable { - ($name:ident, $item:ty, $iter:ty) => { - py_float_iterable!($name, $item, $item, $iter); - }; - ($name:ident, $item:ty, $pyitem:ty, $iter:ty) => { - py_iterable!($name, $item, $pyitem, $iter); - _py_numeric_methods!($name, $item, $pyitem); - _py_float_max_min_methods!($name, $pyitem); - }; -} diff --git a/py-raphtory/src/macros/mod.rs b/py-raphtory/src/macros/mod.rs deleted file mode 100644 index 7b041b8b41..0000000000 --- a/py-raphtory/src/macros/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -#[macro_use] -mod iter; -#[macro_use] -mod iterable; -#[macro_use] -mod nested_iterable; diff --git a/py-raphtory/src/perspective.rs b/py-raphtory/src/perspective.rs deleted file mode 100644 index 89da73496c..0000000000 --- a/py-raphtory/src/perspective.rs +++ /dev/null @@ -1,113 +0,0 @@ -//! This module defines the `PyPerspective` struct and the `PyPerspectiveSet` iterator. -//! -//! `PyPerspective` is a simple struct representing a time range from `start` to `end`. -//! The start time is inclusive and the end time is exclusive. -//! -//! `PyPerspectiveSet` is an iterator over a range of time periods (`Perspective`s). -//! It can be used to generate rolling or expanding perspectives based on a `step` size and an optional `window` size. -//! -//! These perpectives are used when querying the graph to determine the time bounds. -use pyo3::{pyclass, pymethods}; -use raphtory::db::perspective; -use raphtory::db::perspective::PerspectiveSet; -use std::i64; - -/// A struct representing a time range from `start` to `end`. -/// -/// The start time is inclusive and the end time is exclusive. -#[derive(Clone)] -#[pyclass(name = "Perspective")] -pub struct PyPerspective { - pub start: Option, - pub end: Option, -} - -/// Representing a time range from `start` to `end` for a graph -#[pymethods] -impl PyPerspective { - /// Creates a new `Perspective` with the given `start` and `end` times. - /// Arguments: - /// start (int): The start time of the perspective. If None, the perspective will start at the beginning of the graph. - /// end (int): The end time of the perspective. If None, the perspective will end at the end of the graph. - /// - /// Returns: - /// Perspective: A new perspective with the given start and end times. - #[new] - #[pyo3(signature = (start=None, end=None))] - fn new(start: Option, end: Option) -> Self { - PyPerspective { start, end } - } - - /// Creates an `PyPerspectiveSet` with the given `step` size and optional `start` and `end` times, - /// using an expanding window. - /// - /// An expanding window is a window that grows by `step` size at each iteration. - /// - /// Arguments: - /// step (int): The size of the step to take at each iteration. - /// start (int): The start time of the perspective. If None, the perspective will start at the beginning of the graph. (optional) - /// end (int): The end time of the perspective. If None, the perspective will end at the end of the graph. (optional) - /// - /// Returns: - /// PyPerspectiveSet: An iterator over a range of time periods (`Perspective`s). - #[staticmethod] - #[pyo3(signature = (step, start=None, end=None))] - fn expanding(step: u64, start: Option, end: Option) -> PyPerspectiveSet { - PyPerspectiveSet { - ps: perspective::Perspective::expanding(step, start, end), - } - } - - /// Creates an `PerspectiveSet` with the given `window` size and optional `step`, `start` and `end` times, - /// using a rolling window. - /// - /// A rolling window is a window that moves forward by `step` size at each iteration. - /// If `step` is not provided, it defaults to the `window` size. - /// - /// Arguments: - /// window (int): The size of the window to use at each iteration. - /// step (int): The size of the step to take at each iteration. (optional) - /// start (int): The start time of the perspective. If None, the perspective will start at the beginning of the graph. (optional) - /// end (int): The end time of the perspective. If None, the perspective will end at the end of the graph. (optional) - /// - /// Returns: - /// PyPerspectiveSet: An iterator over a range of time periods (`Perspective`s). - #[staticmethod] - #[pyo3(signature = (window, step=None, start=None, end=None))] - fn rolling( - window: u64, - step: Option, - start: Option, - end: Option, - ) -> PyPerspectiveSet { - PyPerspectiveSet { - ps: perspective::Perspective::rolling(window, step, start, end), - } - } -} - -impl From for PyPerspective { - fn from(value: perspective::Perspective) -> Self { - PyPerspective { - start: value.start, - end: value.end, - } - } -} - -impl From for perspective::Perspective { - fn from(value: PyPerspective) -> Self { - perspective::Perspective { - start: value.start, - end: value.end, - } - } -} - -/// A PerspectiveSet represents a set of windows on a timeline, -/// defined by a start, end, step, and window size. -#[pyclass(name = "PerspectiveSet")] -#[derive(Clone)] -pub struct PyPerspectiveSet { - pub(crate) ps: PerspectiveSet, -} diff --git a/py-raphtory/src/types/iterable.rs b/py-raphtory/src/types/iterable.rs deleted file mode 100644 index 3cb803b3b2..0000000000 --- a/py-raphtory/src/types/iterable.rs +++ /dev/null @@ -1,68 +0,0 @@ -use crate::types::repr::{iterator_repr, Repr}; -use pyo3::{IntoPy, PyObject}; -use raphtory::db::view_api::BoxedIter; -use std::marker::PhantomData; -use std::sync::Arc; - -pub struct Iterable + From + Repr> { - pub name: String, - pub builder: Arc BoxedIter + Send + Sync + 'static>, - pytype: PhantomData, -} - -impl + From + Repr> Iterable { - pub fn iter(&self) -> BoxedIter { - (self.builder)() - } - pub fn py_iter(&self) -> BoxedIter { - Box::new(self.iter().map(|i| i.into())) - } - pub fn new BoxedIter + Send + Sync + 'static>(name: String, builder: F) -> Self { - Self { - name, - builder: Arc::new(builder), - pytype: Default::default(), - } - } -} - -impl + From + Repr> Repr for Iterable { - fn repr(&self) -> String { - format!("{}([{}])", self.name, iterator_repr(self.py_iter())) - } -} - -pub struct NestedIterable + From + Repr> { - pub name: String, - pub builder: Arc BoxedIter> + Send + Sync + 'static>, - pytype: PhantomData, -} - -impl + From + Repr> NestedIterable { - pub fn iter(&self) -> BoxedIter> { - (self.builder)() - } - pub fn new BoxedIter> + Send + Sync + 'static>( - name: String, - builder: F, - ) -> Self { - Self { - name, - builder: Arc::new(builder), - pytype: Default::default(), - } - } -} - -impl + From + Repr> Repr for NestedIterable { - fn repr(&self) -> String { - format!( - "{}([{}])", - self.name, - iterator_repr( - self.iter() - .map(|it| format!("[{}]", iterator_repr(it.map(|i| PyI::from(i))))) - ) - ) - } -} diff --git a/py-raphtory/src/types/mod.rs b/py-raphtory/src/types/mod.rs deleted file mode 100644 index 21288e8fdc..0000000000 --- a/py-raphtory/src/types/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod iterable; -pub mod repr; diff --git a/py-raphtory/src/wrappers/iterators.rs b/py-raphtory/src/wrappers/iterators.rs deleted file mode 100644 index 4364ddf8cb..0000000000 --- a/py-raphtory/src/wrappers/iterators.rs +++ /dev/null @@ -1,163 +0,0 @@ -use crate::types::repr::Repr; -use crate::wrappers::prop::{PropHistories, PropHistory, PropValue, Props}; -use num::cast::AsPrimitive; -use pyo3::prelude::*; -use raphtory::core as db_c; -use raphtory::db::view_api::BoxedIter; -use std::collections::HashMap; -use std::i64; -use std::iter::Sum; -use std::ops::Deref; - -pub(crate) trait MeanExt: Iterator -where - V: AsPrimitive + Sum, -{ - fn mean(self) -> f64 - where - Self: Sized, - { - let mut count: usize = 0; - let sum: V = self.inspect(|_| count += 1).sum(); - - if count > 0 { - sum.as_() / (count as f64) - } else { - 0.0 - } - } -} - -impl, V: AsPrimitive + Sum> MeanExt for I {} - -py_iterator!(Float64Iter, f64); -py_float_iterable!(Float64Iterable, f64, Float64Iter); - -py_iterator!(U64Iter, u64); -py_numeric_iterable!(U64Iterable, u64, U64Iter); -py_iterator!(NestedU64Iter, BoxedIter, U64Iter); -py_nested_numeric_iterable!( - NestedU64Iterable, - u64, - NestedU64Iter, - U64Iterable, - OptionU64Iterable -); - -py_iterator!(OptionU64Iter, Option); -py_iterable!(OptionU64Iterable, Option, Option, OptionU64Iter); -_py_ord_max_min_methods!(OptionU64Iterable, Option); - -py_iterator!(I64Iter, i64); -py_numeric_iterable!(I64Iterable, i64, I64Iter); -py_iterator!(NestedI64Iter, BoxedIter, I64Iter); -py_nested_numeric_iterable!( - NestedI64Iterable, - i64, - NestedI64Iter, - I64Iterable, - OptionI64Iterable -); - -py_iterator!(OptionI64Iter, Option); -py_iterable!(OptionI64Iterable, Option, OptionI64Iter); -_py_ord_max_min_methods!(OptionI64Iterable, Option); -py_iterator!(OptionOptionI64Iter, Option>); -py_iterable!( - OptionOptionI64Iterable, - Option>, - OptionOptionI64Iter -); -_py_ord_max_min_methods!(OptionOptionI64Iterable, Option>); - -py_iterator!(NestedOptionI64Iter, BoxedIter>, OptionI64Iter); -py_nested_ordered_iterable!( - NestedOptionI64Iterable, - Option, - NestedOptionI64Iter, - OptionOptionI64Iterable -); - -py_iterator!(UsizeIter, usize); -py_numeric_iterable!(UsizeIterable, usize, UsizeIter); -py_iterator!(OptionUsizeIter, Option); -py_ordered_iterable!(OptionUsizeIterable, Option, OptionUsizeIter); -py_iterator!(NestedUsizeIter, BoxedIter, UsizeIter); -py_nested_numeric_iterable!( - NestedUsizeIterable, - usize, - NestedUsizeIter, - UsizeIterable, - OptionUsizeIterable -); - -py_iterator!(BoolIter, bool); -py_iterable!(BoolIterable, bool, BoolIter); -py_iterator!(NestedBoolIter, BoxedIter, BoolIter); -py_nested_iterable!(NestedBoolIterable, bool, NestedBoolIter); - -py_iterator!(StringIter, String); -py_iterable!(StringIterable, String, StringIter); -py_iterator!(NestedStringIter, BoxedIter, StringIter); -py_nested_iterable!(NestedStringIterable, String, NestedStringIter); - -py_iterator!(StringVecIter, Vec); -py_iterable!(StringVecIterable, Vec, StringVecIter); -py_iterator!(NestedStringVecIter, BoxedIter>, StringVecIter); -py_nested_iterable!(NestedStringVecIterable, Vec, NestedStringVecIter); - -py_iterator!(OptionPropIter, Option, PropValue); -py_iterable!( - OptionPropIterable, - Option, - PropValue, - OptionPropIter -); -py_iterator!( - NestedOptionPropIter, - BoxedIter>, - OptionPropIter -); -py_nested_iterable!( - NestedOptionPropIterable, - Option, - PropValue, - NestedOptionPropIter -); - -py_iterator!(PropHistoryIter, Vec<(i64, db_c::Prop)>, PropHistory); -py_iterable!( - PropHistoryIterable, - Vec<(i64, db_c::Prop)>, - PropHistory, - PropHistoryIter -); -py_iterator!( - NestedPropHistoryIter, - BoxedIter>, - PropHistoryIter -); -py_nested_iterable!( - NestedPropHistoryIterable, - Vec<(i64, db_c::Prop)>, - PropHistory, - NestedPropHistoryIter -); - -py_iterator!(PropsIter, HashMap, Props); -py_iterable!(PropsIterable, HashMap, Props, PropsIter); -py_iterator!( - NestedPropsIter, - BoxedIter>, - PropsIter -); -py_nested_iterable!(NestedPropsIterable, HashMap, Props, NestedPropsIter); - -py_iterator!(PropHistoriesIter, HashMap>, PropHistories); -py_iterable!(PropHistoriesIterable, HashMap>, PropHistories, PropHistoriesIter); -py_iterator!( - NestedPropHistoriesIter, - BoxedIter>>, - PropHistoriesIter -); -py_nested_iterable!(NestedPropHistoriesIterable, HashMap>, PropHistories, NestedPropHistoriesIter); diff --git a/py-raphtory/src/wrappers/prop.rs b/py-raphtory/src/wrappers/prop.rs deleted file mode 100644 index c3307db1d9..0000000000 --- a/py-raphtory/src/wrappers/prop.rs +++ /dev/null @@ -1,190 +0,0 @@ -use crate::graph::PyGraph; -use crate::graph_view::PyGraphView; -use crate::types::repr::Repr; -use chrono::NaiveDateTime; -use pyo3::{FromPyObject, IntoPy, PyAny, PyObject, PyResult, Python}; -use raphtory::core as db_c; -use raphtory::db; -use std::collections::HashMap; -use std::{fmt, i64}; - -#[repr(transparent)] -#[derive(Debug, Clone)] -pub struct PGraph(db::graph::Graph); - -impl IntoPy for PGraph { - fn into_py(self, py: Python<'_>) -> PyObject { - PyGraph::py_from_db_graph(self.0).unwrap().into_py(py) - } -} - -impl<'source> FromPyObject<'source> for PGraph { - fn extract(ob: &'source PyAny) -> PyResult { - let res: PyGraph = ob.extract()?; - Ok(PGraph(res.graph)) - } -} - -#[derive(FromPyObject, Debug, Clone)] -pub enum Prop { - Str(String), - Bool(bool), - I64(i64), - U64(u64), - F64(f64), - DTime(NaiveDateTime), - Graph(PGraph), -} - -impl fmt::Display for Prop { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - Prop::Str(value) => write!(f, "{}", value), - Prop::Bool(value) => write!(f, "{}", value), - Prop::I64(value) => write!(f, "{}", value), - Prop::U64(value) => write!(f, "{}", value), - Prop::F64(value) => write!(f, "{}", value), - Prop::DTime(value) => write!(f, "{}", value), - Prop::Graph(value) => write!(f, "{}", value.0), - } - } -} - -impl IntoPy for Prop { - fn into_py(self, py: Python<'_>) -> PyObject { - match self { - Prop::Str(s) => s.into_py(py), - Prop::Bool(bool) => bool.into_py(py), - Prop::I64(i64) => i64.into_py(py), - Prop::U64(u64) => u64.into_py(py), - Prop::F64(f64) => f64.into_py(py), - Prop::DTime(dtime) => dtime.into_py(py), - Prop::Graph(g) => g.into_py(py), // Need to find a better way - } - } -} - -impl From for db_c::Prop { - fn from(prop: Prop) -> db_c::Prop { - match prop { - Prop::Str(string) => db_c::Prop::Str(string), - Prop::Bool(bool) => db_c::Prop::Bool(bool), - Prop::I64(i64) => db_c::Prop::I64(i64), - Prop::U64(u64) => db_c::Prop::U64(u64), - Prop::F64(f64) => db_c::Prop::F64(f64), - Prop::DTime(dtime) => db_c::Prop::DTime(dtime), - Prop::Graph(g) => db_c::Prop::Graph(g.0), - } - } -} - -impl From for Prop { - fn from(prop: db_c::Prop) -> Prop { - match prop { - db_c::Prop::Str(string) => Prop::Str(string), - db_c::Prop::Bool(bool) => Prop::Bool(bool), - db_c::Prop::I32(i32) => Prop::I64(i32 as i64), - db_c::Prop::I64(i64) => Prop::I64(i64), - db_c::Prop::U32(u32) => Prop::U64(u32 as u64), - db_c::Prop::U64(u64) => Prop::U64(u64), - db_c::Prop::F64(f64) => Prop::F64(f64), - db_c::Prop::F32(f32) => Prop::F64(f32 as f64), - db_c::Prop::DTime(dtime) => Prop::DTime(dtime), - db_c::Prop::Graph(g) => Prop::Graph(PGraph(g)), - } - } -} - -impl Repr for Prop { - fn repr(&self) -> String { - match &self { - Prop::Str(v) => v.repr(), - Prop::Bool(v) => v.repr(), - Prop::I64(v) => v.repr(), - Prop::U64(v) => v.repr(), - Prop::F64(v) => v.repr(), - Prop::DTime(v) => v.repr(), - Prop::Graph(g) => g.0.to_string(), - } - } -} - -pub struct PropValue(Option); - -impl From> for PropValue { - fn from(value: Option) -> Self { - Self(value.map(|v| v.into())) - } -} - -impl IntoPy for PropValue { - fn into_py(self, py: Python<'_>) -> PyObject { - self.0.into_py(py) - } -} - -impl Repr for PropValue { - fn repr(&self) -> String { - self.0.repr() - } -} - -pub struct Props(HashMap); - -impl Repr for Props { - fn repr(&self) -> String { - self.0.repr() - } -} - -impl From> for Props { - fn from(value: HashMap) -> Self { - Self(value.into_iter().map(|(k, v)| (k, v.into())).collect()) - } -} - -impl IntoPy for Props { - fn into_py(self, py: Python<'_>) -> PyObject { - self.0.into_py(py) - } -} - -pub struct PropHistory(Vec<(i64, Prop)>); - -impl Repr for PropHistory { - fn repr(&self) -> String { - self.0.repr() - } -} - -impl From> for PropHistory { - fn from(value: Vec<(i64, db_c::Prop)>) -> Self { - Self(value.into_iter().map(|(t, v)| (t, v.into())).collect()) - } -} - -impl IntoPy for PropHistory { - fn into_py(self, py: Python<'_>) -> PyObject { - self.0.into_py(py) - } -} - -pub struct PropHistories(HashMap); - -impl Repr for PropHistories { - fn repr(&self) -> String { - self.0.repr() - } -} - -impl From>> for PropHistories { - fn from(value: HashMap>) -> Self { - Self(value.into_iter().map(|(k, h)| (k, h.into())).collect()) - } -} - -impl IntoPy for PropHistories { - fn into_py(self, py: Python<'_>) -> PyObject { - self.0.into_py(py) - } -} diff --git a/python/Cargo.toml b/python/Cargo.toml index adbe1cbbcb..afd2fb1612 100644 --- a/python/Cargo.toml +++ b/python/Cargo.toml @@ -18,13 +18,16 @@ name = "raphtory" crate-type = ["cdylib"] [dependencies] -pyo3 = {version="0.18.1", features=["multiple-pymethods", "chrono"]} -py-raphtory = {path = "../py-raphtory", version = "0.4.0" } +pyo3 = {version= "0.19.2", features=["multiple-pymethods", "chrono"]} +pyo3-asyncio = { version = "0.19.0", features = ["tokio-runtime"] } +raphtory_core = {path = "../raphtory", version = "0.5.7", features=["python"], package="raphtory" } +raphtory-graphql = {path = "../raphtory-graphql", version = "0.5.7" } openssl = { version = "0.10", features = ["vendored"] } # DO NOT REMOVE IT BREAKS PYTHON RELEASE + [features] extension-module = ["pyo3/extension-module"] default = ["extension-module"] [build-dependencies] -pyo3-build-config = "0.18.1" +pyo3-build-config = "0.19.2" diff --git a/python/pyproject.toml b/python/pyproject.toml index 2d09eba682..d336f4dc1a 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -4,16 +4,19 @@ build-backend = "maturin" [project] name = "raphtory" -requires-python = ">=3.7" +requires-python = ">=3.8" classifiers = [ "Programming Language :: Rust", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ] dependencies = [ - "pandas >= 1.3.3", "pyvis >= 0.3.2", "networkx >= 2.6.3", + "pandas >= 2.0.3", + "pyarrow >= 12.0.1", + "requests >= 2.31.0", + "gql[all] == 3.4.1" ] @@ -26,7 +29,7 @@ slack = "https://join.slack.com/t/raphtory/shared_invite/zt-xbebws9j-VgPIFRleJFJ youtube = "https://www.youtube.com/@pometry8546/videos" [project.optional-dependencies] -vis = ["pyvis >= 0.3.2", "networkx >= 2.6.3", "matplotlib >= 3.4.3", "seaborn >= 0.11.2"] +export = ["pyvis >= 0.3.2", "networkx >= 2.6.3", "matplotlib >= 3.4.3", "seaborn >= 0.11.2"] [tool.maturin] features = ["pyo3/extension-module"] diff --git a/python/python/raphtory/__init__.py b/python/python/raphtory/__init__.py index cd5307a257..a55a080cac 100644 --- a/python/python/raphtory/__init__.py +++ b/python/python/raphtory/__init__.py @@ -1,9 +1,11 @@ import sys from .raphtory import * + sys.modules["raphtory.algorithms"] = algorithms sys.modules["raphtory.graph_gen"] = graph_gen sys.modules["raphtory.graph_loader"] = graph_loader - +# sys.modules["raphtory.vectors"] = vectors # TODO: re-enable +# sys.modules["raphtory.graphql"] = graphql from .nullmodels import * @@ -13,4 +15,4 @@ algorithms.__doc__ = "Algorithmic functions that can be run on Raphtory graphs" graph_gen.__doc__ = "Generate Raphtory graphs from attachment models" -graph_loader.__doc__ = "Load and save Raphtory graphs from/to file(s)" \ No newline at end of file +graph_loader.__doc__ = "Load and save Raphtory graphs from/to file(s)" diff --git a/python/python/raphtory/export.py b/python/python/raphtory/export.py new file mode 100644 index 0000000000..c643d65fd5 --- /dev/null +++ b/python/python/raphtory/export.py @@ -0,0 +1,271 @@ +""" +Generate a visualisation using matplotlib or pyvis from Raphtory graphs. +""" +from pyvis.network import Network +import networkx as nx +import pandas as pd + + +def to_pyvis( + graph, + explode_edges=False, + edge_color="#000000", + shape=None, + node_image=None, + edge_weight=None, + edge_label=None, + colour_nodes_by_type=False, + type_property="type", + notebook=True, + **kwargs, +): + r"""Draw a graph with Pyvis. + + .. note:: + + Pyvis is a required dependency. + If you intend to use this function make sure that + you install Pyvis with ``pip install pyvis`` + + :param graph: A Raphtory graph. + :param explode_edges: A boolean that is set to True if you want to explode the edges in the graph. By default this is set to False. + :param str edge_color: A string defining the colour of the edges in the graph. By default ``#000000`` (black) is set. + :param str shape: An optional string defining what the node looks like. + There are two types of nodes. One type has the label inside of it and the other type has the label underneath it. + The types with the label inside of it are: ellipse, circle, database, box, text. + The ones with the label outside of it are: image, circularImage, diamond, dot, star, triangle, triangleDown, square and icon. + By default ``"dot"`` is set. + :param str node_image: An optional string defining the url of a custom node image. By default an image of a circle is set. + :param str edge_weight: An optional string defining the name of the property where edge weight is set on your Raphtory graph. By default ``1`` is set. + :param str edge_label: An optional string defining the name of the property where edge label is set on your Raphtory graph. By default, an empty string as the label is set. + :param bool notebook: A boolean that is set to True if using jupyter notebook. By default this is set to True. + :param kwargs: Additional keyword arguments that are passed to the pyvis Network class. + + :returns: A pyvis network + + For Example: + + .. jupyter-execute:: + + from raphtory import Graph + from raphtory import export + + g = Graph() + g.add_vertex(1, src, properties={"image": "image.png"}) + g.add_edge(1, 1, 2, {"title": "edge", "weight": 1}) + g.add_edge(1, 2, 1, {"title": "edge", "weight": 3}) + + export.to_pyvis(graph=g, edge_color="#FF0000", edge_weight= "weight", shape="image", node_image="image", edge_label="title") + + """ + visGraph = Network(notebook=notebook, **kwargs) + if colour_nodes_by_type: + groups = { + value: index + 1 + for index, value in enumerate( + set(graph.vertices.properties.get(type_property)) + ) + } + + for v in graph.vertices: + image = ( + v.properties.get(node_image) + if node_image != None + else "https://cdn-icons-png.flaticon.com/512/7584/7584620.png" + ) + shape = shape if shape is not None else "dot" + if colour_nodes_by_type: + visGraph.add_node( + v.id, + label=v.name, + shape=shape, + image=image, + group=groups[v.properties.get(type_property)], + ) + else: + visGraph.add_node(v.id, label=v.name, shape=shape, image=image) + + edges = graph.edges().explode() if explode_edges else graph.edges().explode_layers() + for e in edges: + weight = e.properties.get(edge_weight) if edge_weight is not None else 1 + if weight is None: + weight = 1 + label = e.properties.get(edge_label) if edge_label is not None else "" + if label is None: + label = "" + visGraph.add_edge( + e.src.id, + e.dst.id, + value=weight, + color=edge_color, + title=label, + arrowStrikethrough=False, + ) + + return visGraph + + +def to_networkx( + graph, + explode_edges=False, + include_vertex_properties=True, + include_edge_properties=True, + include_update_history=True, + include_property_histories=True, +): + r"""Returns a graph with NetworkX. + .. note:: + + Network X is a required dependency. + If you intend to use this function make sure that + you install Network X with ``pip install networkx`` + + :param Graph graph: A Raphtory graph. + :param bool explode_edges: A boolean that is set to True if you want to explode the edges in the graph. By default this is set to False. + :param bool include_vertex_properties: A boolean that is set to True if you want to include the vertex properties in the graph. By default this is set to True. + :param bool include_edge_properties: A boolean that is set to True if you want to include the edge properties in the graph. By default this is set to True. + :param bool include_update_history: A boolean that is set to True if you want to include the update histories in the graph. By default this is set to True. + :param bool include_property_histories: A boolean that is set to True if you want to include the histories in the graph. By default this is set to True. + + :returns: A Networkx MultiDiGraph. + """ + + networkXGraph = nx.MultiDiGraph() + + vertex_tuples = [] + for v in graph.vertices: + properties = {} + if include_vertex_properties: + if include_property_histories: + properties.update(v.properties.constant.as_dict()) + properties.update(v.properties.temporal.histories()) + else: + properties = v.properties.as_dict() + if include_update_history: + properties.update({"update_history": v.history()}) + vertex_tuples.append((v.name, properties)) + networkXGraph.add_nodes_from(vertex_tuples) + + edge_tuples = [] + edges = graph.edges().explode() if explode_edges else graph.edges().explode_layers() + for e in edges: + properties = {} + src = e.src.name + dst = e.dst.name + if include_edge_properties: + if include_property_histories: + properties.update(e.properties.constant.as_dict()) + properties.update(e.properties.temporal.histories()) + else: + properties = e.properties.as_dict() + layer = e.layer_name + if layer is not None: + properties.update({"layer": layer}) + if include_update_history: + if explode_edges: + properties.update({"update_history": e.time}) + else: + properties.update({"update_history": e.history()}) + edge_tuples.append((src, dst, properties)) + + networkXGraph.add_edges_from(edge_tuples) + + return networkXGraph + + +def to_edge_df( + graph, + explode_edges=False, + include_edge_properties=True, + include_update_history=True, + include_property_histories=True, +): + r"""Returns an edge list pandas dataframe for the given graph. + .. note:: + + Pandas is a required dependency. + If you intend to use this function make sure that + you install pandas with ``pip install pandas`` + + :param Graph graph: A Raphtory graph. + :param bool explode_edges: A boolean that is set to True if you want to explode the edges in the graph. By default this is set to False. + :param bool include_edge_properties: A boolean that is set to True if you want to include the edge properties in the graph. By default this is set to True. + :param bool include_update_history: A boolean that is set to True if you want to include the update histories in the graph. By default this is set to True. + :param bool include_property_histories: A boolean that is set to True if you want to include the histories in the graph. By default this is set to True. + + :returns: A pandas dataframe. + """ + edge_tuples = [] + + columns = ["src", "dst", "layer"] + if include_edge_properties: + columns.append("properties") + if include_update_history: + columns.append("update_history") + + edges = graph.edges().explode() if explode_edges else graph.edges().explode_layers() + for e in edges: + tuple = [e.src.name, e.dst.name, e.layer_name] + if include_edge_properties: + properties = {} + if include_property_histories: + properties.update(e.properties.constant.as_dict()) + properties.update(e.properties.temporal.histories()) + else: + properties = e.properties.as_dict() + tuple.append(properties) + + if include_update_history: + if explode_edges: + tuple.append(e.time) + else: + tuple.append(e.history()) + + edge_tuples.append(tuple) + + return pd.DataFrame(edge_tuples, columns=columns) + + +def to_vertex_df( + graph, + include_vertex_properties=True, + include_update_history=True, + include_property_histories=True, +): + r"""Returns an vertex list pandas dataframe for the given graph. + + .. note:: + + Pandas is a required dependency. + If you intend to use this function make sure that + you install pandas with ``pip install pandas`` + + :param Graph graph: A Raphtory graph. + :param bool include_vertex_properties: A boolean that is set to True if you want to include the vertex properties in the graph. By default this is set to True. + :param bool include_update_history: A boolean that is set to True if you want to include the update histories in the graph. By default this is set to True. + :param bool include_property_histories: A boolean that is set to True if you want to include the histories in the graph. By default this is set to True. + + :returns: A pandas dataframe. + + """ + vertex_tuples = [] + columns = ["id"] + if include_vertex_properties: + columns.append("properties") + if include_update_history: + columns.append("update_history") + + for v in graph.vertices: + tuple = [v.name] + if include_vertex_properties: + properties = {} + if include_property_histories: + properties.update(v.properties.constant.as_dict()) + properties.update(v.properties.temporal.histories()) + else: + properties = v.properties.as_dict() + tuple.append(properties) + if include_update_history: + tuple.append(v.history()) + vertex_tuples.append(tuple) + return pd.DataFrame(vertex_tuples, columns=columns) diff --git a/python/python/raphtory/graphqlclient.py b/python/python/raphtory/graphqlclient.py new file mode 100644 index 0000000000..7bc130bfcf --- /dev/null +++ b/python/python/raphtory/graphqlclient.py @@ -0,0 +1,128 @@ +from gql import Client, gql +from gql.transport.requests import RequestsHTTPTransport +import raphtory +from raphtory import internal_graphql + + +class RaphtoryGraphQLClient: + """ + A client for handling GraphQL operations in the context of Raphtory. + """ + + def __init__(self, url: str): + """ + Initialize a GraphQL Client Connection. + + Args: + url (str): URL to a server with the port appended to the URL. + + Note: + This constructor creates a GraphQL client connection to the given URL. + """ + transport = RequestsHTTPTransport(url=url, use_json=True) + self.client = Client(transport=transport, fetch_schema_from_transport=True) + # Below attempts to connect to the server with the url + # self.client.connect_sync() + + def query(self, query: str, variables: dict = {}): + """ + Execute a GraphQL query. + + Args: + query (str): The GraphQL query string. + variables (dict, optional): Variables for the query. Defaults to an empty dictionary. + + Returns: + dict: Result of the query. + """ + query = gql(query) + return self.client.execute(query, variables) + + def load_graphs_from_path(self, path: str) -> dict: + """ + Load graphs from a directory of bincode files. + + Args: + path (str): Directory containing bincode files. + + Returns: + dict: Result after executing the mutation. + + Note: + Existing graphs with the same name are overwritten. + """ + mutation_q = gql( + """ + mutation LoadGraphsFromPath($path: String!) { + loadGraphsFromPath(path: $path) + } + """ + ) + result = self.client.execute(mutation_q, variable_values={"path": path}) + if len(result["loadGraphsFromPath"]): + print("Loaded %i graph(s)" % len(result["loadGraphsFromPath"])) + return result + else: + print("Could not find a graph to load") + return result + + def load_new_graphs_from_path(self, path: str) -> dict: + """ + Load new graphs from a directory of bincode files. + + Args: + path (str): Directory containing bincode files. + + Returns: + dict: Result after executing the mutation. + + Note: + Existing graphs will not be overwritten. + """ + mutation_q = gql( + """ + mutation LoadNewGraphsFromPath($path: String!) { + loadNewGraphsFromPath(path: $path) + } + """ + ) + result = self.client.execute(mutation_q, variable_values={"path": path}) + + if len(result["loadNewGraphsFromPath"]): + print("Loaded %i graph(s)" % len(result["loadNewGraphsFromPath"])) + return result + else: + print("Could not find a graph to load") + return result + + def send_graph(self, name: str, graph: raphtory.Graph): + """ + Upload a graph to the GraphQL Server. + + Args: + name (str): Name of the graph. + graph (raphtory.Graph): Graph object to be uploaded. + + Returns: + dict: Result after executing the mutation. + + Raises: + Exception: If there's an error sending the graph. + """ + encoded_graph = internal_graphql.encode_graph(graph) + + mutation_q = gql( + """ + mutation SendGraph($name: String!, $graph: String!) { + sendGraph(name: $name, graph: $graph) + } + """ + ) + result = self.client.execute( + mutation_q, variable_values={"name": name, "graph": encoded_graph} + ) + if "sendGraph" in result: + print("Sent graph %s to GraphlQL Server" % len(result["sendGraph"])) + return result + else: + raise Exception("Error Sending Graph %s" % result) diff --git a/python/python/raphtory/graphqlserver.py b/python/python/raphtory/graphqlserver.py new file mode 100644 index 0000000000..360feafa4e --- /dev/null +++ b/python/python/raphtory/graphqlserver.py @@ -0,0 +1,103 @@ +""" +This module contains helper functions and classes for working with the GraphQL server for Raphtory. +Calling the run_server function will start the GraphQL server. If run in the background, this will return a +GraphQLServer object that can be used to run queries. +""" + +from raphtory import internal_graphql +import asyncio +import threading +import requests +import time +from raphtory import graphqlclient + + +class GraphQLServer: + """ + A helper class that can be used to query the Raphtory GraphQL server. + """ + + def __init__(self, port): + self.port = port + + def query(self, query): + """ + Runs a GraphQL query on the server. + + :param query(str): The GraphQL query to run. + + :raises Exception: If the query fails to run. + + :return: Returns the json-encoded content of a response, if any. + + """ + r = requests.post("http://localhost:" + str(self.port), json={"query": query}) + if r.status_code == 200: + return r.json() + else: + raise Exception(f"Query failed to run with a {r.status_code}.") + + def wait_for_online(self): + """ + Waits for the server to be online. This is called automatically when run_server is called. + """ + while True: + try: + r = requests.get("http://localhost:" + str(self.port)) + if r.status_code == 200: + return True + except: + pass + time.sleep(1) + + +async def _from_map_and_directory(graphs, graph_dir, port): + await internal_graphql.from_map_and_directory(graphs, graph_dir, port) + + +async def _from_directory(graph_dir, port): + await internal_graphql.from_directory(graph_dir, port) + + +async def _from_map(graphs, port): + await internal_graphql.from_map(graphs, port) + + +def _run(func, daemon, port): + if daemon: + + def _run_in_background(): + asyncio.run(func) + + threading.Thread(target=_run_in_background, daemon=True).start() + server = GraphQLServer(port) + server.wait_for_online() + return graphqlclient.RaphtoryGraphQLClient("http://localhost:" + str(port)) + else: + loop = asyncio.get_event_loop() + loop.run_until_complete(func) + loop.close() + + +def run_server(graphs=None, graph_dir=None, port=1736, daemon=False): + """ + Runs the Raphtory GraphQL server. + + Args: + graphs (dict, optional): A dictionary of graphs to load into the server. Default is None. + graph_dir (str, optional): The directory to load graphs from. Default is None. + port (int, optional): The port to run the server on. Default is 1736. + daemon (bool, optional): Whether to run the server in the background. Default is False. + + Returns: + GraphQLServer: A GraphQLServer object that can be used to query the server. (Only if daemon is True) + """ + + if graph_dir is not None and graphs is not None: + return _run(_from_map_and_directory(graphs, graph_dir, port), daemon, port) + elif graph_dir is not None: + return _run(_from_directory(graph_dir, port), daemon, port) + elif graphs is not None: + return _run(_from_map(graphs, port), daemon, port) + else: + print("No graphs or graph directory specified. Exiting.") diff --git a/python/python/raphtory/nullmodels.py b/python/python/raphtory/nullmodels.py index fa5fc0923e..46b026a0dd 100644 --- a/python/python/raphtory/nullmodels.py +++ b/python/python/raphtory/nullmodels.py @@ -1,15 +1,36 @@ """ -Generate null models for a graph. +Generate randomised reference models for a temporal graph edgelist """ import pandas as pd -def shuffle_column(graph_df:pd.DataFrame, col_number=None, col_name=None, inplace=False): + +def shuffle_column( + graph_df: pd.DataFrame, col_number=None, col_name=None, inplace=False +): """ - returns a dataframe with a given column shuffled + Returns an edgelist with a given column shuffled. Exactly one of col_number or col_name should be specified. + + Args: + graph_df (pd.DataFrame): The input DataFrame representing the timestamped edgelist. + col_number (int, optional): The column number to shuffle. Default is None. + col_name (str, optional): The column name to shuffle. Default is None. + inplace (bool, optional): If True, shuffles the column in-place. Otherwise, creates a copy of the DataFrame. Default is False. + + Returns: + pd.DataFrame: The shuffled DataFrame with the specified column. + + Raises: + AssertionError: If neither col_number nor col_name is provided. + AssertionError: If both col_number and col_name are provided. + """ - assert col_number is not None or col_name is not None, f"No column number or name provided." - assert not (col_name is not None and col_number is not None), f"Cannot have both a column number and a column name." + assert ( + col_number is not None or col_name is not None + ), f"No column number or name provided." + assert not ( + col_name is not None and col_number is not None + ), f"Cannot have both a column number and a column name." if inplace: df = graph_df @@ -19,21 +40,45 @@ def shuffle_column(graph_df:pd.DataFrame, col_number=None, col_name=None, inplac no_events = len(df) if col_number is not None: - col = df[[col_number]].sample(n=no_events) - col.reset_index(inplace=True,drop=True) - df[[col_number]] = col + col = df[df.columns[col_number]].sample(n=no_events) + col.reset_index(inplace=True, drop=True) + df[df.columns[col_number]] = col if col_name is not None: col = df[col_name].sample(n=no_events) - col.reset_index(inplace=True,drop=True) - + col.reset_index(inplace=True, drop=True) + df[col_name] = col return df -def shuffle_multiple_columns(graph_df:pd.DataFrame, col_numbers:list=None, col_names:list=None, inplace=False): + +def shuffle_multiple_columns( + graph_df: pd.DataFrame, + col_numbers: list = None, + col_names: list = None, + inplace=False, +): """ - returns a dataframe with a given columns shuffled. + Returns an edgelist with given columns shuffled. Exactly one of col_numbers or col_names should be specified. + + Args: + graph_df (pd.DataFrame): The input DataFrame representing the graph. + col_numbers (list, optional): The list of column numbers to shuffle. Default is None. + col_names (list, optional): The list of column names to shuffle. Default is None. + inplace (bool, optional): If True, shuffles the columns in-place. Otherwise, creates a copy of the DataFrame. Default is False. + + Returns: + pd.DataFrame: The shuffled DataFrame with the specified columns. + + Raises: + AssertionError: If neither col_numbers nor col_names are provided. + AssertionError: If both col_numbers and col_names are provided. + """ - assert col_numbers is not None or col_names is not None, f"No column numbers or names provided." - assert not (col_names is not None and col_numbers is not None), f"Cannot have both column numbers and column names." + assert ( + col_numbers is not None or col_names is not None + ), f"No column numbers or names provided." + assert not ( + col_names is not None and col_numbers is not None + ), f"Cannot have both column numbers and column names." if col_numbers is not None: for n in col_numbers: @@ -41,19 +86,38 @@ def shuffle_multiple_columns(graph_df:pd.DataFrame, col_numbers:list=None, col_n if col_names is not None: for name in col_names: df = shuffle_column(graph_df, col_name=name) - return df - -def permuted_timestamps_model(graph_df:pd.DataFrame, time_col:int=None, time_name:str=None, inplace=False, sorted=False): + + +def permuted_timestamps_model( + graph_df: pd.DataFrame, + time_col: int = None, + time_name: str = None, + inplace=False, + sorted=False, +): """ - returns a dataframe with the time column shuffled + Returns a DataFrame with the time column shuffled. + + Args: + graph_df (pd.DataFrame): The input DataFrame representing the graph. + time_col (int, optional): The column number of the time column to shuffle. Default is None. + time_name (str, optional): The column name of the time column to shuffle. Default is None. + inplace (bool, optional): If True, shuffles the time column in-place. Otherwise, creates a copy of the DataFrame. Default is False. + sorted (bool, optional): If True, sorts the DataFrame by the shuffled time column. Default is False. + + Returns: + pd.DataFrame or None: The shuffled DataFrame with the time column, or None if inplace=True. + """ shuffled_df = shuffle_column(graph_df, time_col, time_name, inplace) if sorted: - shuffled_df.sort_values(by=time_name if time_name else shuffled_df.columns[time_col], inplace=True) - + shuffled_df.sort_values( + by=time_name if time_name else shuffled_df.columns[time_col], inplace=True + ) + if inplace: return else: - return shuffled_df \ No newline at end of file + return shuffled_df diff --git a/python/python/raphtory/vis.py b/python/python/raphtory/vis.py deleted file mode 100644 index 4011d8c6c0..0000000000 --- a/python/python/raphtory/vis.py +++ /dev/null @@ -1,153 +0,0 @@ -""" -Generate a visualisation using matplotlib or pyvis from Raphtory graphs. -""" -from pyvis.network import Network -import networkx as nx - -r"""Draw a graph with Pyvis. - -.. note:: - - Pyvis is a required dependency. - If you intend to use this function make sure that - you install Pyvis with ``pip install pyvis`` - -:param graph: A Raphtory graph. -:param str height: A string defining the height of the graph. By default ``800px`` is set. -:param str width: A string defining the width of the graph. By default ``800px`` is set. -:param str bg_color: A string defining the colour of the graph background. It must be a HTML color code. By default ``#white`` (white) is set. -:param str font_color: A string defining the colour of the graph font. By default ``"black"`` is set. -:param str edge_color: A string defining the colour of the edges in the graph. By default ``#000000`` (black) is set. -:param str shape: An optional string defining what the node looks like. - There are two types of nodes. One type has the label inside of it and the other type has the label underneath it. - The types with the label inside of it are: ellipse, circle, database, box, text. - The ones with the label outside of it are: image, circularImage, diamond, dot, star, triangle, triangleDown, square and icon. - By default ``"dot"`` is set. -:param str node_image: An optional string defining the url of a custom node image. By default an image of a circle is set. -:param str edge_weight: An optional string defining the name of the property where edge weight is set on your Raphtory graph. By default ``1`` is set. -:param str edge_label: An optional string defining the name of the property where edge label is set on your Raphtory graph. By default, an empty string as the label is set. -:param bool notebook: A boolean that is set to True if using jupyter notebook. By default this is set to True. - - -:returns: A pyvis visualisation in static HTML format that is interactive with toggles menu. -:rtype: IFrame(name, width=self.width, height=self.height) - -For Example: - -.. jupyter-execute:: - - from raphtory import Graph - from raphtory import vis - - g = Graph() - g.add_vertex(1, src, properties={"image": "image.png"}) - g.add_edge(1, 1, 2, {"title": "edge", "weight": 1}) - g.add_edge(1, 2, 1, {"title": "edge", "weight": 3}) - - vis.to_pyvis(graph=g, edge_color="#FF0000", edge_weight= "weight", shape="image", node_image="image", edge_label="title") - -""" - -def to_pyvis( - graph, - height="800px", - width="800px", - bg_color="#white", - font_color="black", - edge_color="#000000", - shape=None, - node_image=None, - edge_weight=None, - edge_label=None, - notebook=True, - ): - """ - Returns a dynamic visualisation in static HTML format from a Raphtory graph. - """ - visGraph = Network(height=height, width=width, bgcolor=bg_color, font_color=font_color, notebook=notebook) - - for v in graph.vertices(): - image = v.property(node_image) if node_image != None else "https://cdn-icons-png.flaticon.com/512/7584/7584620.png" - shape = shape if shape != None else "dot" - visGraph.add_node(v.id(), label= v.name(), shape=shape, image=image) - - for e in graph.edges(): - weight = e.property(edge_weight) if edge_weight != None else 1 - label = e.property(edge_label) if edge_label != None else "" - visGraph.add_edge(e.src().id(), e.dst().id(), value=weight, color=edge_color, title=label) - - visGraph.show_buttons(filter_=['physics']) - visGraph.show('nx.html') - return visGraph - -r"""Draw a graph with NetworkX. - -.. note:: - - Network X is a required dependency. - If you intend to use this function make sure that - you install Network X with ``pip install networkx`` - -:param graph: A Raphtory graph. -:param float k: A float defining optimal distance between nodes. If None the distance is set to 1/sqrt(n) where n is the number of nodes. Increase this value to move nodes farther apart. -:param int iterations: An integer defining the maximum number of iterations taken to generate the optimum spring layout. Increasing this number will increase the computational time to generate the layout. By default ``50`` is set. -:param scalar or array node_size: A scalar defining the size of nodes. By default ``300`` is set. -:param color or array of colors node_color: Node color. Can be a single color or a sequence of colors with the same length as nodelist. Color can be string or rgb (or rgba) tuple of floats from 0-1. If numeric values are specified they will be mapped to colors using the cmap and vmin,vmax parameters. See matplotlib.scatter for more details. By default ``"#1f78b4"`` (blue) is set. -:param color or array of colors edge_color: Edge color. Can be a single color or a sequence of colors with the same length as edgelist. Color can be string or rgb (or rgba) tuple of floats from 0-1. If numeric values are specified they will be mapped to colors using the edge_cmap and edge_vmin,edge_vmax parameters. By default ``'k'`` (black) is set. -:param bool arrows: If None, directed graphs draw arrowheads with FancyArrowPatch, while undirected graphs draw edges via LineCollection for speed. If True, draw arrowheads with FancyArrowPatches (bendable and stylish). If False, draw edges using LineCollection (linear and fast). - Note: Arrowheads will be the same color as edges. Default is None. -:param str arrow_style: Style of the edges, defaults to ``‘-|>’``. - -:returns: A networkx visualisation that appears in the notebook output. -:rtype: matplotlib.collections.PathCollection and matplotlib.collections.LineCollection or a list of matplotlib.patches.FancyArrowPatch. - `PathCollection` of the nodes. - If ``arrows=True``, a list of FancyArrowPatches is returned. - If ``arrows=False``, a LineCollection is returned. - If ``arrows=None`` (the default), then a LineCollection is returned if - `G` is undirected, otherwise returns a list of FancyArrowPatches. - -For Example: - -.. jupyter-execute:: - - from raphtory import Graph - from raphtory import vis - - g = Graph() - g.add_vertex(1, src, properties={"image": "image.png"}) - g.add_edge(1, 1, 2, {"title": "edge", "weight": 1}) - g.add_edge(1, 2, 1, {"title": "edge", "weight": 3}) - - vis.to_networkx(graph=g, k=0.15, iterations=100, node_size=500, node_color='red', edge_color='blue', arrows=True) - -""" -def to_networkx( - graph, - k=None, - iterations=50, - node_size=300, - node_color='#1f78b4', - edge_color='k', - arrows=None, - arrow_style= "-|>" - ): - """ - Returns a Network X graph visualiation from a Raphtory graph. - """ - - networkXGraph = nx.MultiDiGraph() - - networkXGraph.add_nodes_from(list(graph.vertices().id())) - - edges = [] - for e in graph.edges(): - edges.append((e.src().id(), e.dst().id())) - - networkXGraph.add_edges_from(edges) - pos = nx.spring_layout(networkXGraph, k=k, iterations=iterations) - - nx.draw_networkx_nodes(networkXGraph, pos, node_size=node_size, node_color=node_color) - nx.draw_networkx_edges(networkXGraph, pos, edge_color=edge_color, arrows=arrows, arrowstyle=arrow_style) - - - diff --git a/python/src/graphql.rs b/python/src/graphql.rs new file mode 100644 index 0000000000..048cd2050f --- /dev/null +++ b/python/src/graphql.rs @@ -0,0 +1,85 @@ +use pyo3::{exceptions, prelude::*}; +use raphtory_core::{ + db::api::view::internal::MaterializedGraph, + prelude::Graph, + python::{graph::graph::PyGraph, utils::errors::adapt_err_value}, +}; +use raphtory_graphql::{url_decode_graph, url_encode_graph, RaphtoryServer}; +use std::collections::HashMap; + +#[pyfunction] +pub fn from_map( + py: Python, + graphs: HashMap, + port: Option, +) -> PyResult<&PyAny> { + let graphs: HashMap = graphs + .into_iter() + .map(|(key, value)| (key, value.into())) + .collect(); + let server = RaphtoryServer::from_map(graphs); + let port = port.unwrap_or(1736); + pyo3_asyncio::tokio::future_into_py(py, async move { + server + .run_with_port(port) + .await + .map_err(|e| adapt_err_value(&e)) + }) +} + +#[pyfunction] +pub fn from_directory(py: Python, path: String, port: Option) -> PyResult<&PyAny> { + let server = RaphtoryServer::from_directory(path.as_str()); + let port = port.unwrap_or(1736); + pyo3_asyncio::tokio::future_into_py(py, async move { + server + .run_with_port(port) + .await + .map_err(|e| adapt_err_value(&e)) + }) +} + +#[pyfunction] +pub fn from_map_and_directory( + py: Python, + graphs: HashMap, + path: String, + port: Option, +) -> PyResult<&PyAny> { + let graphs: HashMap = graphs + .into_iter() + .map(|(key, value)| (key, value.into())) + .collect(); + let port = port.unwrap_or(1736); + let server = RaphtoryServer::from_map_and_directory(graphs, path.as_str()); + pyo3_asyncio::tokio::future_into_py(py, async move { + server + .run_with_port(port) + .await + .map_err(|e| adapt_err_value(&e)) + }) +} + +#[pyfunction] +pub fn encode_graph(graph: MaterializedGraph) -> PyResult { + let result = url_encode_graph(graph); + match result { + Ok(s) => Ok(s), + Err(e) => Err(exceptions::PyValueError::new_err(format!( + "Error encoding: {:?}", + e + ))), + } +} + +#[pyfunction] +pub fn decode_graph(py: Python, encoded_graph: String) -> PyResult { + let result = url_decode_graph(encoded_graph); + match result { + Ok(s) => Ok(s.into_py(py)), + Err(e) => Err(exceptions::PyValueError::new_err(format!( + "Error decoding: {:?}", + e + ))), + } +} diff --git a/python/src/lib.rs b/python/src/lib.rs index 0222eaf018..9dd2e70636 100644 --- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -1,32 +1,53 @@ -extern crate core; +mod graphql; -use py_raphtory::algorithms::*; -use py_raphtory::graph::PyGraph; -use py_raphtory::graph_gen::*; -use py_raphtory::graph_loader::*; +extern crate core; +use graphql::*; use pyo3::prelude::*; -use py_raphtory::edge::{PyEdge, PyEdges}; -use py_raphtory::vertex::{PyVertex, PyVertices}; +use raphtory_core::python::{ + graph::{ + algorithm_result::AlgorithmResultStrU64, + edge::{PyDirection, PyEdge, PyEdges}, + graph::PyGraph, + graph_with_deletions::PyGraphWithDeletions, + properties::{PyConstProperties, PyProperties, PyTemporalProp, PyTemporalProperties}, + vertex::{PyVertex, PyVertices}, + }, + packages::{algorithms::*, graph_gen::*, graph_loader::*}, +}; /// Raphtory graph analytics library #[pymodule] fn raphtory(py: Python<'_>, m: &PyModule) -> PyResult<()> { + //Graph classes m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + + //GRAPHQL + let graphql_module = PyModule::new(py, "internal_graphql")?; + graphql_module.add_function(wrap_pyfunction!(from_map, graphql_module)?)?; + graphql_module.add_function(wrap_pyfunction!(from_directory, graphql_module)?)?; + graphql_module.add_function(wrap_pyfunction!(from_map_and_directory, graphql_module)?)?; + graphql_module.add_function(wrap_pyfunction!(encode_graph, graphql_module)?)?; + graphql_module.add_function(wrap_pyfunction!(decode_graph, graphql_module)?)?; + m.add_submodule(graphql_module)?; + //ALGORITHMS let algorithm_module = PyModule::new(py, "algorithms")?; algorithm_module.add_function(wrap_pyfunction!(global_reciprocity, algorithm_module)?)?; algorithm_module.add_function(wrap_pyfunction!(all_local_reciprocity, algorithm_module)?)?; + m.add_class::()?; + algorithm_module.add_function(wrap_pyfunction!(triplet_count, algorithm_module)?)?; - algorithm_module.add_function(wrap_pyfunction!( - global_clustering_coefficient, - algorithm_module - )?)?; algorithm_module.add_function(wrap_pyfunction!(local_triangle_count, algorithm_module)?)?; - algorithm_module.add_function(wrap_pyfunction!(generic_taint, algorithm_module)?)?; - algorithm_module.add_function(wrap_pyfunction!( - local_clustering_coefficient, - algorithm_module - )?)?; algorithm_module.add_function(wrap_pyfunction!(average_degree, algorithm_module)?)?; algorithm_module.add_function(wrap_pyfunction!(directed_graph_density, algorithm_module)?)?; algorithm_module.add_function(wrap_pyfunction!(max_out_degree, algorithm_module)?)?; @@ -35,11 +56,20 @@ fn raphtory(py: Python<'_>, m: &PyModule) -> PyResult<()> { algorithm_module.add_function(wrap_pyfunction!(min_in_degree, algorithm_module)?)?; algorithm_module.add_function(wrap_pyfunction!(pagerank, algorithm_module)?)?; algorithm_module.add_function(wrap_pyfunction!( - weakly_connected_components, + global_clustering_coefficient, algorithm_module )?)?; + algorithm_module.add_function(wrap_pyfunction!( - temporal_three_node_motif, + temporally_reachable_nodes, + algorithm_module + )?)?; + algorithm_module.add_function(wrap_pyfunction!( + local_clustering_coefficient, + algorithm_module + )?)?; + algorithm_module.add_function(wrap_pyfunction!( + weakly_connected_components, algorithm_module )?)?; algorithm_module.add_function(wrap_pyfunction!( @@ -47,21 +77,30 @@ fn raphtory(py: Python<'_>, m: &PyModule) -> PyResult<()> { algorithm_module )?)?; algorithm_module.add_function(wrap_pyfunction!( - global_temporal_three_node_motif_from_local, + global_temporal_three_node_motif_multi, + algorithm_module + )?)?; + algorithm_module.add_function(wrap_pyfunction!( + local_temporal_three_node_motifs, algorithm_module )?)?; + algorithm_module.add_function(wrap_pyfunction!(hits, algorithm_module)?)?; + algorithm_module.add_function(wrap_pyfunction!(balance, algorithm_module)?)?; m.add_submodule(algorithm_module)?; + + //GRAPH LOADER let graph_loader_module = PyModule::new(py, "graph_loader")?; graph_loader_module.add_function(wrap_pyfunction!(lotr_graph, graph_loader_module)?)?; + graph_loader_module.add_function(wrap_pyfunction!(neo4j_movie_graph, graph_loader_module)?)?; + graph_loader_module.add_function(wrap_pyfunction!(stable_coin_graph, graph_loader_module)?)?; graph_loader_module.add_function(wrap_pyfunction!( reddit_hyperlink_graph, graph_loader_module )?)?; - graph_loader_module.add_function(wrap_pyfunction!(neo4j_movie_graph, graph_loader_module)?)?; - graph_loader_module.add_function(wrap_pyfunction!(stable_coin_graph, graph_loader_module)?)?; m.add_submodule(graph_loader_module)?; + //GRAPH GENERATOR let graph_gen_module = PyModule::new(py, "graph_gen")?; graph_gen_module.add_function(wrap_pyfunction!(random_attachment, graph_gen_module)?)?; graph_gen_module.add_function(wrap_pyfunction!( @@ -70,10 +109,11 @@ fn raphtory(py: Python<'_>, m: &PyModule) -> PyResult<()> { )?)?; m.add_submodule(graph_gen_module)?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; + // TODO: re-enable + //VECTORS + // let vectors_module = PyModule::new(py, "vectors")?; + // vectors_module.add_class::()?; + // m.add_submodule(vectors_module)?; Ok(()) } diff --git a/python/tests/data/network_traffic_edges.csv b/python/tests/data/network_traffic_edges.csv new file mode 100644 index 0000000000..aa5cfa1563 --- /dev/null +++ b/python/tests/data/network_traffic_edges.csv @@ -0,0 +1,10 @@ +timestamp,source,destination,data_size_MB,transaction_type,is_encrypted +2023-09-01T08:00:00Z,ServerA,ServerB,5.6,Critical System Request,True +2023-09-01T08:05:00Z,ServerA,ServerC,7.1,File Transfer,False +2023-09-01T08:10:00Z,ServerB,ServerD,3.2,Standard Service Request,True +2023-09-01T08:15:00Z,ServerD,ServerE,8.9,Administrative Command,False +2023-09-01T08:20:00Z,ServerC,ServerA,4.5,Critical System Request,True +2023-09-01T08:25:00Z,ServerE,ServerB,6.2,File Transfer,False +2023-09-01T08:30:00Z,ServerD,ServerC,5.0,Standard Service Request,True +2023-09-01T08:31:00Z,ServerD,ServerC,10.0,Standard Service Request,True +2023-09-01T08:32:00Z,ServerD,ServerC,15.0,Standard Service Request,True diff --git a/python/tests/data/network_traffic_vertices.csv b/python/tests/data/network_traffic_vertices.csv new file mode 100644 index 0000000000..4191ee20b1 --- /dev/null +++ b/python/tests/data/network_traffic_vertices.csv @@ -0,0 +1,8 @@ +timestamp,server_id,server_name,hardware_type,OS_version,primary_function,uptime_days +2023-09-01T08:00:00Z,ServerA,Alpha,Blade Server,Ubuntu 20.04,Database,120 +2023-09-01T08:01:00Z,ServerA,Alpha,Blade Server,Ubuntu 20.04,Database,121 +2023-09-01T08:02:00Z,ServerA,Alpha,Blade Server,Ubuntu 20.04,Database,122 +2023-09-01T08:05:00Z,ServerB,Beta,Rack Server,Red Hat 8.1,Web Server,45 +2023-09-01T08:10:00Z,ServerC,Charlie,Blade Server,Windows Server 2022,File Storage,90 +2023-09-01T08:15:00Z,ServerD,Delta,Tower Server,Ubuntu 20.04,Application Server,60 +2023-09-01T08:20:00Z,ServerE,Echo,Rack Server,Red Hat 8.1,Backup,30 \ No newline at end of file diff --git a/python/tests/expected/dataframe_output/edge_df_all.json b/python/tests/expected/dataframe_output/edge_df_all.json new file mode 100644 index 0000000000..aa0fd10414 --- /dev/null +++ b/python/tests/expected/dataframe_output/edge_df_all.json @@ -0,0 +1 @@ +{"src":{"0":"ServerA","1":"ServerA","2":"ServerB","3":"ServerC","4":"ServerD","5":"ServerD","6":"ServerE"},"dst":{"0":"ServerB","1":"ServerC","2":"ServerD","3":"ServerA","4":"ServerC","5":"ServerE","6":"ServerB"},"layer":{"0":"Critical System Request","1":"File Transfer","2":"Standard Service Request","3":"Critical System Request","4":"Standard Service Request","5":"Administrative Command","6":"File Transfer"},"properties":{"0":{"datasource":"data\/network_traffic_edges.csv","is_encrypted":true,"data_size_MB":[[1693555200000,5.6]]},"1":{"is_encrypted":false,"datasource":"data\/network_traffic_edges.csv","data_size_MB":[[1693555500000,7.1]]},"2":{"is_encrypted":true,"datasource":"data\/network_traffic_edges.csv","data_size_MB":[[1693555800000,3.2]]},"3":{"datasource":"data\/network_traffic_edges.csv","is_encrypted":true,"data_size_MB":[[1693556400000,4.5]]},"4":{"datasource":"data\/network_traffic_edges.csv","is_encrypted":true,"data_size_MB":[[1693557000000,5.0],[1693557060000,10.0],[1693557120000,15.0]]},"5":{"datasource":"data\/network_traffic_edges.csv","is_encrypted":false,"data_size_MB":[[1693556100000,8.9]]},"6":{"datasource":"data\/network_traffic_edges.csv","is_encrypted":false,"data_size_MB":[[1693556700000,6.2]]}},"update_history":{"0":[1693555200000],"1":[1693555500000],"2":[1693555800000],"3":[1693556400000],"4":[1693557000000,1693557060000,1693557120000],"5":[1693556100000],"6":[1693556700000]}} \ No newline at end of file diff --git a/python/tests/expected/dataframe_output/edge_df_exploded.json b/python/tests/expected/dataframe_output/edge_df_exploded.json new file mode 100644 index 0000000000..ab848f74f9 --- /dev/null +++ b/python/tests/expected/dataframe_output/edge_df_exploded.json @@ -0,0 +1 @@ +{"src":{"0":"ServerA","1":"ServerA","2":"ServerB","3":"ServerC","4":"ServerD","5":"ServerD","6":"ServerD","7":"ServerD","8":"ServerE"},"dst":{"0":"ServerB","1":"ServerC","2":"ServerD","3":"ServerA","4":"ServerC","5":"ServerC","6":"ServerC","7":"ServerE","8":"ServerB"},"layer":{"0":"Critical System Request","1":"File Transfer","2":"Standard Service Request","3":"Critical System Request","4":"Standard Service Request","5":"Standard Service Request","6":"Standard Service Request","7":"Administrative Command","8":"File Transfer"},"properties":{"0":{"datasource":"data\/network_traffic_edges.csv","is_encrypted":true,"data_size_MB":[[1693555200000,5.6]]},"1":{"is_encrypted":false,"datasource":"data\/network_traffic_edges.csv","data_size_MB":[[1693555500000,7.1]]},"2":{"is_encrypted":true,"datasource":"data\/network_traffic_edges.csv","data_size_MB":[[1693555800000,3.2]]},"3":{"datasource":"data\/network_traffic_edges.csv","is_encrypted":true,"data_size_MB":[[1693556400000,4.5]]},"4":{"is_encrypted":true,"datasource":"data\/network_traffic_edges.csv","data_size_MB":[[1693557000000,5.0]]},"5":{"is_encrypted":true,"datasource":"data\/network_traffic_edges.csv","data_size_MB":[[1693557060000,10.0]]},"6":{"is_encrypted":true,"datasource":"data\/network_traffic_edges.csv","data_size_MB":[[1693557120000,15.0]]},"7":{"datasource":"data\/network_traffic_edges.csv","is_encrypted":false,"data_size_MB":[[1693556100000,8.9]]},"8":{"datasource":"data\/network_traffic_edges.csv","is_encrypted":false,"data_size_MB":[[1693556700000,6.2]]}},"update_history":{"0":1693555200000,"1":1693555500000,"2":1693555800000,"3":1693556400000,"4":1693557000000,"5":1693557060000,"6":1693557120000,"7":1693556100000,"8":1693556700000}} \ No newline at end of file diff --git a/python/tests/expected/dataframe_output/edge_df_exploded_no_hist.json b/python/tests/expected/dataframe_output/edge_df_exploded_no_hist.json new file mode 100644 index 0000000000..b56c7b33d4 --- /dev/null +++ b/python/tests/expected/dataframe_output/edge_df_exploded_no_hist.json @@ -0,0 +1 @@ +{"src":{"0":"ServerA","1":"ServerA","2":"ServerB","3":"ServerC","4":"ServerD","5":"ServerD","6":"ServerD","7":"ServerD","8":"ServerE"},"dst":{"0":"ServerB","1":"ServerC","2":"ServerD","3":"ServerA","4":"ServerC","5":"ServerC","6":"ServerC","7":"ServerE","8":"ServerB"},"layer":{"0":"Critical System Request","1":"File Transfer","2":"Standard Service Request","3":"Critical System Request","4":"Standard Service Request","5":"Standard Service Request","6":"Standard Service Request","7":"Administrative Command","8":"File Transfer"},"properties":{"0":{"datasource":"data\/network_traffic_edges.csv","is_encrypted":true,"data_size_MB":[[1693555200000,5.6]]},"1":{"is_encrypted":false,"datasource":"data\/network_traffic_edges.csv","data_size_MB":[[1693555500000,7.1]]},"2":{"datasource":"data\/network_traffic_edges.csv","is_encrypted":true,"data_size_MB":[[1693555800000,3.2]]},"3":{"datasource":"data\/network_traffic_edges.csv","is_encrypted":true,"data_size_MB":[[1693556400000,4.5]]},"4":{"is_encrypted":true,"datasource":"data\/network_traffic_edges.csv","data_size_MB":[[1693557000000,5.0]]},"5":{"is_encrypted":true,"datasource":"data\/network_traffic_edges.csv","data_size_MB":[[1693557060000,10.0]]},"6":{"is_encrypted":true,"datasource":"data\/network_traffic_edges.csv","data_size_MB":[[1693557120000,15.0]]},"7":{"datasource":"data\/network_traffic_edges.csv","is_encrypted":false,"data_size_MB":[[1693556100000,8.9]]},"8":{"is_encrypted":false,"datasource":"data\/network_traffic_edges.csv","data_size_MB":[[1693556700000,6.2]]}}} \ No newline at end of file diff --git a/python/tests/expected/dataframe_output/edge_df_exploded_no_prop_hist.json b/python/tests/expected/dataframe_output/edge_df_exploded_no_prop_hist.json new file mode 100644 index 0000000000..c960e1cd30 --- /dev/null +++ b/python/tests/expected/dataframe_output/edge_df_exploded_no_prop_hist.json @@ -0,0 +1 @@ +{"src":{"0":"ServerA","1":"ServerA","2":"ServerB","3":"ServerC","4":"ServerD","5":"ServerD","6":"ServerD","7":"ServerD","8":"ServerE"},"dst":{"0":"ServerB","1":"ServerC","2":"ServerD","3":"ServerA","4":"ServerC","5":"ServerC","6":"ServerC","7":"ServerE","8":"ServerB"},"layer":{"0":"Critical System Request","1":"File Transfer","2":"Standard Service Request","3":"Critical System Request","4":"Standard Service Request","5":"Standard Service Request","6":"Standard Service Request","7":"Administrative Command","8":"File Transfer"},"properties":{"0":{"data_size_MB":5.6,"is_encrypted":true,"datasource":"data\/network_traffic_edges.csv"},"1":{"datasource":"data\/network_traffic_edges.csv","is_encrypted":false,"data_size_MB":7.1},"2":{"data_size_MB":3.2,"is_encrypted":true,"datasource":"data\/network_traffic_edges.csv"},"3":{"data_size_MB":4.5,"is_encrypted":true,"datasource":"data\/network_traffic_edges.csv"},"4":{"is_encrypted":true,"data_size_MB":5.0,"datasource":"data\/network_traffic_edges.csv"},"5":{"is_encrypted":true,"data_size_MB":10.0,"datasource":"data\/network_traffic_edges.csv"},"6":{"data_size_MB":15.0,"is_encrypted":true,"datasource":"data\/network_traffic_edges.csv"},"7":{"datasource":"data\/network_traffic_edges.csv","data_size_MB":8.9,"is_encrypted":false},"8":{"data_size_MB":6.2,"is_encrypted":false,"datasource":"data\/network_traffic_edges.csv"}},"update_history":{"0":1693555200000,"1":1693555500000,"2":1693555800000,"3":1693556400000,"4":1693557000000,"5":1693557060000,"6":1693557120000,"7":1693556100000,"8":1693556700000}} \ No newline at end of file diff --git a/python/tests/expected/dataframe_output/edge_df_exploded_no_props.json b/python/tests/expected/dataframe_output/edge_df_exploded_no_props.json new file mode 100644 index 0000000000..3d7debc056 --- /dev/null +++ b/python/tests/expected/dataframe_output/edge_df_exploded_no_props.json @@ -0,0 +1 @@ +{"src":{"0":"ServerA","1":"ServerA","2":"ServerB","3":"ServerC","4":"ServerD","5":"ServerD","6":"ServerD","7":"ServerD","8":"ServerE"},"dst":{"0":"ServerB","1":"ServerC","2":"ServerD","3":"ServerA","4":"ServerC","5":"ServerC","6":"ServerC","7":"ServerE","8":"ServerB"},"layer":{"0":"Critical System Request","1":"File Transfer","2":"Standard Service Request","3":"Critical System Request","4":"Standard Service Request","5":"Standard Service Request","6":"Standard Service Request","7":"Administrative Command","8":"File Transfer"},"update_history":{"0":1693555200000,"1":1693555500000,"2":1693555800000,"3":1693556400000,"4":1693557000000,"5":1693557060000,"6":1693557120000,"7":1693556100000,"8":1693556700000}} \ No newline at end of file diff --git a/python/tests/expected/dataframe_output/edge_df_no_hist.json b/python/tests/expected/dataframe_output/edge_df_no_hist.json new file mode 100644 index 0000000000..6c16f9c08a --- /dev/null +++ b/python/tests/expected/dataframe_output/edge_df_no_hist.json @@ -0,0 +1 @@ +{"src":{"0":"ServerA","1":"ServerA","2":"ServerB","3":"ServerC","4":"ServerD","5":"ServerD","6":"ServerE"},"dst":{"0":"ServerB","1":"ServerC","2":"ServerD","3":"ServerA","4":"ServerC","5":"ServerE","6":"ServerB"},"layer":{"0":"Critical System Request","1":"File Transfer","2":"Standard Service Request","3":"Critical System Request","4":"Standard Service Request","5":"Administrative Command","6":"File Transfer"},"properties":{"0":{"is_encrypted":true,"datasource":"data\/network_traffic_edges.csv","data_size_MB":[[1693555200000,5.6]]},"1":{"is_encrypted":false,"datasource":"data\/network_traffic_edges.csv","data_size_MB":[[1693555500000,7.1]]},"2":{"is_encrypted":true,"datasource":"data\/network_traffic_edges.csv","data_size_MB":[[1693555800000,3.2]]},"3":{"datasource":"data\/network_traffic_edges.csv","is_encrypted":true,"data_size_MB":[[1693556400000,4.5]]},"4":{"datasource":"data\/network_traffic_edges.csv","is_encrypted":true,"data_size_MB":[[1693557000000,5.0],[1693557060000,10.0],[1693557120000,15.0]]},"5":{"is_encrypted":false,"datasource":"data\/network_traffic_edges.csv","data_size_MB":[[1693556100000,8.9]]},"6":{"datasource":"data\/network_traffic_edges.csv","is_encrypted":false,"data_size_MB":[[1693556700000,6.2]]}}} \ No newline at end of file diff --git a/python/tests/expected/dataframe_output/edge_df_no_prop_hist.json b/python/tests/expected/dataframe_output/edge_df_no_prop_hist.json new file mode 100644 index 0000000000..6aeab277eb --- /dev/null +++ b/python/tests/expected/dataframe_output/edge_df_no_prop_hist.json @@ -0,0 +1 @@ +{"src":{"0":"ServerA","1":"ServerA","2":"ServerB","3":"ServerC","4":"ServerD","5":"ServerD","6":"ServerE"},"dst":{"0":"ServerB","1":"ServerC","2":"ServerD","3":"ServerA","4":"ServerC","5":"ServerE","6":"ServerB"},"layer":{"0":"Critical System Request","1":"File Transfer","2":"Standard Service Request","3":"Critical System Request","4":"Standard Service Request","5":"Administrative Command","6":"File Transfer"},"properties":{"0":{"is_encrypted":true,"datasource":"data\/network_traffic_edges.csv","data_size_MB":5.6},"1":{"is_encrypted":false,"datasource":"data\/network_traffic_edges.csv","data_size_MB":7.1},"2":{"is_encrypted":true,"datasource":"data\/network_traffic_edges.csv","data_size_MB":3.2},"3":{"datasource":"data\/network_traffic_edges.csv","data_size_MB":4.5,"is_encrypted":true},"4":{"datasource":"data\/network_traffic_edges.csv","data_size_MB":15.0,"is_encrypted":true},"5":{"datasource":"data\/network_traffic_edges.csv","is_encrypted":false,"data_size_MB":8.9},"6":{"datasource":"data\/network_traffic_edges.csv","is_encrypted":false,"data_size_MB":6.2}},"update_history":{"0":[1693555200000],"1":[1693555500000],"2":[1693555800000],"3":[1693556400000],"4":[1693557000000,1693557060000,1693557120000],"5":[1693556100000],"6":[1693556700000]}} \ No newline at end of file diff --git a/python/tests/expected/dataframe_output/edge_df_no_props.json b/python/tests/expected/dataframe_output/edge_df_no_props.json new file mode 100644 index 0000000000..fd8e7d395f --- /dev/null +++ b/python/tests/expected/dataframe_output/edge_df_no_props.json @@ -0,0 +1 @@ +{"src":{"0":"ServerA","1":"ServerA","2":"ServerB","3":"ServerC","4":"ServerD","5":"ServerD","6":"ServerE"},"dst":{"0":"ServerB","1":"ServerC","2":"ServerD","3":"ServerA","4":"ServerC","5":"ServerE","6":"ServerB"},"layer":{"0":"Critical System Request","1":"File Transfer","2":"Standard Service Request","3":"Critical System Request","4":"Standard Service Request","5":"Administrative Command","6":"File Transfer"},"update_history":{"0":[1693555200000],"1":[1693555500000],"2":[1693555800000],"3":[1693556400000],"4":[1693557000000,1693557060000,1693557120000],"5":[1693556100000],"6":[1693556700000]}} \ No newline at end of file diff --git a/python/tests/expected/dataframe_output/vertex_df_all.json b/python/tests/expected/dataframe_output/vertex_df_all.json new file mode 100644 index 0000000000..f01ebb4df1 --- /dev/null +++ b/python/tests/expected/dataframe_output/vertex_df_all.json @@ -0,0 +1 @@ +{"id":{"0":"ServerA","1":"ServerB","2":"ServerC","3":"ServerD","4":"ServerE"},"properties":{"0":{"server_name":"Alpha","hardware_type":"Blade Server","datasource":"data\/network_traffic_edges.csv","primary_function":[[1693555200000,"Database"],[1693555260000,"Database"],[1693555320000,"Database"]],"uptime_days":[[1693555200000,120],[1693555260000,121],[1693555320000,122]],"OS_version":[[1693555200000,"Ubuntu 20.04"],[1693555260000,"Ubuntu 20.04"],[1693555320000,"Ubuntu 20.04"]]},"1":{"datasource":"data\/network_traffic_edges.csv","hardware_type":"Rack Server","server_name":"Beta","uptime_days":[[1693555500000,45]],"OS_version":[[1693555500000,"Red Hat 8.1"]],"primary_function":[[1693555500000,"Web Server"]]},"2":{"server_name":"Charlie","datasource":"data\/network_traffic_edges.csv","hardware_type":"Blade Server","OS_version":[[1693555800000,"Windows Server 2022"]],"primary_function":[[1693555800000,"File Storage"]],"uptime_days":[[1693555800000,90]]},"3":{"datasource":"data\/network_traffic_edges.csv","server_name":"Delta","hardware_type":"Tower Server","uptime_days":[[1693556100000,60]],"OS_version":[[1693556100000,"Ubuntu 20.04"]],"primary_function":[[1693556100000,"Application Server"]]},"4":{"server_name":"Echo","hardware_type":"Rack Server","datasource":"data\/network_traffic_edges.csv","primary_function":[[1693556400000,"Backup"]],"OS_version":[[1693556400000,"Red Hat 8.1"]],"uptime_days":[[1693556400000,30]]}},"update_history":{"0":[1693555200000,1693555260000,1693555320000,1693555500000,1693556400000],"1":[1693555200000,1693555500000,1693555800000,1693556700000],"2":[1693555500000,1693555800000,1693556400000,1693557000000,1693557060000,1693557120000],"3":[1693555800000,1693556100000,1693557000000,1693557060000,1693557120000],"4":[1693556100000,1693556400000,1693556700000]}} \ No newline at end of file diff --git a/python/tests/expected/dataframe_output/vertex_df_no_hist.json b/python/tests/expected/dataframe_output/vertex_df_no_hist.json new file mode 100644 index 0000000000..c166f0e4d4 --- /dev/null +++ b/python/tests/expected/dataframe_output/vertex_df_no_hist.json @@ -0,0 +1 @@ +{"id":{"0":"ServerA","1":"ServerB","2":"ServerC","3":"ServerD","4":"ServerE"},"properties":{"0":{"datasource":"data\/network_traffic_edges.csv","server_name":"Alpha","hardware_type":"Blade Server","OS_version":[[1693555200000,"Ubuntu 20.04"],[1693555260000,"Ubuntu 20.04"],[1693555320000,"Ubuntu 20.04"]],"uptime_days":[[1693555200000,120],[1693555260000,121],[1693555320000,122]],"primary_function":[[1693555200000,"Database"],[1693555260000,"Database"],[1693555320000,"Database"]]},"1":{"datasource":"data\/network_traffic_edges.csv","hardware_type":"Rack Server","server_name":"Beta","primary_function":[[1693555500000,"Web Server"]],"uptime_days":[[1693555500000,45]],"OS_version":[[1693555500000,"Red Hat 8.1"]]},"2":{"hardware_type":"Blade Server","server_name":"Charlie","datasource":"data\/network_traffic_edges.csv","uptime_days":[[1693555800000,90]],"OS_version":[[1693555800000,"Windows Server 2022"]],"primary_function":[[1693555800000,"File Storage"]]},"3":{"server_name":"Delta","hardware_type":"Tower Server","datasource":"data\/network_traffic_edges.csv","uptime_days":[[1693556100000,60]],"OS_version":[[1693556100000,"Ubuntu 20.04"]],"primary_function":[[1693556100000,"Application Server"]]},"4":{"datasource":"data\/network_traffic_edges.csv","hardware_type":"Rack Server","server_name":"Echo","uptime_days":[[1693556400000,30]],"OS_version":[[1693556400000,"Red Hat 8.1"]],"primary_function":[[1693556400000,"Backup"]]}}} \ No newline at end of file diff --git a/python/tests/expected/dataframe_output/vertex_df_no_prop_hist.json b/python/tests/expected/dataframe_output/vertex_df_no_prop_hist.json new file mode 100644 index 0000000000..1f072ff239 --- /dev/null +++ b/python/tests/expected/dataframe_output/vertex_df_no_prop_hist.json @@ -0,0 +1 @@ +{"id":{"0":"ServerA","1":"ServerB","2":"ServerC","3":"ServerD","4":"ServerE"},"properties":{"0":{"server_name":"Alpha","datasource":"data\/network_traffic_edges.csv","hardware_type":"Blade Server","uptime_days":122,"OS_version":"Ubuntu 20.04","primary_function":"Database"},"1":{"primary_function":"Web Server","server_name":"Beta","OS_version":"Red Hat 8.1","hardware_type":"Rack Server","datasource":"data\/network_traffic_edges.csv","uptime_days":45},"2":{"primary_function":"File Storage","server_name":"Charlie","datasource":"data\/network_traffic_edges.csv","hardware_type":"Blade Server","OS_version":"Windows Server 2022","uptime_days":90},"3":{"primary_function":"Application Server","OS_version":"Ubuntu 20.04","server_name":"Delta","datasource":"data\/network_traffic_edges.csv","uptime_days":60,"hardware_type":"Tower Server"},"4":{"primary_function":"Backup","hardware_type":"Rack Server","OS_version":"Red Hat 8.1","datasource":"data\/network_traffic_edges.csv","uptime_days":30,"server_name":"Echo"}},"update_history":{"0":[1693555200000,1693555260000,1693555320000,1693555500000,1693556400000],"1":[1693555200000,1693555500000,1693555800000,1693556700000],"2":[1693555500000,1693555800000,1693556400000,1693557000000,1693557060000,1693557120000],"3":[1693555800000,1693556100000,1693557000000,1693557060000,1693557120000],"4":[1693556100000,1693556400000,1693556700000]}} \ No newline at end of file diff --git a/python/tests/expected/dataframe_output/vertex_df_no_props.json b/python/tests/expected/dataframe_output/vertex_df_no_props.json new file mode 100644 index 0000000000..d388c1fde7 --- /dev/null +++ b/python/tests/expected/dataframe_output/vertex_df_no_props.json @@ -0,0 +1 @@ +{"id":{"0":"ServerA","1":"ServerB","2":"ServerC","3":"ServerD","4":"ServerE"},"update_history":{"0":[1693555200000,1693555260000,1693555320000,1693555500000,1693556400000],"1":[1693555200000,1693555500000,1693555800000,1693556700000],"2":[1693555500000,1693555800000,1693556400000,1693557000000,1693557060000,1693557120000],"3":[1693555800000,1693556100000,1693557000000,1693557060000,1693557120000],"4":[1693556100000,1693556400000,1693556700000]}} \ No newline at end of file diff --git a/python/tests/notebook.ipynb b/python/tests/notebook.ipynb index 2b65c528bb..fe85c71777 100644 --- a/python/tests/notebook.ipynb +++ b/python/tests/notebook.ipynb @@ -2,13 +2,14 @@ "cells": [ { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "import matplotlib.pyplot as plt\n", - "import seaborn as sns" + "import seaborn as sns\n", + "import tempfile" ] }, { @@ -24,24 +25,13 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": { "pycharm": { "name": "#%%\n" } }, - "outputs": [ - { - "data": { - "text/plain": [ - "Graph(number_of_edges=0, number_of_vertices=0, earliest_time=0, latest_time=0)" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "from raphtory import Graph\n", "g = Graph()\n", @@ -69,26 +59,9 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "True True False\n", - "True False\n", - "1 3\n", - "True True False\n", - "True False\n", - "2 5\n", - "Vertex(name=Ben, properties={_id : Ben})\n", - "Edge(source=Haaroon, target=Hamza, earliest_time=7, latest_time=7, properties={property2 : 9.8, First-Met : 01/01/1990, property1 : 1, property3 : test})\n", - "Graph(number_of_edges=3, number_of_vertices=6, earliest_time=1, latest_time=8)\n", - "True\n" - ] - } - ], + "outputs": [], "source": [ "# Basic Addition with integer IDs\n", "g.add_vertex(timestamp=1,id=10)\n", @@ -99,7 +72,7 @@ "# checking edge 1,2 exists and 2,1 doesn't as Raphtory is directed\n", "print(g.has_edge(1,2),g.has_edge(2,1))\n", "# Check the total number of edges and vertices\n", - "print(g.num_edges(),g.num_vertices())\n", + "print(g.count_edges(),g.count_vertices())\n", "\n", "# Adding vertices and edges with String IDs\n", "g.add_vertex(timestamp=5,id=\"Ben\")\n", @@ -108,43 +81,33 @@ "# Performing the same checks as before, but with strings\n", "print(g.has_vertex(id=\"Ben\"), g.has_vertex(id=\"Hamza\"), g.has_vertex(id=\"Dave\"))\n", "print(g.has_edge(src=\"Hamza\",dst=\"Ben\"),g.has_edge(src=\"Ben\",dst=\"Hamza\"))\n", - "print(g.num_edges(),g.num_vertices())\n", + "print(g.count_edges(),g.count_vertices())\n", "\n", "#Add an edge with Temporal Properties which can change over time\n", - "g.add_edge(timestamp=7,src=\"Haaroon\",dst=\"Hamza\",properties={\"property1\": 1, \"property2\": 9.8, \"property3\": \"test\"})\n", + "e = g.add_edge(timestamp=7,src=\"Haaroon\",dst=\"Hamza\",properties={\"property1\": 1, \"property2\": 9.8, \"property3\": \"test\"})\n", "#Add a static property which is immutable\n", - "g.add_edge_properties(src=\"Haaroon\",dst=\"Hamza\",properties={\"First-Met\":\"01/01/1990\"})\n", + "e.add_constant_properties(properties={\"First-Met\":\"01/01/1990\"})\n", "\n", "#Add an vertex with Temporal Properties which can change over time\n", - "g.add_vertex(timestamp=5,id=\"Hamza\",properties= {\"property1\": 5, \"property2\": 12.5, \"property3\": \"test2\"})\n", + "v = g.add_vertex(timestamp=5,id=\"Hamza\",properties= {\"property1\": 5, \"property2\": 12.5, \"property3\": \"test2\"})\n", "#Add a static property which is immutable\n", - "g.add_vertex_properties(id=\"Hamza\",properties={\"Date-of-Birth\":\"01/01/1990\"})\n", + "v.add_constant_properties(properties={\"Date-of-Birth\":\"01/01/1990\"})\n", "print(g.vertex(\"Ben\").__repr__())\n", "print(g.edge(\"Haaroon\",\"Hamza\").__repr__())\n", "print(g.__repr__())\n", - "g.save_to_file(\"/tmp/graph\")\n", + "with tempfile.NamedTemporaryFile() as g_path:\n", + " g.save_to_file(g_path.name)\n", "\n", - "loaded_graph = Graph.load_from_file(\"/tmp/graph\")\n", + " loaded_graph = Graph.load_from_file(g_path.name)\n", "\n", - "print(loaded_graph.has_vertex(\"Hamza\"))" + " print(loaded_graph.has_vertex(\"Hamza\"))" ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "from raphtory import graph_loader\n", "\n", @@ -153,47 +116,26 @@ "g.add_vertex(timestamp=0,id=\"Gandalf\",properties={\"Race\":\"Maiar\"})\n", "\n", "#view[\"Gandalf\"][\"Race\"]\n", - "from raphtory import vis\n", + "from raphtory import export\n", "\n", - "vis.to_networkx(view, k=20)" + "export.to_networkx(view)" ] }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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1157rSQpJyfHffgtLy9PV155pXuZvLw8NW/e/KK27XBUz4vMcBke53r9PqEyxw2XUaW1OJ2uauuttqN3/+xd8u/+6Z3e/UVZb61VpVJh6YEHHig15nK5lJOTo19//VUPP/zwRRcmSePGjVNeXp4WL17sHvv+++8lSddcc42uuOIK1a9fXxkZGe6wVFBQoB07dnhcagAAAKCyKhWWytoDYrVaFRsbq9TUVPXu3fuiC5OkP//5zxo+fLjmzJmjO++8U9nZ2Zo4caJuv/12NWvWTJLUv39/paenKzIyUpdddpmmT5+u6Ohode/evUpqAAAA/q1SYemPV9iuTjfffLNmzZqlBQsW6I033lCDBg10xx136LHHHnMvM2rUKDkcDqWlpamoqEjJyclauHChAgMDa6RGAADg2y7qnKUvv/xS33zzjQoKChQZGal27dqpU6dOlb6/qVOnlhq77bbbdNttt5W7TkBAgJ544gk98cQTld4uAABAeSoVloqLizV8+HBt3LhRAQEBioiI0PHjxzV//nx16NBB8+fP56tGAACAT6jUZ+xmz56trKwsTZs2Td999502btyo7du368UXX9S2bdv0+uuvV3WdAAAAXlGpsPThhx9qxIgRuvPOO91famuz2XTXXXdpxIgRWrt2bZUWCQAA4C2VCkvHjh1Tq1atypxr1aqVcnNzL6ooAACA2qJSYenKK69UVlZWmXNbtmwp9V1tAAAAdVWlTvC+//77NXXqVIWEhKhnz5665JJLdOTIEX344Yd64403NGLEiKquEwAAwCsqFZb69OmjHTt2KD09XS+//LJ73DAM9erVS0OHDq2yAgEAALyp0pcOmDx5sgYNGqRvvvlGJ06ckMVi0S233OK+sjYAAIAvqNA5S7t371bv3r311ltvSZKaNWumPn36qG/fvnrllVc0evRoZWdnV0uhAAAA3nDBYengwYN64IEHdOTIEcXExHjMBQYGaty4ccrPz1ffvn35NBwAAPAZFxyWFixYoD/96U96//331aNHD4+50NBQPfTQQ1q5cqWCg4M1f/78Ki8UAADAGy44LG3atElDhgxRZGRkucs0btxYgwYN0tdff10lxQEAAHjbBYelvLw8NW3a9LzLxcbGKicn52JqAgAAqDUuOCxFRkYqLy/vvMsdP35cDRs2vKiiAAAAaosLDkvJyclatWrVeZdb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zn8aLigoSP3791d6eroiIyN12WWXafr06YqOjlb37t29XR4AAKjj6nxYkqRRo0bJ4XAoLS1NRUVFSk5O1sKFCxUYGOjt0gAAQB1X5z8NBwAAUJ3q/DlLAAAA1YmwBAAAYIKwBAAAYIKwBAAAYIKwBAAAYIKwBAAAYIKwBAAAYIKw5GPy8/P13HPPqXPnzkpKSlKfPn2UmZnpnt+0aZPuvvtutW3bVj169NBHH33kxWqrT3Z2thITE7Vq1Sr32M6dO9W/f38lJCSoW7duevvtt71YYfVYvXq1/vKXvyguLk49e/bU3//+d/fcwYMHlZqaqqSkJN14442aNWuWnE6nF6utOg6HQ6+88opuuukmJSYmql+/ftq2bZt73lef+/nz52vAgAEeY+fr1eVy6dVXX1WnTp2UkJCghx9+WL/88ktNll0lyur9888/V+/evZWYmKhu3brppZdeUlFRkXvebrdrwoQJ6tixoxITEzVmzBgdO3aspku/aGX1/kdpaWnq1q2bx5gvP+95eXkaPXq02rdvr+uuu67M5/Wdd97RzTffrPj4ePXt21c7duyo2IYN+JSBAwcat99+u7Flyxbjp59+MiZMmGDEx8cbP/74o7Fv3z4jLi7OmDFjhrFv3z7jzTffNFq1amX885//9HbZVaq4uNi4++67jdjYWOO9994zDMMwjh07Zlx33XXG+PHjjX379hkrV6404uLijJUrV3q52qqzevVqo1WrVsayZcuMAwcOGHPnzjVatGhhbN261SguLja6d+9uDB061Ni9e7exfv16IyUlxXjllVe8XXaVePXVV40bbrjB+Oqrr4z9+/cbzzzzjNGuXTsjNzfXZ5/7ZcuWGS1atDD69+/vHruQXmfPnm1cd911xhdffGHs3LnTGDRokNG9e3fDbrd7o41KKav3LVu2GC1btjRef/11Izs72/jHP/5hdO7c2Xjqqafcyzz11FPGLbfcYmzZssXYvn27cddddxn9+vXzRguVVlbvf7R+/XojNjbWuOmmmzzGffV5t9vtRs+ePY3/+Z//MX744Qdj27Ztxl/+8hdjyJAh7mVWrVplxMfHG2vWrDH27t1rPPHEE0ZKSopx9OjRC942YcmH7N+/34iNjTUyMzPdYy6Xy7jllluMWbNmGc8++6xxzz33eKwzevRoY9CgQTVdarV6+eWXjQceeMAjLM2bN8+48cYbjZKSEo/lunfv7q0yq5TL5TJuuukmY+rUqR7jgwYNMubNm2esXbvWaNOmjZGfn++e++tf/2okJSXVqV+W5bnzzjuNF1980X375MmTRmxsrLFu3Tqfe+5zcnKM1NRUIyEhwejRo4fHG8f5erXb7UZiYqLxzjvvuOdPnDhhxMfHG2vXrq25JirJrPcxY8YYDz30kMfy77//vtG6dWvDbrcbOTk5RosWLYx//OMf7vmffvrJiI2NNbZu3VpjPVSWWe/n5ObmGh06dDD69+/vEZZ8+Xl/7733jISEBOPw4cPusS+//NK4+eabjZMnTxqGYRjdu3c3pk2b5p4vKSkxunTpYsybN++Ca+AwnA+JiIjQggULFBcX5x6zWCyyWCwqKChQZmamOnbs6LFOhw4dlJWVJcNHvvVmy5YtWrFihaZOneoxnpmZqZSUFNlsv38dYocOHbR//34dOXKkpsusctnZ2fr11191xx13eIwvXLhQqampyszMVOvWrdWwYUP3XIcOHXTq1Cnt3Lmzpsutco0aNdIXX3yhgwcPyul0asWKFQoKClKLFi187rn/4YcfFBgYqA8++EBt27b1mDtfr7t27dLp06c9fg+Eh4erVatW2rJlS431UFlmvQ8aNEhPPvmkx5jValVJSYlOnTqlrKwsSWcfj3NiYmIUFRVV53uXJMMw9NRTT+m///u/lZKS4jHny8/7xo0b1aFDB11yySXusU6dOmnDhg2qX7++jh49qv3793v0brPZ1L59+wr1TljyIeHh4erSpYuCgoLcY+vWrdOBAwfUqVMn5eTkKDo62mOdJk2aqLCwUMePH6/pcqtcQUGBxo0bp7S0NF166aUec+X1Lkm//fZbjdVYXbKzsyVJZ86c0eDBg9WxY0fde++9+vzzzyX5fv/PPPOMAgMDdfPNNysuLk4zZ87Uq6++qiuvvNLneu/WrZtmz56tK664otTc+XrNycmRpFL/Ppo0aeKeq83Mem/VqpVatGjhvl1SUqLFixerTZs2ioyMVG5uriIiIhQcHOyxni/0LkmLFy/W4cOHNXr06FJzvvy8Z2dn6/LLL9drr72mW2+9VTfddJOeffZZFRQUSKq63glLPmzr1q0aP368unfvrq5du6qoqMgjSEly3y4uLvZGiVXqhRdeUGJiYqm9K5LK7P3cL0273V4j9VWnU6dOSZKefPJJ3X777Vq0aJFuuOEGDR8+XJs2bfL5/vft26cGDRrotdde04oVK3T33Xdr7Nix2rlzp8/3/kfn67WwsFCSylzGlx4Lh8OhcePGae/evXr++eclSYWFhaX6lnyj9127dmnOnDmaPn16mT368vN+6tQprV69Wrt379bLL7+siRMnKisrS8OHD5dhGFXWu+38i6Au2rBhg8aOHaukpCSlp6dLOvvi+M9QdO52aGhojddYlVavXq3MzEytXbu2zPmQkJBSvZ/7hxIWFlbt9VW3wMBASdLgwYPVq1cvSVLLli21Y8cOvfXWWz7d/2+//aYxY8Zo8eLFat++vSQpLi5O+/bt0+zZs3269/90vl5DQkIknf13f+7nc8vU9d8B55w6dUqPPfaYvvnmG82ZM0fx8fGSyn5spLrfu91u19ixYzVs2DCPPWt/5MvPu81mU1hYmF5++WX378GGDRvq3nvv1ffff+/R+x9VtHf2LPmgZcuWaeTIkbrppps0b9489/8sL730UuXl5Xksm5eXp7CwMDVo0MAbpVaZ9957T0ePHlXXrl2VmJioxMRESdLzzz+vIUOGKDo6uszeJSkqKqrG661q53qIjY31GL/mmmt08OBBn+5/+/btKikp8ThXT5Latm2rAwcO+HTv/+l8vZ47FFHWMr7wWOTl5bkvG7Fw4UJ16dLFPRcdHa38/PxSb5p1vfft27dr7969mjNnjvt33/z583Xo0CElJiYqMzPTp5/36OhoxcTEuIOSJF177bWSzl4upap6Jyz5mOXLl2vSpEnq16+fZsyY4bHrsX379vrmm288lt+8ebOSkpJktdbtl0J6ero+/vhjrV692v1HkkaNGqXJkycrOTlZWVlZHtcV2rx5s2JiYtSoUSMvVV11WrdurXr16mn79u0e43v27NGVV16p5ORk7dixw324Tjrbf7169cr932hdce4cnd27d3uM79mzR02bNvX55/6PztdrixYtVL9+fWVkZLjnCwoKtGPHDiUnJ3uj5Cpz4sQJPfjggzp27JjeeeedUv20a9dOLpfLfaK3dPZ8l9zc3Drde3x8vD799FOtWbPG/bvv/vvvV5MmTbR69Wq1adPGp5/35ORk7dq1y+N6Wnv27JEkXXXVVWrUqJFiYmI8enc4HMrMzKxQ73X7HRIesrOzNWXKFN16661KTU3VkSNHdPjwYR0+fFgnT57UgAED9N133yk9PV0//vijFi1apE8++URDhgzxdukXLSoqSldddZXHH+nsp6SioqLUu3dvnTp1Ss8884z27dunVatWafHixUpNTfVy5VUjJCREQ4YM0WuvvaYPP/xQP//8s15//XV9/fXXGjhwoG655RY1btxYjz32mHbt2qUNGzZoxowZGjRoUJnnONQl8fHxateunZ588klt3rxZ+/fv16xZs7Rp0yYNHTrU55/7Pzpfr0FBQerfv7/S09P12WefadeuXXr88ccVHR2t7t27e7n6i/Piiy/ql19+0fTp0xUZGen+3Xf48GE5nU5FRUWpZ8+eSktLU0ZGhr777juNHj1aKSkpSkhI8Hb5lRYSElLqd1/Dhg1ls9l01VVXKSQkxKef9/vvv18BAQEaM2aM9u7dq6ysLKWlpem6665T69atJZ39pORbb72l999/X/v27dPTTz+toqIi3XPPPRe8Hc5Z8iHr1q1TSUmJ1q9fr/Xr13vM9erVS1OnTtXcuXM1ffp0LVmyRJdffrmmT59e6nICvqhRo0Z68803NXnyZPXq1UuNGzfWuHHj3Of3+ILhw4crNDRUM2fOVG5urpo1a6bZs2fruuuukyS9+eabmjBhgu677z41bNhQffv21fDhw71c9cWzWq16/fXXNWvWLI0fP14nTpxQbGysFi9e7P6Ysa8/9+dcyOt81KhRcjgcSktLU1FRkZKTk7Vw4UKPwxh1jdPp1Mcff6ySkhI9+OCDpeY/++wzXX755Zo0aZKmTJmiESNGSJI6d+6stLS0mi7XK3zxeZekyMhIvfPOO3rxxRd17733KigoSLfccoueeuop9zL33XefTp48qVmzZik/P19t2rTRW2+9pcjIyAvejsXwlQvsAAAAVAMOwwEAAJggLAEAAJggLAEAAJggLAEAAJggLAEAAJggLAEAAJggLAEAAJggLAGo9VatWqXmzZvr4MGD3i4FgB8iLAEAAJggLAEAAJggLAGoVVwul+bOnauuXbuqbdu2Gj58uE6cOOGxzJ49e5SamqqkpCQlJSXpkUce0S+//OKxzI8//qiHH35YSUlJuv766zVz5kyNHz9eAwYMcC/TvHlzzZkzR3fffbfi4+M1Z84cSdKhQ4fcX7Latm1bPfjgg9qxY4fH/dvtdk2bNk1dunRRmzZtdMcdd+jjjz+upkcFgDfx3XAAapWXXnpJb7/9toYNG6a2bdvq73//uz744AOVlJTos88+U0lJiXr37q2rr75aqampcjgcev3113Xs2DGtWbNGjRo10rFjx9SzZ081atRII0eOlNPp1CuvvKJDhw4pISFBS5culXQ2LAUGBmrMmDGKiYnRZZddpkaNGumuu+5SaGioRowYodDQUC1ZskT/+te/tHLlSjVr1kyGYejhhx/W1q1bNWrUKDVr1kzr16/XihUr9NJLL+muu+7y7oMIoGoZAFBLnDhxwmjdurUxffp0j/HBgwcbsbGxxi+//GKMHj3auP76642TJ0+6548fP260a9fOmDp1qmEYhjFr1iwjLi7OyMnJcS9z8OBBo3Xr1kb//v3dY7GxscaDDz7osa0ZM2YYcXFxxsGDB91jdrvduPnmm42RI0cahmEYGzduNGJjY42PPvrIY92xY8caN9xwg1FSUnJxDwSAWoXDcABqjW3btqmkpEQ33XSTx/htt93m/nnz5s1KSUlRSEiIHA6HHA6H6tevr/bt2+uf//yne5nExERFRUW517vsssuUmJhYapstW7b0uL1p0ya1bNlSUVFR7vu3Wq3q3Lmz+/43bdoki8WiLl26uJdxOBzq1q2bDh8+rL1791bZYwLA+2zeLgAAzjl3blJERITHeOPGjd0/5+fn6+OPPy7z/KDIyEhJ0rFjx9S6detS85dccomOHDniMRYWFuZxOz8/XwcOHChzfUkqLCxUfn6+DMNQUlJSmcvk5eWVCmEA6i7CEoBa41xIOnr0qK6++mr3eH5+vvvnBg0a6Prrr9fAgQNLrW+znf2VFh0dXSoUnbvf82nQoIFSUlI0bty4MueDgoLUoEEDhYWF6e233y5zmauuuuq82wFQd3AYDkCtkZiYqJCQEH3yySce41988YX755SUFO3bt08tW7ZUXFyc4uLi1KZNGy1evFjr16+XJCUnJ2vbtm06fPiwe728vDxt27btvDWkpKQoOztbMTEx7vuPi4vTmjVrtHLlSgUEBCglJUVnzpyRYRgey+zZs0evvfaaHA5H1TwgAGoFwhKAWqNevXoaPny4li9frvT0dG3cuFGTJ0/2CEvDhw/Xzz//rNTUVG3YsEFfffWVRo4cqY8++kgtWrSQJD3wwAOqV6+eBg8erHXr1mndunV6+OGHVVJSIovFYlrDQw89JJfLpYceekgff/yxNm3apGeffVZLly5VTEyMJKlLly5KTk5215qRkaE33nhDL7zwgqxWq/twIADfwKUDANQ6S5cu1ZIlS5Sbm6vExETddttteuGFF/TZZ5/p8ssv1w8//KCZM2dq69atMgxDsbGxGjp0qG6++Wb3fezdu1eTJ0/Wt99+q3r16qlv377auHGj/vSnP2nevHmSzl46YMSIERo5cqTH9n/++We9/PLL2rRpk+x2u5o2baoBAwbonnvucS9z5swZvfLKK/rkk0909OhRRUVFqWfPnnrkkUcUHBxcMw8UgBpBWALgc7Zv3678/Hx16dLFPeZwONS1a1f17NlT48eP92J1AOoaTvAG4HMOHTqkxx9/XI888ohSUlJUWFioFStW6OTJk7rvvvu8XR6AOoY9SwB80v/93/9p+fLl+uWXXxQYGKi2bdvq0UcfVVxcnLdLA1DHEJYAAABM8Gk4AAAAE4QlAAAAE4QlAAAAE4QlAAAAE4QlAAAAE4QlAAAAE4QlAAAAE4QlAAAAE4QlAAAAE/8PRSIpLfC7S4IAAAAASUVORK5CYII=", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "from raphtory import graph_gen\n", "\n", - "g = Graph(4)\n", + "g = Graph()\n", "graph_gen.ba_preferential_attachment(g,vertices_to_add=1000,edges_per_step=10)\n", "view = g.window(0,1000)\n", "\n", "ids = []\n", "degrees = []\n", - "for v in view.vertices():\n", + "for v in view.vertices:\n", " ids.append(v.id)\n", " degrees.append(v.degree())\n", "\n", @@ -205,7 +147,7 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "metadata": { "pycharm": { "name": "#%%\n" @@ -222,30 +164,9 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAi4AAAHPCAYAAAB0ulFlAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAA9hAAAPYQGoP6dpAABgU0lEQVR4nO3dd3RU1RoF8D0lk947JQkQWuhFBBFQqkoRUBAVrAhIEQQU8NFEQVCqgNJB6aCCIAiCFBFpoXcSAqRAepn0TDnvj5CBmCCZMJObmezfWm/5cmdy+eYDcjfnnnOuTAghQERERGQB5FIXQERERFRSDC5ERERkMRhciIiIyGIwuBAREZHFYHAhIiIii8HgQkRERBaDwYWIiIgsBoMLERERWQwGFyIiIrIYSqkLMDUhBPR6024GLJfLTH5OS8eeFI99KYo9KR77UhR7UlRF6YlcLoNMJivRe60uuOj1AsnJmSY7n1Iph7u7I9TqLGi1epOd15KxJ8VjX4piT4rHvhTFnhRVkXri4eEIhaJkwYW3ioiIiMhiMLgQERGRxWBwISIiIovB4EJEREQWg8GFiIiILAaDCxEREVkMBhciIiKyGAwuREREZDEYXIiIiMhiMLgQERGRxWBwISIiIovB4EJEREQWg8GFiIiILIbkwSUuLg61a9cu8r9ffvlF6tKIiIjoPr1e4PC5GNyJTZe0DqWkvzqAa9euwdbWFvv374dM9uCR1s7OzhJWRURERA/bezISWw/dRL0gd4zp10SyOiQPLjdu3EBQUBB8fHykLoWIiIj+JTohA9cjU7H971sAgKdD/CStR/Lgcv36ddSoUUPqMoiIiCqUg2eicfxKHAZ2C4G3m32x77l8KxnztpyHXggAQL0gd7RuUMGDy40bN+Du7o4333wTt27dQmBgID788EO0bdu21OdUKk03dUehkBf6L7Enj8K+FMWeFI99KYo9KcqcPYmMS8eG/WHQ6QXW/H4N4/s3hUwmQ1JaDjbuv4GE1BwAwN3ETOiFQJC/Myp5OqJv+2DY2ChMXo8xZELcj1ES0Gq1aNy4MYKDgzF+/Hg4OTlh165dWL16NVavXo1WrVoZfU4hRKG5MkRERBVdijoH6sw8XLuTgtCrsYiISUN8Srbh9bpBHrC3VSIsKhXpWXmFvrdedU98MbgVbJTSBpYCkgYXAMjMzIRCoYCdnZ3h2MCBAwEAK1asMPp8Op0eanX2499YQgqFHC4u9lCrs6HT6U12XkvGnhSPfSmKPSke+1IUe1KUqXoScTcN01aHGm73FHCwVeL5ppWx69idQseD/JzxcptqUMjlsFHKUSfQDQq5eUfCXFzsSzyyJPmtIkdHxyLHatasib///rvU59RqTf+HXqfTm+W8low9KR77UhR7Ujz2pSj2pKgn7cmuf+5ALwRsbRTwdrNDi7q+cHOyRY3KLvDzcEBwZVdk5mgAAHYqJRpU9yg0uiL0gFZffn5PJA0uYWFheO211/D999/j6aefNhy/dOkSgoODJayMiIjI8iWrc3D6egIAYEL/pgjwLbrVSKNgr7Iu64lIOguqRo0aqF69OqZNm4bQ0FDcvHkTX331Fc6dO4cPP/xQytKIiIgs3l/n70IvBGpXdSs2tFgiSUdc5HI5lixZgjlz5mDUqFFQq9UICQnB6tWrUatWLSlLIyIisni37+9y26Ku9eyVJvkcFy8vL3z11VdSl0FERGR18jQ6AICDnY3ElZgOF8wTERFZKc39Sb0qG+u53FvPJyEiIqJCcjX3g0s52YPFFBhciIiIrFSeNv9WEUdciIiIqNwrmOPCERciIiIq9/I0nONCREREFiLv/uRcW4kfjGhKDC5ERERWSK8X0N5/xpGN0nou99bzSYiIiMigYGIuAKg44kJERETlWcH8FoAjLkRERFTOPVhRJIdcJpO4GtNhcCEiIrJCeYZdc63nNhHA4EJERGSVCua4WNNtIoDBhYiIyCo92MOFIy5ERERUzhXMcbHliAsRERGVd7kccSEiIiJLobHCBywCDC5ERERWybCqyIoesAgwuBAREVmlXA1HXIiIiMhCPNiAjiMuREREVM49WA5tXZd66/o0REREBADQcOdcIiIishS52gfPKrIm1vVpiIiICMBDc1w44kJERETlnWGOC0dciIiIqLzjiAsRERFZDMMGdFxVREREROVdnpb7uBAREZGF4D4uREREZDG4cy4RERFZjII5LracnEtERETlXR4fskhERESWomDExYb7uBAREVF5pheCzyoiIiIiy6C5v6IIAGw5OZeIiIjKM3VWHgBAIZfBhnNciIiIqDyLjMsAAFT2coRcJpO4GtNicCEiIrIykXHpAIAAX2eJKzE9BhciIiIrExWfP+JS1ddJ4kpMj8GFiIjIyty5P+ISyBEXIiIiKs/Ss/KQkp4LAKjqY30jLkqpCyAiIqKisnO1OBsaiYTkTOh1osjrcrkMTWt5w83JttDxyPu3iXzc7GFva32Xeev7REREROVcSnouEtOyDV8LAYRFpyL0egK09zeOS1LnICdP95/nOXzuLia+1Rw6vR77TkUhM0eLCzeTAAABftZ3mwhgcCEiIioTQghci0zF/tAonAtPhCg6iFJEZW8nVPZ2hNAXffOV28mIis/Ait+uIC4ly7AEGgDcnW3R45kgE1ZffjC4EBERmZleCCzZfgmh1xMMx7zd7ArtseLqqELrhv7wcrUHADja26BpiB9SU7MMozAPO3sjAQt/uYhT1+IBAM4ONni6ri/sbBXo/FQAnOxtzPyppMHgQkREZGa7/rmN0OsJUCpkeLZhJXRoVgWVvRz/83uUSjlk/7F5XJNa3njrhdoIi0qDg60SnVtUhbebvalLL3cYXIiIiMwo4q4a24/cAgAM6FwbbRpVMtm5n2tcGc81rmyy81kCLocmIiIyE70Q2LD/BgSAliG+Jg0tFRWDCxERkZmcvBKHiLtq2KoU6Ns+WOpyrAKDCxERkZmcvpE/Gbdz86pF9luh0mFwISIiMpPohEwAQK2qbtIWYkUYXIiIiMwgV6NDfHIWAKCKFW69LxUGFyIiIjO4m5gJAcDFwQaujiqpy7EaDC5ERERmEHX/mUEcbTEtBhciIiIziC4ILt4MLqbE4EJERGQG0QkMLubA4EJERGRiWp3ecKuoKm8VmRSDCxERkYmduhaPzBwtnB1sUNn7v59JRMZhcCEiIjIhIQR+Px4JAOjYvCqUCl5qTYndJCIiMqErt1MQnZABW5UC7ZtWrAcglgUGFyIiIhM6fiUWAPBMfT842tlIXI31YXAhIiIyEa1OjzM3EgEALer4SFyNdSpXweXWrVto0qQJfvnlF6lLISIiMtrlW8nIztXC1UmFmlXcpC7HKpWb4KLRaDB27FhkZWVJXQoREVGpnLgSBwBoXtsHcrlM4mqsU7kJLgsXLoSTE9e6ExGRZYqMSzcEl2fq+0lcjfUqF8Hl1KlT2Lx5M2bOnCl1KUREREYTQmDj/jAIAC3q+qCav4vUJVktyYOLWq3Gp59+iokTJ8Lf31/qcoiIiIwWFp2G61GpsFHK0ee5YKnLsWpKqQuYOnUqmjRpgu7du5vsnEql6fKY4v7GQQpuIGTAnhSPfSmKPSke+1KUpffk6KV7AIBW9fzg6+lgknNaek/MRdLgsn37doSGhmLnzp0mO6dcLoO7u+m3V3ZxsTf5OS0de1I89qUo9qR47EtRltiTrBwNTl2NBwB0bVPd5NcgS+yJOcmEEEKqX3zAgAE4c+YMVCqV4VhWVhZUKhWefvpprFixwuhz6nR6qNXZJqtRoZDDxcUeanU2dDq9yc5rydiT4rEvRbEnxWNfirLknhw+G4OVu67C39MBM4e0gkxmmtVEltwTY7m42Jd4ZEnSEZfZs2cjJyen0LHOnTvjo48+Qo8ePUp9Xq3W9L/BOp3eLOe1ZOxJ8diXotiT4rEvRVliTw6diwEAPNvAHzqdAGDa8QBL7Ik5SRpcfH19iz3u6en5yNeIiIjKi7uJmbgZo4ZcJuMS6DLCGT9ERESl9PfF/Em5DWt4wtXJVuJqKgbJVxX92/Xr16UugYiI6LESU7Px17m7AIA2DbmdR1nhiAsREZGRNFodFm+/hKxcLapXckHDYE+pS6owGFyIiIiMtHF/GO7EpsPJ3gYfvlwfCjkvp2Wl3N0qIiIiKq/0eoG9JyNx6NxdyAAM6h4CT1c7qcuqUBhciIiISkCn12POpnO4FpkKAOjeOgj1q/MWUVljcCEiIiqB0GsJuBaZClsbBfo8XwPPN6ksdUkVEoMLERHRYwghsOdkJADgxacD0L5pFYkrqrgYXIiIqMI7eyMBRy7cg/6hp+A4O9jg5dbV4OVmj/PhSbgTmw4bpRzPNeVIi5QYXIiIqELLzdNh1e6ryMzRFnnt9r10tGtcCVsOhgMA2jasBBcHVZH3UdlhcCEiogpHo9XjxJU4XLmTDHtbJTJztPBytUP31kEAACGAbUciEJOYiQ37wwAAzWp5o8/zNSSsmgAGFyIiqkAyczQ4dDYG+09HIy0jr9BrXVoEoE3DSoavK3k5Yu7mc3CwU6Jz86ro2Lwq5HLTPPmZSq9UwSUqKgp5eXmoUaMG0tPTMX/+fMTExOCFF15Az549TVwiERHRkxFCYF9oNLb9FYFcjQ4A4Oakgq+7A65HpcLRTonWDQo/JDG4sivmj3gWSoWcgaUcMTq4HD58GMOGDcOAAQMwbtw4TJ48GX/88Qdq1aqFCRMmQKPRoE+fPuaolYiIyGgZ2RpsOxKBg2diAABVvB3RpUUAng7xhVwuw/mwRHi72cNOVfSSqLJRlHW59BhGB5fvv/8ezz77LIYNGwa1Wo19+/Zh0KBBGDlyJObNm4cff/yRwYWIqILKztUi9Fo8tDq94ZhcLkOTWt5lOqn1XlImHOxscDYsAZv2hyFPm19P3+eD0aVFVchkD0ZQmtTyLrO66MkZHVyuXbuG77//Hk5OTvjtt9+g0+nQpUsXAEDr1q2xevVqkxdJRESWYeP+MPx98V6R43+dv4sJ/ZtBqTD/M31OX4/Hd9suAQAKFjcH+jrj5WeroXFNL7P/+mReRgcXW1tbaLX5S8b+/vtveHp6ok6dOgCAxMREuLi4mLZCIiKyCElpOTh2ORYA0DjYC4r780Ku3EnBrXvp+GHPNdQNdEeLur5mCzDJ6hys+f0axEPHeretjq6tAguNspDlMjq4NG3aFKtWrYJarcbevXvRq1cvAMClS5ewaNEiNG3a1ORFEhFR2cnJ0yImMdMwXOHv6QgHu/zLxY2oVETcVRf7fdciU6DTC9QNdMdHrzY0HD9+JRbLdlzB0YuxOHoxFjdj1BjQpTaEEEhIzUaeRg+FUg69XI4niTN3YtPx3faLyMzRItDPGUN61INWL1DZy/EJzkrljdHB5bPPPsOgQYMwZswYBAcH48MPPwQADB48GPb29hg7dqzJiyQiIvOKT8nCmRuJyMjW4PC5mEKbsdnbKtGvQzBOXY3HpVvJjz3XSy0DC33dMsQPqel5CItOxdmwRBw8GwN7WyXCY9JwIyq10Ht7tqmG7s8EGT06kpWjwTcbzyIrN38/liEv14Ovu4NR5yDLIBNCiMe/rTAhBJKSkuDl9eBe4blz5xASEgKVStodBXU6PZKTM012PqVSDnd3R6SkZEKr1T/+GyoA9qR47EtR7Enxyltfdh27je1HbkGnf3A5cHGwga1KgZw8HdKzNIbjCrkMjYO9HrnapqqPU5HJrw/b9GcY/jgVVeh8jnZKCMDw6ygVcvh52GNsvyZwcXz0NeVeUiZsbRTwcLHDn6ejsX7fDfh5OOB/bzWDo52NMS0ol8rbnxNz8vBwhKKEtw9LtY+LTCYrFFoAoHHjxqU5FRERSSg9Kw8/H44AANQNdIe3mx1qVnFDy3q+UMjlyNPosPy3Kzh9PQEtQ3zRs001+DzBSMYr7WpAqZAjJT0Xbk4qdGhWBR4udlAq5Th5PQFLf7kIjU6P6IRMLPn1EtydbRHo64zOLQIKnefUtXgs2X4JAkBwFVdk3A89zzetbBWhhR7N6OCSnJyM6dOn49ChQ8jOzsa/B2xkMhmuXLlisgKJiMh8wmPSAAD+ng745PUmRV5X2SgwrFcD5ObpYKt68j1NbJRyvPpc8dvmd2kZhAZB7rh1V405m8/hWmQqAOD45Tg0qOEJf8/8uSo3olKxfOcVwwTc8Oj8z6BUyNGqnl8xZyZrYnRwmTZtGg4ePIiuXbvCz88Pcrn5l7YREZF5hN2/6Nes4vaf7zNFaCkJe1slalV1w1tdamPj/jCobORIzcjD3pOReL1jLVy9nYKlOy9Dq9OjcbAX3uhUEyt2XsGN6DS0qOsDJ3uOtlg7o4PLX3/9hc8++wyvvfaaOeohIqIyFG4ILq4SV1JY6wb+aN3AH2HRqfhq3RkcuXAPRy7cQ8Egf91Adwx+uR5sbRQY+3oTXItMQY1K5eszkHkYPVxiY2ODqlWrmqMWIiIyo9jkLGz6MwzJ6hwAgEarw+3Y/KXN5S24FKhZxQ11AtwgRP4Tm10cVWjT0B8jX20I2/sThJUKOepX84S9LZ8bXBEY/bvcqVMn/Pbbb3jmmWfMUQ8REZWSEAIp6bkobq1oZo4GU1efApC/T8s7L9bFzRg1tDoBF0cVvN3sy7jakhveuwEi4zLg5+kANydbqcshiRkdXEJCQjB//nxERUWhUaNGsLOzK/S6TCbDsGHDTFYgERE9XnauFvO2nDdMtv0vJ6/G4+Vnq2PV7qsAgJBA93K9q6yDnQ3qBLpLXQaVE0bv41Kwvf8jTyiT4erVq09U1JPgPi7mx54Uj30pij0pnrF9CY9Jw4krcdDpHv3eW7HpuBObDpkMj9xO39/DAZHxGQAATxc7JKlz4ONmj/H9m0o+ksE/K0VVpJ6YdR+Xa9euGV0QERHly9PokKvRwcY2D5nZmkJPUc5/XY99oVG4GJEEIQC9XiA2OatE57ZTKTDujaYI9HN+5HsWb7uI09cTkKTOgZ1KgdH9GkseWoiM8UQzmW7evIn09HR4eHggICDg8d9ARFSB/XX+Ljbsu4E8I//1LAPQsp7vf25hL5MBzWr7oNJjnsvTONgLp68nAAB6ta0On3I8t4WoOKUKLr/99htmzZqFxMREwzEvLy+MGTMGPXv2NFVtRERFCCGQmpEHd2dpRgluRKXi/M1ECCNH7nM1Ohw6F1PsxNl/q+rjhK6tAuHskL/dvZernckmzzau6QVPFzv4uNujQ9MqJjknUVkyOrgcOHAAn3zyCVq2bInRo0fDy8sL8fHx2LFjByZMmAA3Nzc899xzZiiViCo6vRBY+utlnLoWj+caV8IbnWpBIS86qdQUE011ej0ysrX4/fgdw4TXPI0e0QkZT3Teto0q4Z2X6sDD3REpqcXPXVCYcWNPRzsbfP1hKwgA8nI8IZfoUYyenNunTx9UqVIF8+bNK/Laxx9/jNjYWGzcuNFkBRqLk3PNjz0pHvtSlKl6Ep+ShUNn7yI6MQOXIv776cQOtkp8+kYTBPg+ep5HcfRC4Pa9dGTmaHDobAzOhScWOzoil8nwdIgPXEsxL8TH3R5tG1aCSqXgn5V/4d+foipST8w6OffGjRsYMWJEsa/16tULI0eONPaURFRBpGXkYv3+MCSmZpf4ewSAmIQMaHUPUkTH5lVw9GIssnO1Rd6flavF3xfu4Y1Ojw4uer1AerYGkXHp+ONkJGQyGdKzNbgTm17kvYG+zujSoirsVPk/Lit7O5brPU+IrJ3RwcXd3R1pacXvE5CamgqV6tGPICeiiikzW4PYpCys2XOt2HBQEiFB7qhV1Q3BlV0REuSBPs8FIzuvcHC5fCsZy3dewbnwRLzesWaxt4w0Wh2++OF0sbd8bG0U8HK1g7+XI7q1CoSXqx3sbZXleo8ToorG6ODSqlUrLFq0CE899RT8/B48hfPevXtYvHgxWrdubdICiciyJaVl49Pv/0F6lgYA4GRvg7e61IaNsuTzOJwdVKjm71woQNgo5bBRFv6HUpOaXlAq5EhMy8HsTedgo5Tjg+4hcLR78OC9309EGkKLykaOto0qwdvVHnlaHdo0rAQXR/7ji6g8Mzq4jB49Gq+88go6d+6MJk2awMvLC4mJiTh79ixcXV0xZswYc9RJRCZw654a245EIEWdW+zrMhnQtJY3erSuBvn9Sa8Fq3j0eqOmwwEAFEo5NuwPQ3qWBrY2Cvi622NAl9qoUdk8z8WxUylRJ8ANl24l4+qdFADAmt3X8F7XutgXGoVLEcm4fX/EZ3CPeng6xNcsdRCR+RgdXLy9vbFt2zasWrUKp06dwqVLl+Dq6ooBAwbg3XffhZeXlznqJKJSytXo8P32S7hwM6lE749OyMTNmDS0rOcHG6Uce09G4ta90t3eKaBUyDDx7eao/Jg9RkyhUbAXLt3Kn8CrkMtw+kYCTt9IKPSeuoHuaFHXx+y1EJHplWofF09PT3zyySemroWITCQyLt2w2+qJK3GFQkuren5o3cCv2HkbsclZ2Lg/DJdvp+Dy7RTD8f/aRv5xlAo5+jxfo0xCCwC0buCHiLtqNAr2RHqWBuv33QAAeLvZoWurILg6qlCrqhvnrRBZqBIFl0WLFqFPnz7w9fXFokWL/vO9fMgikbSS0nLwxQ+h0D10a0chl2Hkqw1Ro7Ir7G0f/de+bqA7giu74p9L93DrXjp0Oj0CfJ3Ro3VQqZb/SrGc006lxAfdQwxfP1PfDzq9gIOdkvuWEFmBEgeXtm3bMrgQWYBT1+Kh0wu4OKpQydMBCrkMHZpXRf3qniX6/qo+TnitfU0zV1l2/iuoEZHlKdHf6IcfrMiHLBKVb6euxQEAXm4dhOe5pTsRWRmjb1ovWrQIcXFxxb4WHR2NadOmPXFRRFQyQgjcS8pEZFw6IuPSceV2Mm7dS89fHVSbk0+JyPoYPYa6ePFiw22jfzt//jy2bt2KyZMnm6Q4Ino0vRBYsv0SQq8nFHmtdlU3uHI/EiKyQiUKLv369cP58+cB5P8L77XXXnvkexs0aGCayogIAJCszsHRi/eQ96/JrbHJWTh9PQFymQzODg82WLNRyvFSq8CyLpOIqEyUKLh8+eWX2LNnD4QQWLx4MV555ZVCu+YCgFwuh4uLCzp37myWQokqoviULMzacBYp6cVvGAcA73eri1b1/B75OhGRNSlRcAkODsbw4cMB5K8aKlgaXUCr1UKp5Mx9IlMSQmDBTxeQkp4LXw8HNKjuUeQ9IYEeaFyTmz4SUcVhdNoYPnw4li1bhtDQUCxbtgwAcPr0aYwZMwZDhgxB//79TV4kUUWkzszDvaQsyGTAuDeawK0U+6gQEVkbo1cVrVq1CvPnz0dQUJDhWEBAAF544QXMnDkTW7duNWV9RBVWfGo2AMDD2Y6hhYjoPqNHXDZt2oRRo0Zh0KBBhmP+/v6YOHEivLy8sGbNGvTp08ekRRJVRPEp+cHFx91e4kqIiMoPo0dc4uLiHrlyqFGjRoiOjn7iooiIwYWIqDhGB5fKlSvj2LFjxb526tSpIquNiKh0Eu7fKvJxY3AhIipg9K2ivn374ptvvoFGo0HHjh3h6emJ5ORkHDx4EKtXr8aYMWPMUSdRhVMwx8WbwYWIyMDo4PLOO+8gLi4Oa9euxZo1awzHFQoF3n77bbz77rumrI+owuKtIiKiokq1+cq4ceMwdOhQnD17FmlpaXBxcUHDhg3h7u5u6vqIKqSsHC0ysjUAOOJCRPSwUu8a5+zsjLZt2xY5HhERgerVqz9RUUQVXcH8FhcHG9jbcnNHIqICRv9ETEtLw7x583Dy5Enk5eVBCAEgf5fPrKwspKWl4erVqyYvlMha5ebpEJeSZfhaCODw+bsAAG/eJiIiKsTo4DJjxgzs2rULbdq0QUREBOzt7REUFITTp09DrVZj2rRp5qiTyCrp9QJf/hiKmMTMYl9vVsunjCsiIirfjA4uR44cwYgRIzB48GCsWrUKJ0+exPz585GZmYn+/fsjPDzcHHUSWaS0zDzo9QJuTirIZLIir1+MSEJMYiYU8sJPeHZ2UKFH6yA0reVdluUSEZV7RgcXtVqNJk2aAABq1KiBVatWAQAcHR3x3nvvYdGiRZgwYYJpqyQq5xJTs5GerYFzai7S07ORk6vFruN3cOV2CgCgZYgvBvWoV+T7Dp6NAQB0bF4Fr7WvWaY1ExFZIqODi7u7O9LT0wEAQUFBSEpKQmpqKtzc3ODr64u4uDiTF0lUnkXFZ2DamlPQ6cUj33PqWjz6d64FB7sHoyoJqdm4eDMJAPBck8pmr5OIyBoYvXNuq1atsGTJEsTExCAgIACurq7Ytm0bAODgwYNcEk0Vzi+Hb0KnF3B2sIG/lyN83e3h426PprW8MXNwS/h7OkCnF7gQkR9S1Jl5yMrR4NC5GAgA9at5wNfdQdoPQURkIYwecfnoo4/w1ltvYdy4cVi3bh0GDx6MWbNmYcmSJVCr1Rg2bJg56iQqV3R6PY5ejEV4dBrO30yCXCbDxLebIyTYBykpmdBq9Yb3NqnpjXtJd3DySjwiYzOwLzQKTg420OnyR2ie52gLEVGJGR1cqlSpgt27d+P27dsAgHfffRdeXl44c+YMGjZsiF69epm6RqJy4+TVOPx5Ohop6blITMsxHH+2oR/8PR2L/Z7GNb2w+/gdnAtPNBxLy8gDAHi42KJhsKd5iyYisiJGB5f3338fAwcORKtWrQzHunfvju7du5eqgKSkJMycORNHjhxBbm4unnrqKYwbNw41atQo1fmIzCUzR4Mf9lxHdq4WAOBop0TbRpXg5GCD5xo/etSkur8L3J1tkZKeCx93e3RtGYgtB8ORmaNFu0aVoJAbfceWiKjCMjq4nDlzpthlnaU1bNgw6PV6LFu2DI6OjliwYAHeeecd/PHHH7C35+ZbVH7sORGJ7FwtKns74pW2NVCzqiscH5ps+yhyuQwf92mE6MQMNKvlAxulHFV8nHDmRgI6twgog8qJiKyH0f/Ua9OmDXbs2AGNRvPEv3haWhoqV66ML7/8Eg0bNkSNGjUwdOhQxMfHIyws7InPT2QqaZl52BcaBQDo3bY6Gtf0KlFoKVDFxwktQ/xgo8z/K1fN3wWvtKsBWxuFWeolIrJWRo+42NraYseOHfj9999Ro0YNODgUXg0hk8nwww8/lOhcrq6umDNnjuHr5ORkrFmzBn5+fggODja2NCKTEUIg4q4a/p6OcLBTYtex28jT6FHN3wWNg72kLo+IqMIyOrjExsYaNqADYHhW0aO+LqlJkyZhy5YtUKlU+P7774sEIiJz0mh1OBuWiNoB7tBodPhh73VcvpUMf08HDO1ZH4fubxTXu211k94qJSIi48hEaZOGiYWHhyMnJwfr16/H7t27sWHDBtSrV3Sn0cfR6fRQq7NNVpdCIYeLiz3U6mzodPrHf0MFYI09+eNkJNb9cQNKhQwKuRy5Gp3hNblMBr0QqBPghgkDmj0yuFhjX54Ue1I89qUo9qSoitQTFxd7KBQlm71S6uCSlpaG0NBQxMfHo0uXLkhNTUW1atWe+F+jer0e3bp1Q6NGjfDVV18Z/f1CCP6LmIz2zdpQ/HUuxvB1SDUPvNgqCPM3nYVOL1DFxwn/e7cFqvg4S1glEREZfasIAL7//nssXboUOTk5kMlkaNiwIebPn4+UlBSsWrUKLi4uJTpPcnIyjh07hi5dukCpzC9FLpcjODgY8fHxpSkNer2AWp1Vqu8tTkVKvCVljT0Ji8p/plDzOt5oWssbzzTwh1wmwyevN0FschaebeQPlVKOlJTin+IMWGdfnhR7Ujz2pSj2pKiK1BNjRlyMDi7r1q3DwoULMXjwYDz//PPo27cvAKB///749NNPsWDBAkyaNKlE50pMTMTo0aOxYsUKtGnTBgCg0Whw5coVtG/f3tjSDB7etdRUdDq9Wc5ryaylJ7l5OsQm5YfdNzvVhqujCnqdgB4Ctaq6oVZVNwAl/3NlLX0xJfakeOxLUexJUexJYUYvh167di0GDRqEkSNHFpqD0q5dO4waNQoHDhwo8blq1aqFtm3b4ssvv8SpU6dw48YNjB8/Hmq1Gu+8846xpRGVSlRCBgQAV0cVXB1VUpdDRET/wejgcvfuXbRo0aLY16pXr47ExMRiX3uUuXPnolWrVvj444/Rp08fpKamYv369ahUqZKxpRGVSmRc/tPOA3w5f4WIqLwz+laRv78/zp49i2eeeabIa5cuXYK/v79R53N2dsbUqVMxdepUY0shMonIuAwAQICvk8SVEBHR4xgdXF599VUsXLgQdnZ2eO655wAAWVlZ2Lt3L5YuXYp3333X1DUSmZxeLxCdkIGU9Fycv5k/SsgRFyKi8s/o4PLBBx8gOjoas2fPxuzZswEAb731FoD8hy0OHjzYtBUSmVhUfAZW7b6KO7HphmOeLraoF+QuYVVERFQSRgcXmUyGadOm4b333sPx48eRmpoKZ2dnPPXUU6hVq5Y5aiQyCSEEdh69jZ3/3IZOL6CykcPFQYWQIA/0eb4GHIx49hAREUnD6OCyaNEi9OnTB0FBQQgKCir0WnR0NFatWoXJkyebqj4ikzlzIxHb/74FAGhS0wsDutSGm5OtxFUREZExjF5VtHjxYsTFxRX72vnz57F169YnLorIHKLi828NPR3ii+G9GzC0EBFZoBKNuPTr1w/nz58HkD/c/tprrz3yvQ0aNDBNZUQmlpKeCwDw93TgYyGIiCxUiYLLl19+iT179kAIgcWLF+OVV16Bn59foffI5XK4uLigc+fOZimU6EkVBBd3Z460EBFZqhIFl+DgYAwfPhxA/uTcPn36wNfX16yFEZlaSkZ+cPFwtpO4EiIiKi2jJ+cWBJi0tDRkZ2dDry/6/ATuekvlUYo6P7i4ccSFiMhiGR1cIiMj8emnnxrmvBTn6tWrT1QUkanl5GmRlasFAHgwuBARWSyjg8u0adNw+/ZtDB8+HH5+fpDLjV6YRFTmCua32KkUsLc1+o89ERGVE0b/BD916hSmT5+Obt26maMeIrNI5cRcIiKrYPRwiZOTE1xdXc1RC5HZJKcXTMxlcCEismRGB5eXX34Z69evhxDCHPUQmUXBrSJOzCUismxG3yqyt7fH6dOn0alTJzRo0AB2doWXlspkMsyYMcNkBRKZwoM9XLgUmojIkhkdXLZt2wZnZ2fo9fpiVxZxR1Iqj1J4q4iIyCoYHVwOHDhgjjqITOrWPTVCr8cjNT0XdQM9cDUyBQDg7W4vcWVERPQkuC6UrIpGq8e2IxHYeyISBbOwjl3OfyhonQA31A1wl644IiJ6YiUKLosWLSrxCWUyGYYNG1bqgoiMlZ2rhZ1KgeiETCzfeRnRCZkAgOa1vaGyUeCfS7FwdVJhcI96kMt5K5OIyJIxuJBFC70Wj++2X4K/pwMSUrOh1Qk4O9jgnRfroElNbwDAiy0D4WxvAxdHlcTVEhHRkypRcLl27Zq56yAqlaMX7wEA7iVlAQCa1PTC2y/UKRRSKns5SlIbERGZHue4kMXS6fW4HpUKAGjdwA8NqnviqTo+XNlGRGTFGFzIYt26l46cPB0c7ZR496W6kDOwEBFZPT4hkSzW1dvJAIC6ge4MLUREFQSDC1msK7fz92apG+QhcSVERFRWGFzIIumFwK1YNQCgdlU3aYshIqIy80TBJT09HTdv3kReXh50Op2paiJ6rITUbORp9LBRyuHn4SB1OUREVEZKFVxOnDiBPn36oEWLFujevTvCwsIwZswYzJw509T1ERUrOj5/k7lKno7cVI6IqAIxOrgcO3YM77//Puzs7DB27FgIkb+xep06dfDjjz9i9erVJi+S6N9iEjIAAFW8uUcLEVFFYnRwmT9/Pjp06IC1a9fi7bffNgSXIUOGYODAgdi6davJiyT6t+j7waWyt5PElRARUVkyOrhcvXoVr7zyCgAU2eirdevWiImJMU1lRP+h4HlEVXw44kJEVJEYHVycnZ2RkJBQ7Gv37t2Ds7PzExdF9F/yNDrEpeRv8V+FIy5ERBWK0cGlQ4cOmDdvHi5evGg4JpPJEBsbiyVLluC5554zZX1ERdxLyoIQgJO9DVz54EQiogrF6C3/x4wZg/Pnz6Nv377w8vICAIwePRqxsbHw9/fH6NGjTV4k0cOu3MnfMTfI35nPJSIiqmCMDi6urq7YunUrtm/fjuPHjyM1NRXOzs4YMGAAevfuDXt7e3PUSWRwNiwRANA42EviSoiIqKyV6iGLKpUKffv2Rd++fU1dD1VwFyOSkKfRoVltn2JfV2fl4WZ0GgAGFyKiisjo4LJo0aJHviaXy+Hg4IDAwEC0bt0aKhXnH1DJ5ObpcPJaHFbvvgYZgNnDWsPd2bbI+86HJ0IACPR1hoeLXZnXSURE0jI6uOzYsQOxsbHIy8uDUqmEm5sbUlNTodVqIZPJDPu6BAcH48cff4SHBx+AR/9t/b4b+PN0tOFrAeB2rBruzt6F3peszsHOo7cBAI1rcrSFiKgiMnpV0ciRI6FSqTB37lxcuHABf//9Ny5evIhFixbB3d0d8+fPx86dOyGTyTB37lxz1ExWJD41GwfO5IeWh+fZ3olNL/Q+jVaPuVvOIzEtBz7u9mjftHJZlklEROWE0cFl4cKFGDVqFF566SXI5fnfLpPJ0LFjR3z00UdYsGABatasiSFDhuDw4cMmL5isy96TkRACqFfNA0vHPofXO9QEAETGZRR635ELd3E3MRMujiqM7dcYzg68DUlEVBEZHVzu3buHwMDAYl+rXLmyYedcX19fpKWlPVl1ZNUi49Lx94V7AICuLQOhVMgR6Je/gWFk/IMRlzyNDjv/uQ0A6NE6CF6uXLlGRFRRGR1cgoODH/k8op9++gnVqlUDANy+fRs+PsWvDCGKjs/A3M3noNHqERLkjtoBbgCAqj75O+Emq3ORnpUHANh/OhppGXnwdLFDm4aVpCqZiIjKAaMn544YMQLDhg1Dr1690LlzZ3h6eiIxMRH79+/H9evX8e233+LKlSv45ptvDM80oopJnZWHXf/cQXauttBxnV6PU9cSoNXpEeDrhKE9Gxg2krO3VcLX3R5xKdm4eVeNQF9nw4Tcnm2qwUZpdNYmIiIrYnRwee6557By5UosXLgQixYtgk6ng1KpRLNmzfDDDz+gefPmOHDgALp27YpRo0aZoWSyFH9fuId9oVGPfL1BdU8M7FYXDnaF/xgG+jkjLiUb3/50AUqFHFqdHjUqu6BVfT9zl0xEROVcqTaga9myJVq2bIm8vDykpaXB09PTMFEXANq3b4/27dubrEiyTMnqHABA3UB31KtWeFm8j5s9mtX2LnbL/k5PVUVschYi4zKg1elho5Sjf6fakHN7fyKiCq9UwSU3NxfXr19HXl4ehBC4ffs29Ho9srOzERoairFjx5q6TrJA6sz8OSqNa3qhU/OqJf6+GpVcMfXdFsjI1iArRwNHexs42tmYq0wiIrIgRgeXEydOYOTIkY9cMeTo6MjgQgAAdZYGAEr9BGcnexs42TOwEBHRA0YHl3nz5sHd3R1ffPEFduzYAblcjt69e+Ovv/7Cxo0bsXz5cnPUSRaoYMTFhXuuEBGRiRgdXK5fv44vv/wSnTp1Qnp6OjZt2oR27dqhXbt20Gg0+P7777Fs2TJz1EoWpiC4OJdyxIWIiOjfjF5bqtfr4evrCwAIDAxEWFiY4bUuXbrgypUrpquOLJZGq0fW/WXQpb1VRERE9G9GB5eAgABcv34dAFCtWjVkZ2cjIiICAKDVapGZmWnaCskiFWwep5DLiix3JiIiKi2jg0v37t0xe/ZsrFu3Dh4eHqhfvz6++OILHDhwAIsXL0ZwcLA56iQLk1Zwm8jBhsuYiYjIZIwOLgMHDkS/fv1w/vx5AMCUKVNw9epVDB06FBEREfj0009NXiRZHsPEXN4mIiIiEzJ6DP/WrVsYN26c4esGDRpg//79iIiIQPXq1eHk5GTSAskyqbMYXIiIyPSMHnF54403sH379kLHnJyc0LBhQ4YWMigYcXHlUmgiIjIho4OLjY0N3N3dzVELWRF1Zv7mcxxxISIiUzL6VtHIkSPx9ddfIz09HXXq1IGDg0OR91SqVMkkxZHlKrhV5MwRFyIiMiGjg8vUqVOh0+nwySefPPI9V69efaKiyPIZbhVxxIWIiEzI6ODy5ZdfmqMOsjKpGbkAeKuIiIhMy+jg0qtXL3PUQVYkJjET95KyIJfJUNnbUepyiIjIipRqS9O8vDz89NNP+Oeff5CQkIAZM2bg5MmTqFevHho2bGjqGsnCHD4XAwBoFOwJNydbiashIiJrYnRwSU5Oxttvv23YtyU8PBw5OTk4dOgQZs6ciTVr1qBJkybmqJXKsSMX7uLanVQAwLnwBABAu8aVJayIiIiskdHLob/++mtkZmZi9+7d2LZtG4QQAIBvv/0WDRo0wLfffmvU+VJTUzF58mS0bdsWTZs2xeuvv47Q0FBjyyIJCCEghEBGtgZrdl/DscuxOHY5Ftm5Oni52qF+NQ+pSyQiIitj9IjLwYMH8dlnnyEwMBA6nc5w3NbWFu+99x7Gjx9v1PlGjx6NhIQEzJ07F56enli7di3ef/99bNu2DdWrVze2PCpDP+y5juOXYtHn+RoQyF9B1KVFAID820RyOZ9RREREpmX0iEtubi7c3NyKfU2hUECj0ZT4XHfu3MHRo0cxdepUNG/eHNWqVcOkSZPg4+ODnTt3GlsalaFkdQ4OnYlBVq4W24/cAgDUquqGF54OwAtPB8Dfk5NyiYjI9IwOLg0aNMCGDRuKfW3nzp2oX79+ic/l7u6OZcuWoUGDBoZjMpkMMpkMarXa2NKoDB0IjYL+/m3CgidBB/k7S1kSERFVAEYHl5EjR+Lo0aN4+eWXsWDBAshkMvz2228YMmQI9uzZg2HDhpX4XC4uLmjXrh1Uqgd7fezduxd37txBmzZtjC2NyogQAvtP3ilyPMjPRYJqiIioIjF6jkvz5s2xevVqzJkzBytWrIAQAmvWrEFISAiWLl2Kli1blrqYM2fOYMKECejcuTOee+65Up9HqTQ6jz2SQiEv9F8CDp6JQUxCJmxtFFDZyJGelX97sEZlV5P23tLwz0pR7Enx2Jei2JOi2JPiyUTBsqBSyMnJQVpaGpycnODo+GRzGvbv34+xY8eiadOm+P7772FrW7r9P4QQkMk4KdRcbkSmYNyiv6HV6fFut3q4GZ2Kv87FoJKXI5ZO6Ch1eUREZOWMHnHp2bMnevbsiW7dusHLywt2dnZPXMS6deswffp0vPDCC5g1a1ahW0fG0usF1OqsJ66pgEIhh4uLPdTqbOh0epOd1xKlZ+XhqzUnodXp0bK+Hzo0rQRHlRx/nYtB3UB3pKRkSl2ipPhnpSj2pHjsS1HsSVEVqScuLvYlHlkyOrhUqlQJc+bMwTfffIOWLVuiZ8+e6NSpU6kDzIYNG/DFF19gwIAB+N///meS0RKt1vS/wTqd3iznLUt6IfDHySgkpmUXOl6rqhta1PV97Pcu2X4ZiWk58HG3x8h+TaHJyUPTWt6Y9HZzVPJ0tPj+mIo1/FkxNfakeOxLUexJUexJYUYHl++++w7p6enYu3cvdu/ejfHjx2PKlCno1KkTXn75ZbRq1arE4ePWrVuYMWMGOnXqhMGDByMxMdHwmp2dHZyduUrFlC7cTMKWg+FFjh88E4Pq/i7wcrM3HMvT6KDVPbiLuD80ChcjkmCjlOOjVxvCyd4GKTn5q4mq+XNSLhERlY1SPavI2dkZr776Kl599VUkJSVhz5492LNnDz744AN4eXnh8OHDJTrP3r17odFosG/fPuzbt6/Qa7169cLMmTNLU16FpdXpcfVOSqFkrlDIUTfQHTZKOY5fjgUA1A10R80qrgCAMzcSEZ2Qgb8u3EPvtvkb/oVei8eSXy8bljs/rH/nWgjwZaAkIiJplCq4PCwpKQmJiYlQq9XQ6XRwdXUt8fcOGTIEQ4YMedIS6L51f9zAX+fvFjke5OeMkX0a4WxY/ohWn+drGJYuV/Z2wvfbL+HIhbvo/FRVONnb4PcTkUVCi1wmQ5cWVdGmYSXzfxAiIqJHKFVwiYqKwm+//Ybdu3cjPDwcXl5e6NatG2bNmoU6deqYukYqgQs3k/DX+buQAaheyQW4f7fubmImbsemY9qaU9Bo9fDzcEDgQyMmTWp6wdnBBmkZefhowRHUCXDDrXtqKOQyfP3hM3B2sDG8V8kleUREJDGjg8srr7yCK1euwM7ODp06dcL48ePRqlUryOX5FzUuRy57mTkarPn9KgCgY/OqeL1jTcNrN6JSMXvTWaSk5wIAWjfwK/T7o1TI0bVVELYcCIdeCFyLTAUANA72grtz6ZakExERmYvRwcXNzQ0zZ85E586dYW//YDJnfHw8tmzZgp9//hkHDx40aZH03zbsC0NqRh58PRzQu13hB1PWquqGz99rgRtRqbBTKdGstneR7+/8VFV0bF4FZ28k4rttFyEAPNvQv4yqJyIiKjmjg8vKlSsLfX3kyBFs2rQJhw8fhlarRZUqVUxWHD1eeHQajl2OhUwGvN+1LmxtFEXe4+/p+NiHHsplMjSr7Y0Pe9ZHfGo2GtbwNFfJREREpVaqOS7Jycn46aefsGXLFsTExMDJyQm9evXCyy+/jObNm5u6RvoPl24lAQCa1/ZBcOWST4x+lOZ1fJ74HEREROZiVHA5fvw4Nm/ejP3790On06FZs2aIiYnB4sWL0aJFC3PVSP/h5t38p2jXDnCTthAiIqIyUKLgsmbNGmzevBm3bt1CYGAghg4dil69esHBwQEtWrTgZFyJ6PUCEXfTAAA1Kj35aAsREVF5V6LgMnPmTNSuXRs//vhjoZGV9PR0sxVGj3c3MRPZuTrY2ihQxefJHnJJRERkCUq0MUfXrl1x584dDB48GEOHDsW+ffug1WrNXRs9Rvj90ZZq/s5QyLnHChERWb8SjbjMmTMHGRkZ2LlzJ3755ReMGDEC7u7u6NixI2QyGW8VSSQ8Oj+4BFfhbSIiIqoYSvzPdCcnJ7z++uvYunUrdu7ciZdffhkHDhyAEAKfffYZFixYgPDwog/wI/PQ6vS4cDN/RVGdAHeJqyEiIiobpbq/ULNmTYwfPx6HDx/GwoULUb16dSxfvhzdu3dHjx49TF0jFeNaZAoysjVwdrDhiiIiIqownughi0qlEp06dUKnTp2QmJiIbdu2Ydu2baaqjf7DqavxAIBmtX04v4WIiCqMJ346dAEvLy988MEH+OCDD0x1SipGWHQqlu64bHj2UAtuGEdERBWIyYILlY0/TkYhWZ0fWvw8HFCrqpu0BREREZUhBhcLkp2rxYWI/Am5I19tiJAgd8jlXNFFREQVB4OLBTkfngiNVg9fd3s0rOHJZehERFThMLiUM3ohkJqeCyEKH8/O1WL/6WgAwFN1fRlaiIioQmJwKUeEEPj2pwuG/VmKo1TI0KqebxlWRUREVH4wuJQjRy7cM4QWpaLwEmeZDKhV1Q292lSHvyefS0RERBUTg0s5kazOweYDYQCA19oHo0uLAIkrIiIiKn+4c1k5IITAj3uvIztXh+qVXNCpeVWpSyIiIiqXGFzKgVPX4nHhZhKUChnefakulzgTERE9AoNLOXAuPBEA0LF5VVT24vwVIiKiR2FwKQcSU3MAAEF+zhJXQkREVL4xuJQDCWnZAABvN3uJKyEiIirfGFwklqfRIS0jDwDg5WoncTVERETlG4OLxJLU+beJbFUKONnbSFwNERFR+cbgIrGE+/NbvF3tuI0/ERHRYzC4SCzx/vwWL1fObyEiInocBheJFawo8nLj/BYiIqLHYXCRmGFFEUdciIiIHovBRWIccSEiIio5BhcJ6YVAQipHXIiIiEqKwUVCd2LTkZWrha1KAV8PB6nLISIiKvcYXCR0NiwBANCgmgdslPytICIiehxeLSV0Niz/4YpNanpLXAkREZFlUEpdQEV09U4KDp+LQUxCJuQyGRrU8JS6JCIiIovA4FLG0rPy8N22i8jM0QIA6ga5c6t/IiKiEmJwKWNbDoYjM0eLSl6OaN+0Mm8TERERGYHBpQylpOfi6MVYAMA7L9ZBcGVXiSsiIiKyLJycW4aiEzIAAJW9HBlaiIiISoHBpQzdS8oCAPh5cs8WIiKi0mBwKUOxSZkAAD9uNkdERFQqDC5lKDY5f8TFnyMuREREpcLgUoYKbhX5ezpKXAkREZFlYnApI1k5WqRl5gHgrSIiIqLSYnApIwW3iVydVLC35Sp0IiKi0mBwKSP37k/M9edoCxERUakxuJQBIQTO3Mh/ErS/F+e3EBERlRbvWZhRSnouZm86CzuVErfuqaGQy9CuUSWpyyIiIrJYDC5mFHE3zbCSCAC6tgpEgK+zhBURERFZNt4qMqPsXJ3h/3d+qiq6PRMkXTFERERWgCMuZpSTpwUANK/jg34dakpcDRERkeXjiIsZZeflj7jYqxQSV0JERGQdGFzMqGDEhfu2EBERmQaDixnl3J/jYscRFyIiIpNgcDGj7PsjLnYqjrgQERGZAoOLkdRZeVjx2xXciEp97HsNIy62HHEhIiIyBQYXI/108Cb+uRSLmevPPPa9hjkuHHEhIiIyCQYXIyWmZZf4vYZVRRxxISIiMgkGFyOpbEoeQnJyOceFiIjIlMpVcFm6dCkGDBggdRn/ydaI4FIw4sJVRURERKZRboLL+vXrMX/+fKnLeKyHg4teL/7zvdzHhYiIyLQkv6LGxcVhypQpOHHiBIKCgqQu57EeDi6ZORo4O6iKfZ9Or0eeRg+AIy5ERESmIvmIy+XLl2FjY4MdO3agUaNGUpfzWHrxYJQlI1vzyPfl5j14wCLnuBAREZmG5FfU9u3bo3379lKXUWIard7w/zNztI98X8GToZUKGWyUkudDIiIiqyB5cDEHpQmDgkIhL/Rfrf5BcMnO0z7y19Lo8t9nb6s0aT3lwb97QvnYl6LYk+KxL0WxJ0WxJ8WzuuAil8vg7u5o8vO6uNgDAGTyB3+AhEz+yF8rLi0XAOBob2OWesqDgp5QYexLUexJ8diXotiTotiTwqwuuOj1Amp1lsnOp1DI4eJiD7U6GzqdHplZeYbX4hIzkJKSWez3xSVmAABUSvkj32Op/t0Tyse+FMWeFI99KYo9Kaoi9cTFxb7EI0tWF1wAQKs1/W+wTqeHVqtHnubBpFt1Zt4jf62s+xN37WwUZqmnPCjoCRXGvhTFnhSPfSmKPSmKPSmMN86MpHko9Wb+x6qi7IJdc7mHCxERkckwuBhJqy3ZcmjumktERGR65Wo4YObMmVKX8FgPj7j8V3DhrrlERESmxxEXI2m0D+a4ZGQ/eh+XnFyOuBAREZkag4uRCm9AV4IRF+6aS0REZDK8qhrp4eBS3K2iPI0OFyOScSM6DQBHXIiIiEyJwcVID89x0Wj1GDLnEGSQGY5pdXroHnpqdCUv69x8joiISAoMLkYqGHFxdrBBepbG8AToh3m62KJ5HR+0qOuLav4uZV0iERGR1WJwMYJOr0fBw6Gnvf90oc3oCshlMni42EImkxV5jYiIiJ4Mg4sRHp7fYqdSwNVRJWE1REREFQ9XFRnh4eBiw6d1EhERlTlefY1QEFwUchnkct4KIiIiKmsMLkYoWFFko2TbiIiIpMArsBEKRlwYXIiIiKTBK7ARGFyIiIikxSuwEbT3bxUpOTGXiIhIErwCG4EjLkRERNLiFdgIhuDCERciIiJJ8ApsBI64EBERSYtXYCNwOTQREZG0eAU2Am8VERERSYtXYCPwVhEREZG0eAU2gpa3ioiIiCTFK7ARCkZcuI8LERGRNHgFNgJvFREREUmLV2AjcFURERGRtHgFNgJHXIiIiKTFK7ARuByaiIhIWrwCG+HBiItC4kqIiIgqJgYXI3A5NBERkbR4BTYC57gQERFJi1dgIxSsKlIqZBJXQkREVDExuJTQkfN3cflWMgDOcSEiIpIKg0sJpGflYfnOK4avneyUElZDRERUcfEKXALODioM6hGCyLgMuDvbonaAu9QlERERVUgMLiX0bMNK0N6fnEtERETS4K0iIiIishgMLkRERGQxGFyIiIjIYjC4EBERkcVgcCEiIiKLweBCREREFoPBhYiIiCwGgwsRERFZDAYXIiIishgMLkRERGQxGFyIiIjIYjC4EBERkcVgcCEiIiKLIRNCCKmLMCUhBPR6034khUIOnY5Phn4Ye1I89qUo9qR47EtR7ElRFaUncrkMMpmsRO+1uuBCRERE1ou3ioiIiMhiMLgQERGRxWBwISIiIovB4EJEREQWg8GFiIiILAaDCxEREVkMBhciIiKyGAwuREREZDEYXIiIiMhiMLgQERGRxWBwISIiIovB4EJEREQWg8GFiIiILAaDCxEREVkMBhcionJICCF1CeUOe1K8itYXpdQFlDf//PMPsrKyoNfr8cwzz8DJyUnqkiyGEAIymczwtV6vh1zObFwc9qZ47MsDD/9donzsCQGATFS0qPYfZs2ahR07dsDNzQ137txBo0aN0K1bN7z++utSl1bubdq0CZcvX4ZWq0VwcDDef/99qUsqd9LS0qDRaODl5WU49u+wVxGxL4X98ssvuHXrFhITE9G1a1c0atQIzs7OUpclKfakeOvXr8eNGzcQHR2N7t27o3HjxggKCpK6LLNjcLnv0KFDmDp1KhYuXIhq1aohKysL06ZNQ0xMDJ555hl88sknUpdYbs2bNw+bN2/GSy+9hJiYGNy8eRMuLi6YM2cOqlWrJnV55cKiRYtw4MABJCQkoFKlSnj99dfRrl07uLu7V+hRBvalsNmzZ+Pnn39GkyZNkJ2djZMnT6J3797o3bs3mjRpInV5kmBPilfwc7djx47IzMzEP//8g6ZNm+KVV15Bx44dpS7PvAQJIYTYuHGj6Nmzp8jNzTUcS0pKEtOnTxfdu3cX8+bNk664ciw6Olq8+OKL4tChQ0IIIfR6vbh06ZLo3bu36NChg7hw4YLEFUpv1apVomXLluLnn38Whw4dEsOHDxfdunUT48ePF7GxsUIIIXQ6ncRVlj32pbArV66Izp07i3PnzhmO/frrr+LFF18UgwcPFv/884+E1UmDPSne7du3RY8ePcTx48cNxw4dOiTee+890bt3b/Hbb79JWJ35Vax/zhRD3B9wsrGxQV5eHtRqNQBAq9XCw8MDw4YNQ4sWLXDkyBHs2LFDylLLpezsbKSkpKBKlSoA8u9B16tXD8uXL4ePjw/Gjh2L2NhYAPnzFyoSIQTy8vJw8uRJfPDBB+jduzfatWuHhQsXonv37rh+/TqmT5+OuLg4yOXyCjPBjn0pnkwmQ3Z2NpTKB1MPe/TogXHjxiEhIQHr16/HpUuXJKyw7LEnxVMoFEhISEBubq7hWLt27TBixAj4+flh7dq1OHz4sIQVmleFDy4F99GfeuopREVFYe3atQAApVIJrVYLV1dXfPjhh3BycmJweUjBxSQgIAD29vbYuXOn4TW9Xg8PDw8sWLAAdnZ2GDVqFABUuGF/mUwGlUqF7OxsxMXFAQB0Oh0AYNCgQejduzdiYmKwZMkSqNXqCjOng30pnlarRW5uLlJSUgAAeXl5APIvSMOHD8f169exc+dO6HS6ChPm2JOihBDQ6/VwcHDAvXv3AAAajQYA0LhxY7z//vtQqVTYvn07EhMTpSzVbCrWleQ/BAQE4LPPPsPSpUuxceNGAA/Ci6enJyZMmIBjx47h8uXLEldaPhRcTBQKBV544QUcPXoU+/fvBwDDv5K9vb0xadIkJCcn448//pCyXEkIISCEgI+PD06dOoWMjAwoFArDD9/+/fvj+eefx/Hjx3Hu3DkAFWNUin0pXv369fHss8/i008/RVxcHFQqleGC9Pzzz2PIkCFYt24dbty4UWHCHHvyQEEwk8lkCAgIQIcOHTBr1ixcv34dNjY2hr40bdoU77zzDg4cOIDw8HApSzYbBpeH9OrVCx988AE+//xzrF+/HgAKDVFWrVoVLi4uUpVXLmzcuBFffPEFBg8ejN27dyMtLQ3vvvsuFAoF1q1bh6NHjwJ4EGzq1KkDvV6PqKgoKcsuU0lJSUhLS0N6ejpkMhk++eQTxMXFYfLkyQAAlUpluEgPHz4cXl5e2LJlCwDrHpViXwrbvn075s6di2+++Qa7du0CAIwZMwZVqlTBhx9+iLi4OMMtbAB45ZVXUKVKFZw5c0bKss2KPSnepk2bMHnyZEyYMAHLli0DAIwePRrNmjXD22+/jaioqELhpX379qhevTqOHz8uZdlmY30/DZ6Ara0thgwZgsGDB+PLL7/E119/jRs3biAuLg579uwBADg4OEhcpXTmzp2LBQsWICsrCwqFAlOnTsXEiRMRFxeHOXPmICEhAcuWLcPvv/9u+B4nJydUrVq1wvRt0aJFGD58OLp164aRI0diy5Yt8Pb2xpQpU3Dw4EGMHz8eQP5FumAUoUWLFsjMzJSybLNjXwqbM2cOZs6ciZiYGPzzzz9YsGABBg0aBA8PD4waNQpyuRyDBg1CbGwsVCoVgPz5ZPb29nB1dZW4evNgT4o3b948zJ8/HzY2NkhOTsaWLVsMt1PHjBmDmjVrok+fPrh06RJsbGwA5N9is7W1hY+Pj8TVm4kEE4LLvdzcXLFjxw7RqlUr0bZtW9GxY0fRrl07cfnyZalLk8zNmzdFt27dxKlTpwzH9u3bJ9566y3Rq1cvcebMGREdHS369+8vevbsKaZMmSJ27twppk6dKp566ilx584dCasvG8uXLxctW7YUu3btEuvWrRPTp08XtWvXFl9//bWIjY0VP//8s2jcuLEYMWKESElJEVqtVgghxLhx48SoUaOEVqsVer1e4k9heuxLYWFhYaJTp06GFTG5ubli79694vnnnxevvfaaSExMFCdOnBB9+vQRzZs3F9u2bRO7du0Ss2fPFq1btxZRUVESfwLTY0+K96hVm7169RKdO3cW58+fF2FhYWLw4MGiYcOG4rvvvhM//PCD+Oqrr8TTTz8tbt++LfEnMA/unFsMlUqF7t27o0WLFoiMjIRWq0X16tXh6+srdWmSUSgUSExMNAxFAkDHjh3h5uaGFStWYObMmfj888+xYMEC/Prrr9i+fTvOnj0LJycn/PjjjwgICJCwevMTQuDChQt499138dJLLwEAcnJyEBISgokTJyI3NxdDhw6Fh4cHJk6ciAEDBsDb2xuOjo74559/sHHjRigUCok/hemxL0Wp1WpkZGSgevXqAPJ/3nTo0AH+/v745JNPMHz4cKxfvx4rVqzA/PnzsXDhQiiVSjg7O2PZsmWGFXzWhD0p3qNWba5YsQJDhw7F+PHjsWrVKixZsgSLFi3C4cOHkZ6eDi8vL6xevRqBgYESfwIzkTo5Ufmn1+tFRESEaN++vfj555+FEELk5eUZXj9x4oR48803xejRo0VWVpbheEZGRqGvrVl2drZ44YUXxNy5c4u89scff4h69eqJBQsWCCGESE9PF/PmzROTJk0SX331lQgPDy/rcssM+/JAwZ40cXFx4rnnnhObNm0q8p5z586J559/XowYMcJw7O7duyItLU2kpaWVWa1lhT0pXsEIY25urnj++ecL7SNW0LP4+HjRrVs38dprrxleS01NFTk5OSIjI6NM6y1rDC5UYlOmTBHNmzcXERERQghRaLO+33//XTRs2FCEhoZKVZ4kHr6FMXfuXNG1a1dx7dq1Iu/bsmWLqFu3rti1a9cjv9+asC+Plp6eLkaMGCHeeecdcfHixUKv5ebmim3btolu3bqJs2fPCiEqxiZ87EnxtFqtmDVrlnj11VfFvn37DMcL/n6cOnVKdOrUSfz+++9CiIrTF07OpWJt2bIFU6ZMweTJk7F69WoAwKeffop69ephwIABRZYmvvDCCwgICMCxY8ekLLtMpaenG/aXAIA2bdpAqVRi69atuHv3ruG4EAIvvvgiXnrpJRw/fhxardawb4k1Yl8K2759OxYsWIDJkyfjxIkTcHJywqhRoxAWFoYlS5bg5s2bhveqVCq0adMGsbGxhuPWuqKKPSmqtKs2Y2JiAFhvX/6tYnxKMsq8efMwd+5cCCFw9+5d/Pjjj+jXrx+SkpLw6aefokqVKnjllVdw48YNwyz2vLw8ODg4WO8s9n9ZtGgR3nvvPfTs2RP9+/fH7t270axZM7z55pv4448/sGHDBsMPE5lMBicnJzg5OeHWrVtQKpWGeRvWtvcE+1LYN998g1mzZuHKlSuIiIjAe++9hylTpsDJyQkrVqzAkSNHMHfuXJw9e9bwPc7OzqhZs6bVPkSQPSmeKVZtigqyCR8n51IhkZGR2Lt3L77++mu0bdsWOp0Oly5dwsSJEzF06FDMmjULn3/+Ob7++mv069cPQ4cOhYODA6KiohAZGYmWLVtK/RHMbuXKlVi3bh3GjBkDd3d3bNmyBYsXL8bp06cxYcIE5OTkYOXKlUhPT8ebb76JWrVqAcjfHbZKlSrQaDSGwGdN2JfCLl26hP3792Pp0qVo2LAhAGDr1q1YsWIF4uLiMGnSJGzevBlDhw7F3Llz0bp1azRs2BCHDh1CREQEQkJCJP4EpseeFC8iIgIHDx7EokWL0Lx5cwDA/v37sXbtWkyePBmTJk3CsmXLMH78eCxbtgwnTpxA8+bNcfr0aVy+fBmff/45AOsJ/I/Dp0NTITdv3sSAAQOwYcOGQo9Hj4+Px5AhQ6DVarFy5Up4e3tj7ty5OHbsGDIyMuDl5YXPPvsMdevWla54MxNCIDc3FyNHjsQzzzyDt99+2/Da4sWLsXfvXtStWxfTp0/Hzp07sWnTJiQkJKBOnTrQ6XQIDQ3Fhg0bULt2bQk/hemxL8W7cuUKPvzwQyxduhR16tQxHN+/fz8WL16MypUrY9q0aUhPT8ePP/6Iv/76C3K5HI6Ojpg+fbpV/l1iT4p3584d9OvXD3PnzkWrVq0Mx0NDQ7FixQqkpKTg888/h4+Pj2HVJpA/4jJp0qRCvawQpJteQ+XJzz//LK5cuSKysrJE27ZtxeLFiw2vFUz4unfvnujSpYvo37+/4bXk5GSRlZUl0tPTy7xmqfTv31/MmjVLCCEMe44IIcTq1atFjx49xMyZM4UQQly9elWsX79ejBo1SnzzzTciLCxMknrNqWCSoE6nY1/+5dy5c6JFixbixIkTQojCk9n37t0r2rdvb+hJbm6uyMzMFHFxcVb9d4k9KYqrNo3H4ELiiy++EPXr1xeRkZFCp9OJ6dOni9dee00cPHjQ8J6CC9SxY8dEhw4dDDPcK8osdiHye6DT6cTHH38s+vbta/ih8fAP33nz5okuXbqIY8eOSVVmmbp586YQIv/PwejRo9mXfxk2bJho06aNSEpKEkIU7sn69etFvXr1xI0bN6Qqr0zMmzdPrFy50vD1yJEjK3xPisNVmyXHybkV3IwZM7Bz505s3boVVatWhVwuR58+faDVarF+/XrDKqGCe6chISEVbhZ7cnIy0tPTkZGRAblcjrFjx+L27duYNm0agMLP2Bk1ahRcXV2xefNmKUsuE19++SWGDBnCvty3Y8cOLFiwAAsWLMDevXsB5K/E8/LywpAhQ5CcnAyVSoXc3FwAwBtvvAE/P79Ck1CtzfTp07Fu3Tq0bdvWcGzw4MHw9fWtsD0BuGrzSVn/VYceadasWdi+fTt++uknwz1SIQRq1qyJiRMnIjIyEhs3biw0i93FxaXCPXtoxIgR6N69Oz7++GNs374dlSpVwuTJk7Fr1y5MmTIFQOFn7LRs2RJqtVrKss1uxowZ2LFjB7799ls4OTlBp9PB398fkydPxs6dOzF16lQAFacvc+bMwYwZM3Dz5k38+eefmD17NoYPHw4/Pz+MHDkSGo0GH3zwAZKSkmBrawsAyMzMhIODg9WulCn4M7J27VoEBwcbVrzUrl0bQ4YMQW5uboXrCcBVm6bAVUUVlE6nw7lz5+Dv74+qVasCADQaDb799luEh4fDz88PNWvWRHx8PDZt2oTTp0+jWbNmOHXqFK5evYovv/xS4k9gfsuXL8f69evx2WefISkpCXfu3MH48eMRGRmJfv364X//+x9mzJiBrKwsTJ48GY6OjgCAe/fuwc3NDTqdDnK53Opm+s+cORO//vor1q9fj5o1awKAYRnziy++CLVajZkzZyIjIwNTpkyx+r7cuHEDe/bswfz589GyZUtkZ2fj4MGDmDlzJt5//30sWLAAn3zyCWbPno1u3bph0qRJUCqVuHTpEpKTk9GgQQOpP4LJrVmzBuvWrcOWLVsME2plMhnS0tKg0+nQoUMH2NnZYcGCBejatSsmT55s9T0BgKioKK7aNAEGlwpKoVBgwoQJ+N///od58+bh448/xuDBg5GVlYWQkBBERkYiKysLAPDMM8/gt99+w4kTJ+Do6Igff/zREHasVUGwGzhwILp37w4g/7khdevWxeeff47s7GzDk2snT56Mt99+G56ennBwcMDff/9tlc/YAYCjR49iw4YNmDRpkiG06PV6HDlyBCkpKfD19UXXrl3h7e2NqVOn4q233oKXl5dV9yUtLQ3Z2dmGftjb26Nz586oVKkSxowZg48//hg//PADVqxYgTlz5mD27NlQKpVwcnKy2ufsREdHo3LlyrC3tweQP2IwZcoUhIWFISEhAXXr1sVnn32G+fPnY/HixRWiJwCQlZUFtVpteHabQqFAo0aNsHLlSgwZMgTjx4/HypUrsXLlSsydOxd79+41rNpctWqV1T/zraS4HLoCy8nJwdq1a3HgwAEEBARAr9fjs88+g7u7O/Ly8rBq1Sr8+eef+PrrrxEYGIjMzEwoFAqrv02k1+uRk5ODnj174uWXX8awYcMKvf7bb79h3LhxGDFiBIYMGYKUlBSsXLkSKSkpsLe3x+uvv44aNWpIVL153b59GxMmTEBwcDCmTJkCuVyO9957D0lJSUhNTUVSUhJefvllfPLJJ5DL5YalnNbcl9jYWLzxxhv48MMP0adPn0KvnT59GqNGjcLTTz+N2bNnA8i/qDs6OkIul8PV1VWKksvEG2+8AQDYsGEDhg8fjqysLHTs2BEqlQqrVq2CUqnEtm3boFAoEBUVBScnJ6vvSU5ODrp06YK+ffsafq7o9XrI5XLExsbinXfegbe3N9auXQsASElJgZ2dHXQ6HZycnKQsvXyRdGowSS42NlYMHjxY1KlTR3zzzTeGlTNCCJGUlCTq1asnduzYIXGV0pg5c6bo0aNHsQ/7W7dunahbt26h54cIUTFWWZ06dUrUr19frFixQixcuFB8+OGH4tatWyIxMVEcPXpUhISEiDlz5gghCi+XtiZ//PGHWLNmjfj+++/F0aNHxZAhQ8Tw4cPFpUuXCr0vNzdXbN26VXTr1k1cuHBBCGF9vShQ0JOFCxeKiIgIERUVJdq3by9effVVMXHiRBEXF2d4b1RUlHj22WfF8uXLhRDW2xMhhDhy5IjYtWuX+OWXX0RmZqaYPn266Nu3L1dtPgFOzq3gfH198fHHH6N69ero0aMHZDIZ5HI5RP5SedStWxd+fn5Sl1kmfv31V6xYscLw9VNPPQWFQoEtW7YgLi7OcFwIgR49eqBz5844duwYdDqd4Rk71jJv42H/7kvz5s0xfvx4zJs3D3/99RcGDhyIoKAgeHp64plnnsEnn3yC/fv3Izk52TAx15r6Mnv2bHz++ef466+/sGbNGixbtgze3t4IDQ3FqlWrcPv2bcN7C56zc/fuXURERACwzpV4D/dk/fr1GDVqFI4dO4YRI0bg6tWriIuLg7u7u+H9Pj4+qFSpEjIyMgBYZ0+A/AUQn332GVatWoUJEyZg4cKFeO+996DT6bhq8wmwK4TatWvjl19+Qa1atXDv3j2kpaUhIyMD69atQ0JCgtXPZykIaSdOnMDq1auxdetWAED79u3Rvn177Nu3Dxs3bkRsbCyA/B8yzs7OcHR0xM2bN6FQKKzuGTtA0b78/PPPhte6du2K9u3bIzY2FpUrVza8H8i/b29nZwcXFxer68uuXbvw+++/Y8WKFVi5ciUOHDiAjIwM5ObmYubMmdi7dy8WLFiACxcuGL7Hzc0NNWvWtNqh/uJ6Ymtri3379qF58+YYMGAARo8eXehxDiqVCi4uLobbQsIKZyxs27YNu3fvxrJly7BmzRrMmDEDv/zyC/z8/DBp0iRERkZi3bp12LNnj+F7KtqqzdLi5FwCANja2iIpKQmvvvoq9Ho9/P39kZqaiu+++87qR1z0ej0UCgXs7e2RnZ2NtWvXIicnBwMGDDDcm9+5c6fhGTvVq1cHkH8xrlKlCrRaLZRK6/ur9O++/PDDD8jJycGbb74JNzc3vP/++/D29oavry/y8vKgUqkA5K+c8PHxgUajsbq+REREoGbNmqhduzY0Gg0cHBwwaNAgjB49Gv/73/+wfPlyjB8/HmlpaWjdujUaNGiAP//8E3fu3LHabdn/3RN7e3t88MEHGD16NABg7NixUCgUCA8PR3h4OGrVqoVt27bhwoULmDhxIgDrCbYPCwsLQ7NmzQy/766urnB0dMTUqVPh4uKCVq1a4fr161i/fj1CQ0Mr3KrNJ2FdP1XoiXh6ehr+tejj44MmTZoY/jVtzQpGBW7fvo0GDRrA29sbW7ZsAQAMGDAAn376KVxdXXHgwAEMGjQI9evXR05ODk6dOoWNGzda3cW5QHF92bRpEwDgzTffRKNGjQAAV69exdy5c1GtWjWkp6cbHg5XsKLEGgghIJPJkJCQgKSkJMhkMsMIgqurK7RaLe7evYtWrVph8eLF2LJlC9atWwcbGxvY29tj1apVVvd36b964uLiAq1Wa3jScWZmJmbMmIETJ06gSpUqUKlUWLNmDQIDAyX+FKZXMHoUExNjCGRCCCxduhQAkJGRgdDQULi4uMDb2xv169fHr7/+WqFWbT4p6/yJS6XWvHlzw9NJKwohBFJSUpCZmYmhQ4eievXqmDdvXqHwMnjwYDRv3hwXL17EmTNnEBQUhLFjxyI4OFji6s3nUX3ZtGkTZDKZYdVIeHg4FAoFzp49i+DgYGzYsMGwNNhaFFyAOnXqhHPnziEqKspwcXF1dYVCoUBeXh6EEKhfvz7q16+P9PR06HQ6KBQKq9xQ7XE9kcvlhh1xHR0dMXPmTMTGxsLe3h4eHh7w9PSUrHZzKujLoEGDcObMGQD5IebZZ59F//794eHhgYyMDMycORPh4eF45ZVXMHDgwAqzatMUuByaCIBWq8WWLVvw9NNPo0aNGggPD8eSJUtw/fp1vPbaa+jfv7/UJUriv/rSr18/vPnmmwBg2Nr/4X91W6vY2Fh4enoaPmdoaCgGDhyIrVu3Ijg4GDKZDD/88ANUKhVef/11iastGyXpyfr162FjY4O+fftKXK00cnJyDEubFQoFYmJi0KFDByxduhTt2rWTujyLwsm5RACUSiX69u2LGjVqQK/XIzg4GEOGDEHt2rWxefNmbNy4UeoSJfFffdm0aRM2bNgAIH+ypUqlsvrQAgB+fn6FPmdcXBy0Wi2cnZ0hk8mwYMECzJo1q0KNXJakJzNmzEDjxo2lK1IiBWMDdnZ2APJvwQohoNVqUatWLfj7+0tZnkVicCG6r2CuSsFQb8FFOiQkBN9//z1++uknKcuTzH/1ZcmSJRW2LwU0Gg0UCgWcnJywePFirFq1Clu2bLG622XGKK4nmzdvRq1ataQurcwV/L25e/cuTp48ieTkZGRkZGD79u3Izs4utEycSoZzXIj+5eEVDsHBwXj33Xdha2uLp59+WsKqpMe+FFYwOdXW1hYuLi6YOHEi9u/fj02bNqF+/fpSlycJ9uTREhISMHDgQDg7O8PHxwdpaWlYtGgRvL29pS7N4nCOC1EJPLzclx5gX/JXVfXq1Qu2trbYtGmT4aGCFRl7UrwLFy4gLCwMrq6uqFevHm8TlRKDCxHRE8jJycE333yDN954wyqfxVQa7AmZE4MLEdET0mg0FWJisjHYEzIXBhciIiKyGFxVRERERBaDwYWIiIgsBoMLERERWQwGFyIiIrIYDC5ERERkMbhzLhGZ3fjx47Ft27b/fE/lypURExODP//8E1WqVCmjyojI0nA5NBGZXWRkJJKTkw1ff/fdd7hy5QoWLVpkOFawC29ISEiF342XiB6NIy5EZHYBAQEICAgwfO3h4QGVSlUhnxZMRE+Gc1yIqFz45ZdfULt2bURHRwPIv730/vvvY/PmzejYsSMaNmyIfv364datWzh48CC6d++ORo0aoU+fPrh69Wqhc4WGhqJ///5o1KgRWrRogXHjxhUa8SEiy8URFyIqt86ePYv4+HiMHz8eubm5mDp1KgYNGgSZTIaPPvoI9vb2mDJlCsaOHYtdu3YBAE6dOoV3330XLVu2xPz585GWloYFCxbgrbfewk8//QQ7OzuJPxURPQkGFyIqtzIzMzF//nzDg/pOnjyJTZs2Yc2aNWjVqhUA4M6dO5g1axbUajVcXFwwZ84cVKtWDUuXLoVCoQAANGrUCF27dsXPP/+MN998U7LPQ0RPjreKiKjccnV1LfR0YS8vLwD5QaSAm5sbAECtViM7Oxvnz59Hu3btIISAVquFVqtF1apVUaNGDRw9erRM6yci0+OICxGVW05OTsUed3BwKPa4Wq2GXq/H8uXLsXz58iKv29ramrQ+Iip7DC5EZDUcHR0hk8nwzjvvoGvXrkVet7e3l6AqIjIlBhcishpOTk4ICQlBREQEGjRoYDiek5ODjz76CO3atUNwcLCEFRLRk+IcFyKyKqNHj8bff/+NMWPG4PDhwzhw4AAGDhyIY8eOoV69elKXR0RPiMGFiKzKs88+i5UrVyI2NhYfffQRPv30UygUCqxevZob3hFZAW75T0RERBaDIy5ERERkMRhciIiIyGIwuBAREZHFYHAhIiIii8HgQkRERBaDwYWIiIgsBoMLERERWQwGFyIiIrIYDC5ERERkMRhciIiIyGIwuBAREZHFYHAhIiIii/F//MNYDmF3XrAAAAAASUVORK5CYII=", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "views = g.expanding(100)\n", "\n", @@ -255,10 +176,10 @@ "degree = []\n", "\n", "for view in views:\n", - " timestamps.append(view.latest_time())\n", + " timestamps.append(view.latest_time)\n", " #vertex_count.append(view.num_vertices()) \n", " #edge_count.append(view.num_edges())\n", - " degree.append(view.num_edges()/max(1,view.num_vertices())) \n", + " degree.append(view.count_edges()/max(1,view.count_vertices())) \n", " \n", "sns.set_context()\n", "ax = plt.gca()\n", @@ -270,30 +191,9 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "image/png": "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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "views = g.expanding(step=10) \n", "\n", @@ -301,7 +201,7 @@ "degree = []\n", "\n", "for view in views:\n", - " timestamps.append(view.latest_time())\n", + " timestamps.append(view.latest_time)\n", " gandalf = view.vertex(\"Gandalf\")\n", " if(gandalf is not None):\n", " degree.append(gandalf.degree())\n", @@ -320,7 +220,7 @@ ], "metadata": { "kernelspec": { - "display_name": "raphtory", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -334,7 +234,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.9" + "version": "3.11.2" }, "vscode": { "interpreter": { diff --git a/python/tests/test_graph_conversions.py b/python/tests/test_graph_conversions.py new file mode 100644 index 0000000000..b3cdedf581 --- /dev/null +++ b/python/tests/test_graph_conversions.py @@ -0,0 +1,1008 @@ +from raphtory import Graph +from raphtory import export +import pandas as pd +import json +from pathlib import Path + +base_dir = Path(__file__).parent + + +def build_graph(): + edges_df = pd.read_csv(base_dir / "data/network_traffic_edges.csv") + edges_df["timestamp"] = pd.to_datetime(edges_df["timestamp"]).astype( + "datetime64[ms, UTC]" + ) + + vertices_df = pd.read_csv(base_dir / "data/network_traffic_vertices.csv") + vertices_df["timestamp"] = pd.to_datetime(vertices_df["timestamp"]).astype( + "datetime64[ms, UTC]" + ) + + return Graph.load_from_pandas( + edges_df=edges_df, + src="source", + dst="destination", + time="timestamp", + props=["data_size_MB"], + layer_in_df="transaction_type", + const_props=["is_encrypted"], + shared_const_props={"datasource": "data/network_traffic_edges.csv"}, + vertex_df=vertices_df, + vertex_col="server_id", + vertex_time_col="timestamp", + vertex_props=["OS_version", "primary_function", "uptime_days"], + vertex_const_props=["server_name", "hardware_type"], + vertex_shared_const_props={"datasource": "data/network_traffic_edges.csv"}, + ) + + +def test_py_vis(): + g = build_graph() + pyvis_g = export.to_pyvis(g, directed=True) + + assert pyvis_g.nodes == [ + { + "color": "#97c2fc", + "id": 7678824742430955432, + "image": "https://cdn-icons-png.flaticon.com/512/7584/7584620.png", + "label": "ServerA", + "shape": "dot", + }, + { + "color": "#97c2fc", + "id": 7718004695861170879, + "image": "https://cdn-icons-png.flaticon.com/512/7584/7584620.png", + "label": "ServerB", + "shape": "dot", + }, + { + "color": "#97c2fc", + "id": 17918514325589227856, + "image": "https://cdn-icons-png.flaticon.com/512/7584/7584620.png", + "label": "ServerC", + "shape": "dot", + }, + { + "color": "#97c2fc", + "id": 14902018402467198225, + "image": "https://cdn-icons-png.flaticon.com/512/7584/7584620.png", + "label": "ServerD", + "shape": "dot", + }, + { + "color": "#97c2fc", + "id": 11577954539736240602, + "image": "https://cdn-icons-png.flaticon.com/512/7584/7584620.png", + "label": "ServerE", + "shape": "dot", + }, + ] + assert pyvis_g.edges == [ + { + "arrowStrikethrough": False, + "arrows": "to", + "color": "#000000", + "from": 7678824742430955432, + "title": "", + "to": 7718004695861170879, + "value": 1, + }, + { + "arrowStrikethrough": False, + "arrows": "to", + "color": "#000000", + "from": 7678824742430955432, + "title": "", + "to": 17918514325589227856, + "value": 1, + }, + { + "arrowStrikethrough": False, + "arrows": "to", + "color": "#000000", + "from": 7718004695861170879, + "title": "", + "to": 14902018402467198225, + "value": 1, + }, + { + "arrowStrikethrough": False, + "arrows": "to", + "color": "#000000", + "from": 17918514325589227856, + "title": "", + "to": 7678824742430955432, + "value": 1, + }, + { + "arrowStrikethrough": False, + "arrows": "to", + "color": "#000000", + "from": 14902018402467198225, + "title": "", + "to": 17918514325589227856, + "value": 1, + }, + { + "arrowStrikethrough": False, + "arrows": "to", + "color": "#000000", + "from": 14902018402467198225, + "title": "", + "to": 11577954539736240602, + "value": 1, + }, + { + "arrowStrikethrough": False, + "arrows": "to", + "color": "#000000", + "from": 11577954539736240602, + "title": "", + "to": 7718004695861170879, + "value": 1, + }, + ] + + +def test_networkx_full_history(): + g = build_graph() + + networkxGraph = export.to_networkx(g) + assert networkxGraph.number_of_nodes() == 5 + assert networkxGraph.number_of_edges() == 7 + + nodeList = list(networkxGraph.nodes(data=True)) + server_list = [ + ( + "ServerA", + { + "OS_version": [ + (1693555200000, "Ubuntu 20.04"), + (1693555260000, "Ubuntu 20.04"), + (1693555320000, "Ubuntu 20.04"), + ], + "datasource": "data/network_traffic_edges.csv", + "hardware_type": "Blade Server", + "primary_function": [ + (1693555200000, "Database"), + (1693555260000, "Database"), + (1693555320000, "Database"), + ], + "server_name": "Alpha", + "update_history": [ + 1693555200000, + 1693555260000, + 1693555320000, + 1693555500000, + 1693556400000, + ], + "uptime_days": [ + (1693555200000, 120), + (1693555260000, 121), + (1693555320000, 122), + ], + }, + ), + ( + "ServerB", + { + "OS_version": [(1693555500000, "Red Hat 8.1")], + "datasource": "data/network_traffic_edges.csv", + "hardware_type": "Rack Server", + "primary_function": [(1693555500000, "Web Server")], + "server_name": "Beta", + "update_history": [ + 1693555200000, + 1693555500000, + 1693555800000, + 1693556700000, + ], + "uptime_days": [(1693555500000, 45)], + }, + ), + ( + "ServerC", + { + "OS_version": [(1693555800000, "Windows Server 2022")], + "datasource": "data/network_traffic_edges.csv", + "hardware_type": "Blade Server", + "primary_function": [(1693555800000, "File Storage")], + "server_name": "Charlie", + "update_history": [ + 1693555500000, + 1693555800000, + 1693556400000, + 1693557000000, + 1693557060000, + 1693557120000, + ], + "uptime_days": [(1693555800000, 90)], + }, + ), + ( + "ServerD", + { + "OS_version": [(1693556100000, "Ubuntu 20.04")], + "datasource": "data/network_traffic_edges.csv", + "hardware_type": "Tower Server", + "primary_function": [(1693556100000, "Application Server")], + "server_name": "Delta", + "update_history": [ + 1693555800000, + 1693556100000, + 1693557000000, + 1693557060000, + 1693557120000, + ], + "uptime_days": [(1693556100000, 60)], + }, + ), + ( + "ServerE", + { + "OS_version": [(1693556400000, "Red Hat 8.1")], + "datasource": "data/network_traffic_edges.csv", + "hardware_type": "Rack Server", + "primary_function": [(1693556400000, "Backup")], + "server_name": "Echo", + "update_history": [1693556100000, 1693556400000, 1693556700000], + "uptime_days": [(1693556400000, 30)], + }, + ), + ] + assert nodeList == server_list + + edgeList = list(networkxGraph.edges(data=True)) + resultList = [ + ( + "ServerA", + "ServerB", + { + "data_size_MB": [(1693555200000, 5.6)], + "datasource": "data/network_traffic_edges.csv", + "is_encrypted": True, + "layer": "Critical System Request", + "update_history": [1693555200000], + }, + ), + ( + "ServerA", + "ServerC", + { + "data_size_MB": [(1693555500000, 7.1)], + "datasource": "data/network_traffic_edges.csv", + "is_encrypted": False, + "layer": "File Transfer", + "update_history": [1693555500000], + }, + ), + ( + "ServerB", + "ServerD", + { + "data_size_MB": [(1693555800000, 3.2)], + "datasource": "data/network_traffic_edges.csv", + "is_encrypted": True, + "layer": "Standard Service Request", + "update_history": [1693555800000], + }, + ), + ( + "ServerC", + "ServerA", + { + "data_size_MB": [(1693556400000, 4.5)], + "datasource": "data/network_traffic_edges.csv", + "is_encrypted": True, + "layer": "Critical System Request", + "update_history": [1693556400000], + }, + ), + ( + "ServerD", + "ServerC", + { + "data_size_MB": [ + (1693557000000, 5.0), + (1693557060000, 10.0), + (1693557120000, 15.0), + ], + "datasource": "data/network_traffic_edges.csv", + "is_encrypted": True, + "layer": "Standard Service Request", + "update_history": [1693557000000, 1693557060000, 1693557120000], + }, + ), + ( + "ServerD", + "ServerE", + { + "data_size_MB": [(1693556100000, 8.9)], + "datasource": "data/network_traffic_edges.csv", + "is_encrypted": False, + "layer": "Administrative Command", + "update_history": [1693556100000], + }, + ), + ( + "ServerE", + "ServerB", + { + "data_size_MB": [(1693556700000, 6.2)], + "datasource": "data/network_traffic_edges.csv", + "is_encrypted": False, + "layer": "File Transfer", + "update_history": [1693556700000], + }, + ), + ] + assert edgeList == resultList + + +def test_networkx_exploded(): + g = build_graph() + + networkxGraph = export.to_networkx(g, explode_edges=True) + assert networkxGraph.number_of_nodes() == 5 + assert networkxGraph.number_of_edges() == 9 + + edgeList = list(networkxGraph.edges(data=True)) + resultList = [ + ( + "ServerA", + "ServerB", + { + "data_size_MB": [(1693555200000, 5.6)], + "datasource": "data/network_traffic_edges.csv", + "is_encrypted": True, + "layer": "Critical System Request", + "update_history": 1693555200000, + }, + ), + ( + "ServerA", + "ServerC", + { + "data_size_MB": [(1693555500000, 7.1)], + "datasource": "data/network_traffic_edges.csv", + "is_encrypted": False, + "layer": "File Transfer", + "update_history": 1693555500000, + }, + ), + ( + "ServerB", + "ServerD", + { + "data_size_MB": [(1693555800000, 3.2)], + "datasource": "data/network_traffic_edges.csv", + "is_encrypted": True, + "layer": "Standard Service Request", + "update_history": 1693555800000, + }, + ), + ( + "ServerC", + "ServerA", + { + "data_size_MB": [(1693556400000, 4.5)], + "datasource": "data/network_traffic_edges.csv", + "is_encrypted": True, + "layer": "Critical System Request", + "update_history": 1693556400000, + }, + ), + ( + "ServerD", + "ServerC", + { + "data_size_MB": [(1693557000000, 5.0)], + "datasource": "data/network_traffic_edges.csv", + "is_encrypted": True, + "layer": "Standard Service Request", + "update_history": 1693557000000, + }, + ), + ( + "ServerD", + "ServerC", + { + "data_size_MB": [(1693557060000, 10.0)], + "datasource": "data/network_traffic_edges.csv", + "is_encrypted": True, + "layer": "Standard Service Request", + "update_history": 1693557060000, + }, + ), + ( + "ServerD", + "ServerC", + { + "data_size_MB": [(1693557120000, 15.0)], + "datasource": "data/network_traffic_edges.csv", + "is_encrypted": True, + "layer": "Standard Service Request", + "update_history": 1693557120000, + }, + ), + ( + "ServerD", + "ServerE", + { + "data_size_MB": [(1693556100000, 8.9)], + "datasource": "data/network_traffic_edges.csv", + "is_encrypted": False, + "layer": "Administrative Command", + "update_history": 1693556100000, + }, + ), + ( + "ServerE", + "ServerB", + { + "data_size_MB": [(1693556700000, 6.2)], + "datasource": "data/network_traffic_edges.csv", + "is_encrypted": False, + "layer": "File Transfer", + "update_history": 1693556700000, + }, + ), + ] + assert edgeList == resultList + + +def test_networkx_no_props(): + g = build_graph() + + networkxGraph = export.to_networkx( + g, include_vertex_properties=False, include_edge_properties=False + ) + + nodeList = list(networkxGraph.nodes(data=True)) + resultList = [ + ( + "ServerA", + { + "update_history": [ + 1693555200000, + 1693555260000, + 1693555320000, + 1693555500000, + 1693556400000, + ] + }, + ), + ( + "ServerB", + { + "update_history": [ + 1693555200000, + 1693555500000, + 1693555800000, + 1693556700000, + ] + }, + ), + ( + "ServerC", + { + "update_history": [ + 1693555500000, + 1693555800000, + 1693556400000, + 1693557000000, + 1693557060000, + 1693557120000, + ] + }, + ), + ( + "ServerD", + { + "update_history": [ + 1693555800000, + 1693556100000, + 1693557000000, + 1693557060000, + 1693557120000, + ] + }, + ), + ("ServerE", {"update_history": [1693556100000, 1693556400000, 1693556700000]}), + ] + assert nodeList == resultList + + edgeList = list(networkxGraph.edges(data=True)) + resultList = [ + ( + "ServerA", + "ServerB", + {"layer": "Critical System Request", "update_history": [1693555200000]}, + ), + ( + "ServerA", + "ServerC", + {"layer": "File Transfer", "update_history": [1693555500000]}, + ), + ( + "ServerB", + "ServerD", + {"layer": "Standard Service Request", "update_history": [1693555800000]}, + ), + ( + "ServerC", + "ServerA", + {"layer": "Critical System Request", "update_history": [1693556400000]}, + ), + ( + "ServerD", + "ServerC", + { + "layer": "Standard Service Request", + "update_history": [1693557000000, 1693557060000, 1693557120000], + }, + ), + ( + "ServerD", + "ServerE", + {"layer": "Administrative Command", "update_history": [1693556100000]}, + ), + ( + "ServerE", + "ServerB", + {"layer": "File Transfer", "update_history": [1693556700000]}, + ), + ] + assert edgeList == resultList + + networkxGraph = export.to_networkx( + g, + include_vertex_properties=False, + include_edge_properties=False, + include_update_history=False, + ) + + nodeList = list(networkxGraph.nodes(data=True)) + resultList = [ + ("ServerA", {}), + ("ServerB", {}), + ("ServerC", {}), + ("ServerD", {}), + ("ServerE", {}), + ] + assert nodeList == resultList + + edgeList = list(networkxGraph.edges(data=True)) + resultList = [ + ("ServerA", "ServerB", {"layer": "Critical System Request"}), + ("ServerA", "ServerC", {"layer": "File Transfer"}), + ("ServerB", "ServerD", {"layer": "Standard Service Request"}), + ("ServerC", "ServerA", {"layer": "Critical System Request"}), + ("ServerD", "ServerC", {"layer": "Standard Service Request"}), + ("ServerD", "ServerE", {"layer": "Administrative Command"}), + ("ServerE", "ServerB", {"layer": "File Transfer"}), + ] + assert edgeList == resultList + + networkxGraph = export.to_networkx( + g, include_edge_properties=False, explode_edges=True + ) + edgeList = list(networkxGraph.edges(data=True)) + resultList = [ + ( + "ServerA", + "ServerB", + {"layer": "Critical System Request", "update_history": 1693555200000}, + ), + ( + "ServerA", + "ServerC", + {"layer": "File Transfer", "update_history": 1693555500000}, + ), + ( + "ServerB", + "ServerD", + {"layer": "Standard Service Request", "update_history": 1693555800000}, + ), + ( + "ServerC", + "ServerA", + {"layer": "Critical System Request", "update_history": 1693556400000}, + ), + ( + "ServerD", + "ServerC", + {"layer": "Standard Service Request", "update_history": 1693557000000}, + ), + ( + "ServerD", + "ServerC", + {"layer": "Standard Service Request", "update_history": 1693557060000}, + ), + ( + "ServerD", + "ServerC", + {"layer": "Standard Service Request", "update_history": 1693557120000}, + ), + ( + "ServerD", + "ServerE", + {"layer": "Administrative Command", "update_history": 1693556100000}, + ), + ( + "ServerE", + "ServerB", + {"layer": "File Transfer", "update_history": 1693556700000}, + ), + ] + assert edgeList == resultList + + +def test_networkx_no_history(): + g = build_graph() + + networkxGraph = export.to_networkx( + g, include_property_histories=False, include_update_history=False + ) + + nodeList = list(networkxGraph.nodes(data=True)) + resultList = [ + ( + "ServerA", + { + "OS_version": "Ubuntu 20.04", + "datasource": "data/network_traffic_edges.csv", + "hardware_type": "Blade Server", + "primary_function": "Database", + "server_name": "Alpha", + "uptime_days": 122, + }, + ), + ( + "ServerB", + { + "OS_version": "Red Hat 8.1", + "datasource": "data/network_traffic_edges.csv", + "hardware_type": "Rack Server", + "primary_function": "Web Server", + "server_name": "Beta", + "uptime_days": 45, + }, + ), + ( + "ServerC", + { + "OS_version": "Windows Server 2022", + "datasource": "data/network_traffic_edges.csv", + "hardware_type": "Blade Server", + "primary_function": "File Storage", + "server_name": "Charlie", + "uptime_days": 90, + }, + ), + ( + "ServerD", + { + "OS_version": "Ubuntu 20.04", + "datasource": "data/network_traffic_edges.csv", + "hardware_type": "Tower Server", + "primary_function": "Application Server", + "server_name": "Delta", + "uptime_days": 60, + }, + ), + ( + "ServerE", + { + "OS_version": "Red Hat 8.1", + "datasource": "data/network_traffic_edges.csv", + "hardware_type": "Rack Server", + "primary_function": "Backup", + "server_name": "Echo", + "uptime_days": 30, + }, + ), + ] + assert nodeList == resultList + + edgeList = list(networkxGraph.edges(data=True)) + resultList = [ + ( + "ServerA", + "ServerB", + { + "data_size_MB": 5.6, + "datasource": "data/network_traffic_edges.csv", + "is_encrypted": True, + "layer": "Critical System Request", + }, + ), + ( + "ServerA", + "ServerC", + { + "data_size_MB": 7.1, + "datasource": "data/network_traffic_edges.csv", + "is_encrypted": False, + "layer": "File Transfer", + }, + ), + ( + "ServerB", + "ServerD", + { + "data_size_MB": 3.2, + "datasource": "data/network_traffic_edges.csv", + "is_encrypted": True, + "layer": "Standard Service Request", + }, + ), + ( + "ServerC", + "ServerA", + { + "data_size_MB": 4.5, + "datasource": "data/network_traffic_edges.csv", + "is_encrypted": True, + "layer": "Critical System Request", + }, + ), + ( + "ServerD", + "ServerC", + { + "data_size_MB": 15.0, + "datasource": "data/network_traffic_edges.csv", + "is_encrypted": True, + "layer": "Standard Service Request", + }, + ), + ( + "ServerD", + "ServerE", + { + "data_size_MB": 8.9, + "datasource": "data/network_traffic_edges.csv", + "is_encrypted": False, + "layer": "Administrative Command", + }, + ), + ( + "ServerE", + "ServerB", + { + "data_size_MB": 6.2, + "datasource": "data/network_traffic_edges.csv", + "is_encrypted": False, + "layer": "File Transfer", + }, + ), + ] + assert edgeList == resultList + + networkxGraph = export.to_networkx( + g, include_property_histories=False, explode_edges=True + ) + edgeList = list(networkxGraph.edges(data=True)) + resultList = [ + ( + "ServerA", + "ServerB", + { + "data_size_MB": 5.6, + "datasource": "data/network_traffic_edges.csv", + "is_encrypted": True, + "layer": "Critical System Request", + "update_history": 1693555200000, + }, + ), + ( + "ServerA", + "ServerC", + { + "data_size_MB": 7.1, + "datasource": "data/network_traffic_edges.csv", + "is_encrypted": False, + "layer": "File Transfer", + "update_history": 1693555500000, + }, + ), + ( + "ServerB", + "ServerD", + { + "data_size_MB": 3.2, + "datasource": "data/network_traffic_edges.csv", + "is_encrypted": True, + "layer": "Standard Service Request", + "update_history": 1693555800000, + }, + ), + ( + "ServerC", + "ServerA", + { + "data_size_MB": 4.5, + "datasource": "data/network_traffic_edges.csv", + "is_encrypted": True, + "layer": "Critical System Request", + "update_history": 1693556400000, + }, + ), + ( + "ServerD", + "ServerC", + { + "data_size_MB": 5.0, + "datasource": "data/network_traffic_edges.csv", + "is_encrypted": True, + "layer": "Standard Service Request", + "update_history": 1693557000000, + }, + ), + ( + "ServerD", + "ServerC", + { + "data_size_MB": 10.0, + "datasource": "data/network_traffic_edges.csv", + "is_encrypted": True, + "layer": "Standard Service Request", + "update_history": 1693557060000, + }, + ), + ( + "ServerD", + "ServerC", + { + "data_size_MB": 15.0, + "datasource": "data/network_traffic_edges.csv", + "is_encrypted": True, + "layer": "Standard Service Request", + "update_history": 1693557120000, + }, + ), + ( + "ServerD", + "ServerE", + { + "data_size_MB": 8.9, + "datasource": "data/network_traffic_edges.csv", + "is_encrypted": False, + "layer": "Administrative Command", + "update_history": 1693556100000, + }, + ), + ( + "ServerE", + "ServerB", + { + "data_size_MB": 6.2, + "datasource": "data/network_traffic_edges.csv", + "is_encrypted": False, + "layer": "File Transfer", + "update_history": 1693556700000, + }, + ), + ] + assert edgeList == resultList + + +def save_df_to_json(df, filename): + df.to_json(filename) + # Below is if you want to pretty print the json + # json_str = df.to_json() + # parsed = json.loads(json_str) + # with open(filename, "w") as f: + # json.dump(parsed, f, indent=4) + + +# DO NOT RUN UNLESS RECREATING THE OUTPUT +def build_to_df(): + g = build_graph() + + edge_df = export.to_edge_df(g) + save_df_to_json(edge_df, "expected/dataframe_output/edge_df_all.json") + edge_df = export.to_edge_df(g, include_edge_properties=False) + save_df_to_json(edge_df, "expected/dataframe_output/edge_df_no_props.json") + edge_df = export.to_edge_df(g, include_update_history=False) + save_df_to_json(edge_df, "expected/dataframe_output/edge_df_no_hist.json") + edge_df = export.to_edge_df(g, include_property_histories=False) + save_df_to_json(edge_df, "expected/dataframe_output/edge_df_no_prop_hist.json") + + edge_df = export.to_edge_df(g, explode_edges=True) + save_df_to_json(edge_df, "expected/dataframe_output/edge_df_exploded.json") + edge_df = export.to_edge_df(g, explode_edges=True, include_edge_properties=False) + save_df_to_json(edge_df, "expected/dataframe_output/edge_df_exploded_no_props.json") + edge_df = export.to_edge_df(g, explode_edges=True, include_update_history=False) + save_df_to_json(edge_df, "expected/dataframe_output/edge_df_exploded_no_hist.json") + edge_df = export.to_edge_df(g, explode_edges=True, include_property_histories=False) + save_df_to_json( + edge_df, "expected/dataframe_output/edge_df_exploded_no_prop_hist.json" + ) + + vertex_df = export.to_vertex_df(g) + save_df_to_json(vertex_df, "expected/dataframe_output/vertex_df_all.json") + vertex_df = export.to_vertex_df(g, include_vertex_properties=False) + save_df_to_json(vertex_df, "expected/dataframe_output/vertex_df_no_props.json") + vertex_df = export.to_vertex_df(g, include_update_history=False) + save_df_to_json(vertex_df, "expected/dataframe_output/vertex_df_no_hist.json") + vertex_df = export.to_vertex_df(g, include_property_histories=False) + save_df_to_json(vertex_df, "expected/dataframe_output/vertex_df_no_prop_hist.json") + + +def compare_df(df1, df2): + # Have to do this way due to the number of maps inside the dataframes + s1 = df1.to_json() + s2 = df2.to_json() + data1 = json.loads(s1) + data2 = json.loads(s2) + assert data1 == data2 + + +def test_to_df(): + g = build_graph() + + compare_df( + export.to_edge_df(g), + pd.read_json(base_dir / "expected/dataframe_output/edge_df_all.json"), + ) + + compare_df( + export.to_edge_df(g, include_edge_properties=False), + pd.read_json(base_dir / "expected/dataframe_output/edge_df_no_props.json"), + ) + + compare_df( + export.to_edge_df(g, include_update_history=False), + pd.read_json(base_dir / "expected/dataframe_output/edge_df_no_hist.json"), + ) + + compare_df( + export.to_edge_df(g, include_property_histories=False), + pd.read_json(base_dir / "expected/dataframe_output/edge_df_no_prop_hist.json"), + ) + + compare_df( + export.to_edge_df(g, explode_edges=True), + pd.read_json(base_dir / "expected/dataframe_output/edge_df_exploded.json"), + ) + compare_df( + export.to_edge_df(g, explode_edges=True, include_edge_properties=False), + pd.read_json( + base_dir / "expected/dataframe_output/edge_df_exploded_no_props.json" + ), + ) + + compare_df( + export.to_edge_df(g, explode_edges=True, include_update_history=False), + pd.read_json( + base_dir / "expected/dataframe_output/edge_df_exploded_no_hist.json" + ), + ) + + compare_df( + export.to_edge_df(g, explode_edges=True, include_property_histories=False), + pd.read_json( + base_dir / "expected/dataframe_output/edge_df_exploded_no_prop_hist.json" + ), + ) + + compare_df( + export.to_vertex_df(g), + pd.read_json(base_dir / "expected/dataframe_output/vertex_df_all.json"), + ) + compare_df( + export.to_vertex_df(g, include_vertex_properties=False), + pd.read_json(base_dir / "expected/dataframe_output/vertex_df_no_props.json"), + ) + compare_df( + export.to_vertex_df(g, include_update_history=False), + pd.read_json(base_dir / "expected/dataframe_output/vertex_df_no_hist.json"), + ) + compare_df( + export.to_vertex_df(g, include_property_histories=False), + pd.read_json( + base_dir / "expected/dataframe_output/vertex_df_no_prop_hist.json" + ), + ) diff --git a/python/tests/test_graphdb.py b/python/tests/test_graphdb.py index aed3939434..89eef86330 100644 --- a/python/tests/test_graphdb.py +++ b/python/tests/test_graphdb.py @@ -1,60 +1,68 @@ -import re +import math import sys -import time -import datetime +import pandas as pd +import pandas.core.frame import pytest -from raphtory import Graph +from raphtory import Graph, GraphWithDeletions, PyDirection from raphtory import algorithms from raphtory import graph_loader import tempfile from math import isclose import datetime -def create_graph(num_shards): - g = Graph(num_shards) - edges = [ - (1, 1, 2), - (2, 1, 3), - (-1, 2, 1), - (0, 1, 1), - (7, 3, 2), - (1, 1, 1) - ] +edges = [(1, 1, 2), (2, 1, 3), (-1, 2, 1), (0, 1, 1), (7, 3, 2), (1, 1, 1)] + + +def create_graph(): + g = Graph() g.add_vertex(0, 1, {"type": "wallet", "cost": 99.5}) g.add_vertex(-1, 2, {"type": "wallet", "cost": 10.0}) - g.add_vertex(6, 3, {"type": "wallet", "cost": 76}) + g.add_vertex(6, 3, {"type": "wallet", "cost": 76.0}) for e in edges: - g.add_edge(e[0], e[1], e[2], {"prop1": 1, - "prop2": 9.8, "prop3": "test"}) + g.add_edge(e[0], e[1], e[2], {"prop1": 1, "prop2": 9.8, "prop3": "test"}) + + return g + + +def create_graph_with_deletions(): + g = GraphWithDeletions() + + g.add_vertex(0, 1, {"type": "wallet", "cost": 99.5}) + g.add_vertex(-1, 2, {"type": "wallet", "cost": 10.0}) + g.add_vertex(6, 3, {"type": "wallet", "cost": 76.0}) + for e in edges: + g.add_edge(e[0], e[1], e[2], {"prop1": 1, "prop2": 9.8, "prop3": "test"}) + g.edge(edges[0][1], edges[0][2]).add_constant_properties({"static": "test"}) + g.delete_edge(10, edges[0][1], edges[0][2]) return g def test_graph_len_edge_len(): - g = create_graph(2) + g = create_graph() - assert g.num_vertices() == 3 - assert g.num_edges() == 5 + assert g.count_vertices() == 3 + assert g.count_edges() == 5 def test_id_iterable(): - g = create_graph(2) + g = create_graph() - assert g.vertices.id().max() == 3 - assert g.vertices.id().min() == 1 - assert set(g.vertices.id().collect()) == {1, 2, 3} - out_neighbours = g.vertices.out_neighbours().id().collect() + assert g.vertices.id.max() == 3 + assert g.vertices.id.min() == 1 + assert set(g.vertices.id.collect()) == {1, 2, 3} + out_neighbours = g.vertices.out_neighbours.id.collect() out_neighbours = (set(n) for n in out_neighbours) - out_neighbours = dict(zip(g.vertices.id(), out_neighbours)) + out_neighbours = dict(zip(g.vertices.id, out_neighbours)) assert out_neighbours == {1: {1, 2, 3}, 2: {1}, 3: {2}} def test_degree_iterable(): - g = create_graph(2) + g = create_graph() assert g.vertices.degree().min() == 2 assert g.vertices.degree().max() == 3 assert g.vertices.in_degree().min() == 1 @@ -69,14 +77,14 @@ def test_degree_iterable(): def test_vertices_time_iterable(): - g = create_graph(2) + g = create_graph() - assert g.vertices.earliest_time().min() == -1 - assert g.vertices.latest_time().max() == 7 + assert g.vertices.earliest_time.min() == -1 + assert g.vertices.latest_time.max() == 7 def test_graph_has_edge(): - g = create_graph(2) + g = create_graph() assert not g.window(-1, 1).has_edge(1, 3) assert g.window(-1, 3).has_edge(1, 3) @@ -84,213 +92,347 @@ def test_graph_has_edge(): def test_graph_has_vertex(): - g = create_graph(2) + g = create_graph() assert g.has_vertex(3) def test_windowed_graph_has_vertex(): - g = create_graph(2) + g = create_graph() assert g.window(-1, 1).has_vertex(1) def test_windowed_graph_get_vertex(): - g = create_graph(2) + g = create_graph() view = g.window(0, sys.maxsize) - assert view.vertex(1).id() == 1 + assert view.vertex(1).id == 1 assert view.vertex(10) is None assert view.vertex(1).degree() == 3 def test_windowed_graph_degree(): - g = create_graph(3) + g = create_graph() view = g.window(0, sys.maxsize) - degrees = [v.degree() for v in view.vertices()] + degrees = [v.degree() for v in view.vertices] degrees.sort() assert degrees == [2, 2, 3] - in_degrees = [v.in_degree() for v in view.vertices()] + in_degrees = [v.in_degree() for v in view.vertices] in_degrees.sort() assert in_degrees == [1, 1, 2] - out_degrees = [v.out_degree() for v in view.vertices()] + out_degrees = [v.out_degree() for v in view.vertices] out_degrees.sort() assert out_degrees == [0, 1, 3] def test_windowed_graph_get_edge(): - g = create_graph(2) + g = create_graph() max_size = sys.maxsize min_size = -sys.maxsize - 1 view = g.window(min_size, max_size) - assert (view.edge(1, 3).src().id(), view.edge(1, 3).dst().id()) == (1, 3) - assert view.edge(2, 3) == None - assert view.edge(6, 5) == None + assert (view.edge(1, 3).src.id, view.edge(1, 3).dst.id) == (1, 3) + assert view.edge(2, 3) is None + assert view.edge(6, 5) is None - assert (view.vertex(1).id(), view.vertex(3).id()) == (1, 3) + assert (view.vertex(1).id, view.vertex(3).id) == (1, 3) view = g.window(2, 3) - assert (view.edge(1, 3).src().id(), view.edge(1, 3).dst().id()) == (1, 3) + assert (view.edge(1, 3).src.id, view.edge(1, 3).dst.id) == (1, 3) view = g.window(3, 7) - assert view.edge(1, 3) == None + assert view.edge(1, 3) is None def test_windowed_graph_edges(): - g = create_graph(1) + g = create_graph() view = g.window(0, sys.maxsize) - tedges = [v.edges() for v in view.vertices()] + tedges = [v.edges for v in view.vertices] edges = [] for e_iter in tedges: for e in e_iter: - edges.append([e.src().id(), e.dst().id()]) + edges.append([e.src.id, e.dst.id]) - assert edges == [ - [1, 1], - [1, 1], - [1, 2], - [1, 3], - [1, 2], - [3, 2], - [1, 3], - [3, 2] - ] + assert edges == [[1, 1], [1, 1], [1, 2], [1, 3], [1, 2], [3, 2], [1, 3], [3, 2]] - tedges = [v.in_edges() for v in view.vertices()] + tedges = [v.in_edges for v in view.vertices] in_edges = [] for e_iter in tedges: for e in e_iter: - in_edges.append([e.src().id(), e.dst().id()]) + in_edges.append([e.src.id, e.dst.id]) - assert in_edges == [ - [1, 1], - [1, 2], - [3, 2], - [1, 3] - ] + assert in_edges == [[1, 1], [1, 2], [3, 2], [1, 3]] - tedges = [v.out_edges() for v in view.vertices()] + tedges = [v.out_edges for v in view.vertices] out_edges = [] for e_iter in tedges: for e in e_iter: - out_edges.append([e.src().id(), e.dst().id()]) + out_edges.append([e.src.id, e.dst.id]) - assert out_edges == [ - [1, 1], - [1, 2], - [1, 3], - [3, 2] - ] + assert out_edges == [[1, 1], [1, 2], [1, 3], [3, 2]] def test_windowed_graph_vertex_ids(): - g = create_graph(3) + g = create_graph() - vs = [v for v in g.window(-1, 2).vertices().id()] + vs = [v for v in g.window(-1, 2).vertices.id] vs.sort() assert vs == [1, 2] # this makes clear that the end of the range is exclusive - vs = [v for v in g.window(-5, 3).vertices().id()] + vs = [v for v in g.window(-5, 3).vertices.id] vs.sort() assert vs == [1, 2, 3] def test_windowed_graph_vertices(): - g = create_graph(1) + g = create_graph() view = g.window(-1, 0) - vertices = list(view.vertices().id()) + vertices = list(view.vertices.id) assert vertices == [1, 2] def test_windowed_graph_neighbours(): - g = create_graph(1) + g = create_graph() max_size = sys.maxsize min_size = -sys.maxsize - 1 view = g.window(min_size, max_size) - neighbours = view.vertices.neighbours().id().collect() + neighbours = view.vertices.neighbours.id.collect() assert neighbours == [[1, 2, 3], [1, 3], [1, 2]] - in_neighbours = view.vertices.in_neighbours().id().collect() + in_neighbours = view.vertices.in_neighbours.id.collect() assert in_neighbours == [[1, 2], [1, 3], [1]] - out_neighbours = view.vertices.out_neighbours().id().collect() + out_neighbours = view.vertices.out_neighbours.id.collect() assert out_neighbours == [[1, 2, 3], [1], [2]] def test_name(): - # Currently deadlocking g = Graph() g.add_vertex(1, "Ben") g.add_vertex(1, 10) g.add_edge(1, "Ben", "Hamza") - assert g.vertex(10).name() == "10" - assert g.vertex("Ben").name() == "Ben" + assert g.vertex(10).name == "10" + assert g.vertex("Ben").name == "Ben" + assert g.vertex("Hamza").name == "Hamza" + + +def test_getitem(): + g = Graph() + g.add_vertex(0, 1, {"cost": 0}) + g.add_vertex(1, 1, {"cost": 1}) + + assert ( + g.vertex(1).properties.temporal.get("cost") + == g.vertex(1).properties.temporal["cost"] + ) + + +def test_graph_properties(): + g = create_graph() + + props = {"prop 1": 1, "prop 2": "hi", "prop 3": True} + g.add_constant_properties(props) + + sp = g.properties.constant.keys() + sp.sort() + assert sp == ["prop 1", "prop 2", "prop 3"] + assert g.properties["prop 1"] == 1 + + props = {"prop 4": 11, "prop 5": "world", "prop 6": False} + g.add_property(1, props) + + props = {"prop 6": True} + g.add_property(2, props) + + def history_test(key, value): + if value is None: + assert g.properties.temporal.get(key) is None + else: + assert g.properties.temporal.get(key).items() == value + + history_test("prop 1", None) + history_test("prop 2", None) + history_test("prop 3", None) + history_test("prop 4", [(1, 11)]) + history_test("prop 5", [(1, "world")]) + history_test("prop 6", [(1, False), (2, True)]) + history_test("undefined", None) + + def time_history_test(time, key, value): + if value is None: + assert g.at(time).properties.temporal.get(key) is None + else: + assert g.at(time).properties.temporal.get(key).items() == value + + time_history_test(2, "prop 6", [(1, False), (2, True)]) + time_history_test(1, "static prop", None) + + def time_static_property_test(time, key, value): + assert g.at(time).properties.constant.get(key) == value + + def static_property_test(key, value): + assert g.properties.constant.get(key) == value + + time_static_property_test(1, "prop 1", 1) + time_static_property_test(100, "prop 1", 1) + static_property_test("prop 1", 1) + static_property_test("prop 3", True) + + # testing property + def time_property_test(time, key, value): + assert g.at(time).properties.get(key) == value + + def property_test(key, value): + assert g.properties.get(key) == value + + static_property_test("prop 2", "hi") + property_test("prop 2", "hi") + time_static_property_test(2, "prop 3", True) + time_property_test(2, "prop 3", True) + + # testing properties + assert g.properties.as_dict() == { + "prop 1": 1, + "prop 2": "hi", + "prop 3": True, + "prop 4": 11, + "prop 5": "world", + "prop 6": True, + } + + assert g.properties.temporal.latest() == { + "prop 4": 11, + "prop 5": "world", + "prop 6": True, + } + assert g.at(2).properties.as_dict() == { + "prop 1": 1, + "prop 2": "hi", + "prop 3": True, + "prop 4": 11, + "prop 5": "world", + "prop 6": True, + } + # testing property histories + assert g.properties.temporal.histories() == { + "prop 4": [(1, 11)], + "prop 5": [(1, "world")], + "prop 6": [(1, False), (2, True)], + } + + assert g.at(2).properties.temporal.histories() == { + "prop 4": [(1, 11)], + "prop 5": [(1, "world")], + "prop 6": [(1, False), (2, True)], + } + + # testing property names + expected_names = sorted( + ["prop 1", "prop 2", "prop 3", "prop 4", "prop 5", "prop 6"] + ) + assert sorted(g.properties.keys()) == expected_names + + expected_names_no_static = sorted(["prop 4", "prop 5", "prop 6"]) + assert sorted(g.properties.temporal.keys()) == expected_names_no_static -# assert g.vertex("Hamza").name() == "Hamza" TODO need to fix + assert sorted(g.at(1).properties.temporal.keys()) == expected_names_no_static + + # testing has_property + assert "prop 4" in g.properties + assert "prop 7" not in g.properties + assert "prop 7" not in g.at(1).properties + assert "prop 1" in g.properties + assert "prop 2" in g.at(1).properties + assert "static prop" not in g.properties.constant def test_vertex_properties(): g = Graph() g.add_edge(1, 1, 1) props_t1 = {"prop 1": 1, "prop 3": "hi", "prop 4": True} - g.add_vertex(1, 1, props_t1) + v = g.add_vertex(1, 1, props_t1) props_t2 = {"prop 1": 2, "prop 2": 0.6, "prop 4": False} - g.add_vertex(2, 1, props_t2) + v.add_updates(2, props_t2) props_t3 = {"prop 2": 0.9, "prop 3": "hello", "prop 4": True} - g.add_vertex(3, 1, props_t3) + v.add_updates(3, props_t3) + v.add_constant_properties({"static prop": 123}) - g.add_vertex_properties(1, {"static prop": 123}) - - # testing property_history + # testing property history def history_test(key, value): - assert g.vertex(1).property_history(key) == value - assert g.vertices.property_history(key).collect() == [value] - assert g.vertices.out_neighbours().property_history(key).collect() == [[value]] + if value is None: + assert g.vertex(1).properties.temporal.get(key) is None + assert g.vertices.properties.temporal.get(key) is None + assert g.vertices.out_neighbours.properties.temporal.get(key) is None + else: + assert g.vertex(1).properties.temporal.get(key).items() == value + assert g.vertices.properties.temporal.get(key).items() == [value] + assert g.vertices.out_neighbours.properties.temporal.get(key).items() == [ + [value] + ] history_test("prop 1", [(1, 1), (2, 2)]) history_test("prop 2", [(2, 0.6), (3, 0.9)]) - history_test("prop 3", [(1, "hi"), (3, 'hello')]) + history_test("prop 3", [(1, "hi"), (3, "hello")]) history_test("prop 4", [(1, True), (2, False), (3, True)]) - history_test("undefined", []) + history_test("undefined", None) def time_history_test(time, key, value): - assert g.at(time).vertex(1).property_history(key) == value - assert g.at(time).vertices.property_history(key).collect() == [value] - assert g.at(time).vertices.out_neighbours().property_history(key).collect() == [[value]] + if value is None: + assert g.at(time).vertex(1).properties.temporal.get(key) is None + assert g.at(time).vertices.properties.temporal.get(key) is None + assert ( + g.at(time).vertices.out_neighbours.properties.temporal.get(key) is None + ) + else: + assert g.at(time).vertex(1).properties.temporal.get(key).items() == value + assert g.at(time).vertices.properties.temporal.get(key).items() == [value] + assert g.at(time).vertices.out_neighbours.properties.temporal.get( + key + ).items() == [[value]] time_history_test(1, "prop 4", [(1, True)]) - time_history_test(1, "static prop", []) + time_history_test(1, "static prop", None) def time_static_property_test(time, key, value): gg = g.at(time) - assert gg.vertex(1).static_property(key) == value - assert gg.vertices.static_property(key).collect() == [value] - assert gg.vertices.out_neighbours().static_property(key).collect() == [[value]] + if value is None: + assert gg.vertex(1).properties.constant.get(key) is None + assert gg.vertices.properties.constant.get(key) is None + assert gg.vertices.out_neighbours.properties.constant.get(key) is None + else: + assert gg.vertex(1).properties.constant.get(key) == value + assert gg.vertices.properties.constant.get(key) == [value] + assert gg.vertices.out_neighbours.properties.constant.get(key) == [[value]] def static_property_test(key, value): - assert g.vertex(1).static_property(key) == value - assert g.vertices.static_property(key).collect() == [value] - assert g.vertices.out_neighbours().static_property(key).collect() == [[value]] + if value is None: + assert g.vertex(1).properties.constant.get(key) is None + assert g.vertices.properties.constant.get(key) is None + assert g.vertices.out_neighbours.properties.constant.get(key) is None + else: + assert g.vertex(1).properties.constant.get(key) == value + assert g.vertices.properties.constant.get(key) == [value] + assert g.vertices.out_neighbours.properties.constant.get(key) == [[value]] time_static_property_test(1, "static prop", 123) time_static_property_test(100, "static prop", 123) @@ -300,19 +442,36 @@ def static_property_test(key, value): # testing property def time_property_test(time, key, value): gg = g.at(time) - assert gg.vertex(1).property(key) == value - assert gg.vertices.property(key).collect() == [value] - assert gg.vertices.out_neighbours().property(key).collect() == [[value]] + if value is None: + assert gg.vertex(1).properties.get(key) is None + assert gg.vertices.properties.get(key) is None + assert gg.vertices.out_neighbours.properties.get(key) is None + else: + assert gg.vertex(1).properties.get(key) == value + assert gg.vertices.properties.get(key) == [value] + assert gg.vertices.out_neighbours.properties.get(key) == [[value]] def property_test(key, value): - assert g.vertex(1).property(key) == value - assert g.vertices.property(key).collect() == [value] - assert g.vertices.out_neighbours().property(key).collect() == [[value]] + if value is None: + assert g.vertex(1).properties.get(key) is None + assert g.vertices.properties.get(key) is None + assert g.vertices.out_neighbours.properties.get(key) is None + else: + assert g.vertex(1).properties.get(key) == value + assert g.vertices.properties.get(key) == [value] + assert g.vertices.out_neighbours.properties.get(key) == [[value]] def no_static_property_test(key, value): - assert g.vertex(1).property(key, include_static=False) == value - assert g.vertices.property(key, include_static=False).collect() == [value] - assert g.vertices.out_neighbours().property(key, include_static=False).collect() == [[value]] + if value is None: + assert g.vertex(1).properties.temporal.get(key) is None + assert g.vertices.properties.temporal.get(key) is None + assert g.vertices.out_neighbours.properties.temporal.get(key) is None + else: + assert g.vertex(1).properties.temporal.get(key).value() == value + assert g.vertices.properties.temporal.get(key).value() == [value] + assert g.vertices.out_neighbours.properties.temporal.get(key).value() == [ + [value] + ] property_test("static prop", 123) assert g.vertex(1)["static prop"] == 123 @@ -322,185 +481,295 @@ def no_static_property_test(key, value): time_property_test(1, "prop 2", None) # testing properties - assert g.vertex(1).properties() == {'prop 2': 0.9, 'prop 3': 'hello', 'prop 1': 2, 'prop 4': True, - 'static prop': 123} - assert g.vertices.properties().collect() == [{'prop 2': 0.9, 'prop 3': 'hello', 'prop 1': 2, 'prop 4': True, - 'static prop': 123}] - assert g.vertices.out_neighbours().properties().collect() == [[ - {'prop 2': 0.9, 'prop 3': 'hello', 'prop 1': 2, 'prop 4': True, - 'static prop': 123}]] - - assert g.vertex(1).properties(include_static=False) == {'prop 2': 0.9, 'prop 3': 'hello', 'prop 1': 2, - 'prop 4': True} - assert g.vertices.properties(include_static=False).collect() == [{'prop 2': 0.9, 'prop 3': 'hello', 'prop 1': 2, - 'prop 4': True}] - assert g.vertices.out_neighbours().properties(include_static=False).collect() == [ - [{'prop 2': 0.9, 'prop 3': 'hello', 'prop 1': 2, - 'prop 4': True}]] - - assert g.at(2).vertex(1).properties() == {'prop 1': 2, 'prop 4': False, 'prop 2': 0.6, 'static prop': 123, - 'prop 3': 'hi'} - assert g.at(2).vertices.properties().collect() == [{'prop 1': 2, 'prop 4': False, 'prop 2': 0.6, 'static prop': 123, - 'prop 3': 'hi'}] - assert g.at(2).vertices.out_neighbours().properties().collect() == [ - [{'prop 1': 2, 'prop 4': False, 'prop 2': 0.6, 'static prop': 123, - 'prop 3': 'hi'}]] + assert g.vertex(1).properties == { + "prop 2": 0.9, + "prop 3": "hello", + "prop 1": 2, + "prop 4": True, + "static prop": 123, + } + assert g.vertices.properties == { + "prop 2": [0.9], + "prop 3": ["hello"], + "prop 1": [2], + "prop 4": [True], + "static prop": [123], + } + assert g.vertices.out_neighbours.properties == { + "prop 2": [[0.9]], + "prop 3": [["hello"]], + "prop 1": [[2]], + "prop 4": [[True]], + "static prop": [[123]], + } + + assert g.vertex(1).properties.temporal.latest() == { + "prop 2": 0.9, + "prop 3": "hello", + "prop 1": 2, + "prop 4": True, + } + assert g.vertices.properties.temporal.latest() == { + "prop 2": [0.9], + "prop 3": ["hello"], + "prop 1": [2], + "prop 4": [True], + } + assert g.vertices.out_neighbours.properties.temporal.latest() == { + "prop 2": [[0.9]], + "prop 3": [["hello"]], + "prop 1": [[2]], + "prop 4": [[True]], + } + + assert g.at(2).vertex(1).properties == { + "prop 1": 2, + "prop 4": False, + "prop 2": 0.6, + "static prop": 123, + "prop 3": "hi", + } + assert g.at(2).vertices.properties == { + "prop 1": [2], + "prop 4": [False], + "prop 2": [0.6], + "static prop": [123], + "prop 3": ["hi"], + } + assert g.at(2).vertices.out_neighbours.properties == { + "prop 1": [[2]], + "prop 4": [[False]], + "prop 2": [[0.6]], + "static prop": [[123]], + "prop 3": [["hi"]], + } # testing property histories - assert g.vertex(1).property_histories() == {'prop 3': [(1, 'hi'), (3, 'hello')], 'prop 1': [(1, 1), (2, 2)], - 'prop 4': [(1, True), (2, False), (3, True)], - 'prop 2': [(2, 0.6), (3, 0.9)]} - assert g.vertices.property_histories().collect() == [ - {'prop 3': [(1, 'hi'), (3, 'hello')], 'prop 1': [(1, 1), (2, 2)], - 'prop 4': [(1, True), (2, False), (3, True)], - 'prop 2': [(2, 0.6), (3, 0.9)]}] - assert g.vertices.out_neighbours().property_histories().collect() == [ - [{'prop 3': [(1, 'hi'), (3, 'hello')], 'prop 1': [(1, 1), (2, 2)], - 'prop 4': [(1, True), (2, False), (3, True)], - 'prop 2': [(2, 0.6), (3, 0.9)]}]] - - assert g.at(2).vertex(1).property_histories() == {'prop 2': [(2, 0.6)], 'prop 4': [(1, True), (2, False)], - 'prop 1': [(1, 1), (2, 2)], 'prop 3': [(1, 'hi')]} - assert g.at(2).vertices.property_histories().collect() == [{'prop 2': [(2, 0.6)], 'prop 4': [(1, True), (2, False)], - 'prop 1': [(1, 1), (2, 2)], 'prop 3': [(1, 'hi')]}] - assert g.at(2).vertices.out_neighbours().property_histories().collect() == [ - [{'prop 2': [(2, 0.6)], 'prop 4': [(1, True), (2, False)], - 'prop 1': [(1, 1), (2, 2)], 'prop 3': [(1, 'hi')]}]] + assert g.vertex(1).properties.temporal == { + "prop 3": [(1, "hi"), (3, "hello")], + "prop 1": [(1, 1), (2, 2)], + "prop 4": [(1, True), (2, False), (3, True)], + "prop 2": [(2, 0.6), (3, 0.9)], + } + assert g.vertices.properties.temporal == { + "prop 3": [[(1, "hi"), (3, "hello")]], + "prop 1": [[(1, 1), (2, 2)]], + "prop 4": [[(1, True), (2, False), (3, True)]], + "prop 2": [[(2, 0.6), (3, 0.9)]], + } + assert g.vertices.out_neighbours.properties.temporal == { + "prop 3": [[[(1, "hi"), (3, "hello")]]], + "prop 1": [[[(1, 1), (2, 2)]]], + "prop 4": [[[(1, True), (2, False), (3, True)]]], + "prop 2": [[[(2, 0.6), (3, 0.9)]]], + } + + assert g.at(2).vertex(1).properties.temporal == { + "prop 2": [(2, 0.6)], + "prop 4": [(1, True), (2, False)], + "prop 1": [(1, 1), (2, 2)], + "prop 3": [(1, "hi")], + } + assert g.at(2).vertices.properties.temporal == { + "prop 2": [[(2, 0.6)]], + "prop 4": [[(1, True), (2, False)]], + "prop 1": [[(1, 1), (2, 2)]], + "prop 3": [[(1, "hi")]], + } + assert g.at(2).vertices.out_neighbours.properties.temporal == { + "prop 2": [[[(2, 0.6)]]], + "prop 4": [[[(1, True), (2, False)]]], + "prop 1": [[[(1, 1), (2, 2)]]], + "prop 3": [[[(1, "hi")]]], + } # testing property names - expected_names = sorted(['prop 4', 'prop 1', 'prop 2', 'prop 3', 'static prop']) - assert sorted(g.vertex(1).property_names()) == expected_names - names = g.vertices.property_names().collect() - assert len(names) == 1 and sorted(names[0]) == expected_names - names = g.vertices.out_neighbours().property_names().collect() - assert len(names) == 1 and len(names[0]) == 1 and sorted(names[0][0]) == expected_names - - expected_names_no_static = sorted(['prop 4', 'prop 1', 'prop 2', 'prop 3']) - assert sorted(g.vertex(1).property_names(include_static=False)) == expected_names_no_static - names = g.vertices.property_names(include_static=False).collect() - assert len(names) == 1 and sorted(names[0]) == expected_names_no_static - names = g.vertices.out_neighbours().property_names(include_static=False).collect() - assert len(names) == 1 and len(names[0]) == 1 and sorted(names[0][0]) == expected_names_no_static - - assert sorted(g.at(1).vertex(1).property_names(include_static=False)) == expected_names_no_static - names = g.at(1).vertices.property_names(include_static=False).collect() - assert len(names) == 1 and sorted(names[0]) == expected_names_no_static - names = g.at(1).vertices.out_neighbours().property_names(include_static=False).collect() - assert len(names) == 1 and len(names[0]) == 1 and sorted(names[0][0]) == expected_names_no_static + expected_names = sorted(["prop 4", "prop 1", "prop 2", "prop 3", "static prop"]) + assert sorted(g.vertex(1).properties.keys()) == expected_names + assert sorted(g.vertices.properties.keys()) == expected_names + assert sorted(g.vertices.out_neighbours.properties.keys()) == expected_names + + expected_names_no_static = sorted(["prop 4", "prop 1", "prop 2", "prop 3"]) + assert sorted(g.vertex(1).properties.temporal.keys()) == expected_names_no_static + assert sorted(g.vertices.properties.temporal.keys()) == expected_names_no_static + assert ( + sorted(g.vertices.out_neighbours.properties.temporal.keys()) + == expected_names_no_static + ) + + expected_names_no_static_at_1 = sorted(["prop 4", "prop 1", "prop 3"]) + assert ( + sorted(g.at(1).vertex(1).properties.temporal.keys()) + == expected_names_no_static_at_1 + ) + assert ( + sorted(g.at(1).vertices.properties.temporal.keys()) + == expected_names_no_static_at_1 + ) + assert ( + sorted(g.at(1).vertices.out_neighbours.properties.temporal.keys()) + == expected_names_no_static_at_1 + ) # testing has_property - assert g.vertex(1).has_property("prop 4") - assert g.vertices.has_property("prop 4").collect() == [True] - assert g.vertices.out_neighbours().has_property("prop 4").collect() == [[True]] + assert "prop 4" in g.vertex(1).properties + assert "prop 4" in g.vertices.properties + assert "prop 4" in g.vertices.out_neighbours.properties - assert g.vertex(1).has_property("prop 2") - assert g.vertices.has_property("prop 2").collect() == [True] - assert g.vertices.out_neighbours().has_property("prop 2").collect() == [[True]] + assert "prop 2" in g.vertex(1).properties + assert "prop 2" in g.vertices.properties + assert "prop 2" in g.vertices.out_neighbours.properties - assert not g.vertex(1).has_property("prop 5") - assert g.vertices.has_property("prop 5").collect() == [False] - assert g.vertices.out_neighbours().has_property("prop 5").collect() == [[False]] + assert "prop 5" not in g.vertex(1).properties + assert "prop 5" not in g.vertices.properties + assert "prop 5" not in g.vertices.out_neighbours.properties - assert not g.at(1).vertex(1).has_property("prop 2") - assert g.at(1).vertices.has_property("prop 2").collect() == [False] - assert g.at(1).vertices.out_neighbours().has_property("prop 2").collect() == [[False]] + assert "prop 2" not in g.at(1).vertex(1).properties + assert "prop 2" not in g.at(1).vertices.properties + assert "prop 2" not in g.at(1).vertices.out_neighbours.properties - assert g.vertex(1).has_property("static prop") - assert g.vertices.has_property("static prop").collect() == [True] - assert g.vertices.out_neighbours().has_property("static prop").collect() == [[True]] + assert "static prop" in g.vertex(1).properties + assert "static prop" in g.vertices.properties + assert "static prop" in g.vertices.out_neighbours.properties - assert g.at(1).vertex(1).has_property("static prop") - assert g.at(1).vertices.has_property("static prop").collect() == [True] - assert g.at(1).vertices.out_neighbours().has_property("static prop").collect() == [[True]] + assert "static prop" in g.at(1).vertex(1).properties + assert "static prop" in g.at(1).vertices.properties + assert "static prop" in g.at(1).vertices.out_neighbours.properties - assert not g.at(1).vertex(1).has_property("static prop", include_static=False) - assert g.at(1).vertices.has_property("static prop", include_static=False).collect() == [False] - assert g.at(1).vertices.out_neighbours().has_property("static prop", include_static=False).collect() == [[False]] + assert "static prop" not in g.at(1).vertex(1).properties.temporal + assert "static prop" not in g.at(1).vertices.properties.temporal + assert "static prop" not in g.at(1).vertices.out_neighbours.properties.temporal - assert g.vertex(1).has_static_property("static prop") - assert g.vertices.has_static_property("static prop").collect() == [True] - assert g.vertices.out_neighbours().has_static_property("static prop").collect() == [[True]] + assert "static prop" in g.vertex(1).properties.constant + assert "static prop" in g.vertices.properties.constant + assert "static prop" in g.vertices.out_neighbours.properties.constant - assert not g.vertex(1).has_static_property("prop 2") - assert g.vertices.has_static_property("prop 2").collect() == [False] - assert g.vertices.out_neighbours().has_static_property("prop 2").collect() == [[False]] + assert "prop 2" not in g.vertex(1).properties.constant + assert "prop 2" not in g.vertices.properties.constant + assert "prop 2" not in g.vertices.out_neighbours.properties.constant - assert g.at(1).vertex(1).has_static_property("static prop") - assert g.at(1).vertices.has_static_property("static prop").collect() == [True] - assert g.at(1).vertices.out_neighbours().has_static_property("static prop").collect() == [[True]] + assert "static prop" in g.at(1).vertex(1).properties.constant + assert "static prop" in g.at(1).vertices.properties.constant + assert "static prop" in g.at(1).vertices.out_neighbours.properties.constant def test_edge_properties(): g = Graph() props_t1 = {"prop 1": 1, "prop 3": "hi", "prop 4": True} - g.add_edge(1, 1, 2, props_t1) + e = g.add_edge(1, 1, 2, props_t1) props_t2 = {"prop 1": 2, "prop 2": 0.6, "prop 4": False} - g.add_edge(2, 1, 2, props_t2) + e.add_updates(2, props_t2) props_t3 = {"prop 2": 0.9, "prop 3": "hello", "prop 4": True} - g.add_edge(3, 1, 2, props_t3) - - g.add_edge_properties(1, 2, {"static prop": 123}) - - # testing property_history - assert g.edge(1, 2).property_history("prop 1") == [(1, 1), (2, 2)] - assert g.edge(1, 2).property_history("prop 2") == [(2, 0.6), (3, 0.9)] - assert g.edge(1, 2).property_history("prop 3") == [(1, "hi"), (3, 'hello')] - assert g.edge(1, 2).property_history("prop 4") == [(1, True), (2, False), (3, True)] - assert g.edge(1, 2).property_history("undefined") == [] - assert g.at(1).edge(1, 2).property_history("prop 4") == [(1, True)] - assert g.at(1).edge(1, 2).property_history("static prop") == [] + e.add_updates(3, props_t3) + + e.add_constant_properties({"static prop": 123}) + + # testing property history + assert g.edge(1, 2).properties.temporal.get("prop 1") == [(1, 1), (2, 2)] + assert g.edge(1, 2).properties.temporal.get("prop 2") == [(2, 0.6), (3, 0.9)] + assert g.edge(1, 2).properties.temporal.get("prop 3") == [(1, "hi"), (3, "hello")] + assert g.edge(1, 2).properties.temporal.get("prop 4") == [ + (1, True), + (2, False), + (3, True), + ] + assert g.edge(1, 2).properties.temporal.get("undefined") is None + assert g.at(1).edge(1, 2).properties.temporal.get("prop 4") == [(1, True)] + assert g.at(1).edge(1, 2).properties.temporal.get("static prop") is None - assert g.at(1).edge(1, 2).static_property("static prop") == 123 - assert g.at(100).edge(1, 2).static_property("static prop") == 123 - assert g.edge(1, 2).static_property("static prop") == 123 - assert g.edge(1, 2).static_property("prop 4") is None + assert g.at(1).edge(1, 2).properties.constant.get("static prop") == 123 + assert g.at(100).edge(1, 2).properties.constant.get("static prop") == 123 + assert g.edge(1, 2).properties.constant.get("static prop") == 123 + assert g.edge(1, 2).properties.constant.get("prop 4") is None # testing property - assert g.edge(1, 2).property("static prop") == 123 + assert g.edge(1, 2).properties.get("static prop") == 123 assert g.edge(1, 2)["static prop"] == 123 - assert g.edge(1, 2).property("static prop", include_static=False) is None - assert g.edge(1, 2).property("prop 1", include_static=False) == 2 - assert g.at(2).edge(1, 2).property("prop 2") == 0.6 - assert g.at(1).edge(1, 2).property("prop 2") is None + assert g.edge(1, 2).properties.temporal.get("static prop") is None + assert g.edge(1, 2).properties.temporal.get("prop 1").value() == 2 + assert g.at(2).edge(1, 2).properties.get("prop 2") == 0.6 + assert g.at(1).edge(1, 2).properties.get("prop 2") is None # testing properties - assert g.edge(1, 2).properties() == {'prop 2': 0.9, 'prop 3': 'hello', 'prop 1': 2, 'prop 4': True, - 'static prop': 123} + assert g.edge(1, 2).properties == { + "prop 2": 0.9, + "prop 3": "hello", + "prop 1": 2, + "prop 4": True, + "static prop": 123, + } - assert g.edge(1, 2).properties(include_static=False) == {'prop 2': 0.9, 'prop 3': 'hello', 'prop 1': 2, - 'prop 4': True} + assert g.edge(1, 2).properties.temporal.latest() == { + "prop 2": 0.9, + "prop 3": "hello", + "prop 1": 2, + "prop 4": True, + } - assert g.at(2).edge(1, 2).properties() == {'prop 1': 2, 'prop 4': False, 'prop 2': 0.6, 'static prop': 123, - 'prop 3': 'hi'} + assert g.at(2).edge(1, 2).properties == { + "prop 1": 2, + "prop 4": False, + "prop 2": 0.6, + "static prop": 123, + "prop 3": "hi", + } # testing property histories - assert g.edge(1, 2).property_histories() == {'prop 3': [(1, 'hi'), (3, 'hello')], 'prop 1': [(1, 1), (2, 2)], - 'prop 4': [(1, True), (2, False), (3, True)], - 'prop 2': [(2, 0.6), (3, 0.9)]} + assert g.edge(1, 2).properties.temporal == { + "prop 3": [(1, "hi"), (3, "hello")], + "prop 1": [(1, 1), (2, 2)], + "prop 4": [(1, True), (2, False), (3, True)], + "prop 2": [(2, 0.6), (3, 0.9)], + } - assert g.at(2).edge(1, 2).property_histories() == {'prop 2': [(2, 0.6)], 'prop 4': [(1, True), (2, False)], - 'prop 1': [(1, 1), (2, 2)], 'prop 3': [(1, 'hi')]} + assert g.at(2).edge(1, 2).properties.temporal == { + "prop 2": [(2, 0.6)], + "prop 4": [(1, True), (2, False)], + "prop 1": [(1, 1), (2, 2)], + "prop 3": [(1, "hi")], + } # testing property names - assert g.edge(1, 2).property_names().sort() == ['prop 4', 'prop 1', 'prop 2', 'prop 3', 'static prop'].sort() + assert sorted(g.edge(1, 2).properties.keys()) == sorted( + ["prop 4", "prop 1", "prop 2", "prop 3", "static prop"] + ) - assert g.edge(1, 2).property_names(include_static=False).sort() == ['prop 4', 'prop 1', 'prop 2', 'prop 3'].sort() + assert sorted(g.edge(1, 2).properties.temporal.keys()) == sorted( + ["prop 4", "prop 1", "prop 2", "prop 3"] + ) - assert g.at(1).edge(1, 2).property_names(include_static=False).sort() == ['prop 4', 'prop 1', 'prop 2', - 'prop 3'].sort() + assert sorted(g.at(1).edge(1, 2).properties.temporal.keys()) == sorted( + ["prop 4", "prop 1", "prop 3"] + ) # testing has_property - assert g.edge(1, 2).has_property("prop 4") - assert g.edge(1, 2).has_property("prop 2") - assert not g.edge(1, 2).has_property("prop 5") - assert not g.at(1).edge(1, 2).has_property("prop 2") - assert g.edge(1, 2).has_property("static prop") - assert g.at(1).edge(1, 2).has_property("static prop") - assert not g.at(1).edge(1, 2).has_property("static prop", include_static=False) + assert "prop 4" in g.edge(1, 2).properties + assert "prop 2" in g.edge(1, 2).properties + assert "prop 5" not in g.edge(1, 2).properties + assert "prop 2" not in g.at(1).edge(1, 2).properties + assert "static prop" in g.edge(1, 2).properties + assert "static prop" in g.at(1).edge(1, 2).properties + assert "static prop" not in g.at(1).edge(1, 2).properties.temporal + + assert "static prop" in g.edge(1, 2).properties.constant + assert "prop 2" not in g.edge(1, 2).properties.constant + assert "static prop" in g.at(1).edge(1, 2).properties.constant + + +def test_graph_as_property(): + g = Graph() + g.add_edge(0, 1, 2, {"graph": g}) + assert "graph" in g.edge(1, 2).properties + assert g.edge(1, 2).properties["graph"].has_edge(1, 2) + - assert g.edge(1, 2).has_static_property("static prop") - assert not g.edge(1, 2).has_static_property("prop 2") - assert g.at(1).edge(1, 2).has_static_property("static prop") +def test_map_and_list_property(): + g = Graph() + g.add_edge(0, 1, 2, {"map": {"test": 1, "list": [1, 2, 3]}}) + e_props = g.edge(1, 2).properties + assert "map" in e_props + assert e_props["map"]["test"] == 1 + assert e_props["map"]["list"] == [1, 2, 3] def test_exploded_edge_time(): @@ -509,15 +778,12 @@ def test_exploded_edge_time(): his = e.history() exploded_his = [] for ee in e.explode(): - exploded_his.append(ee.time()) + exploded_his.append(ee.time) assert his == exploded_his -# assert g.vertex(1).property_history("prop 3") == [(1, 3), (3, 'hello')] - - def test_algorithms(): - g = Graph(1) + g = Graph() lotr_graph = graph_loader.lotr_graph() g.add_edge(1, 1, 2, {"prop1": 1}) g.add_edge(2, 2, 3, {"prop1": 1}) @@ -542,39 +808,42 @@ def test_algorithms(): assert min_in_degree == 1 assert clustering_coefficient == 1.0 - lotr_clustering_coefficient = algorithms.local_clustering_coefficient(lotr_graph, 'Frodo') - lotr_local_triangle_count = algorithms.local_triangle_count(lotr_graph, 'Frodo') + lotr_clustering_coefficient = algorithms.local_clustering_coefficient( + lotr_graph, "Frodo" + ) + lotr_local_triangle_count = algorithms.local_triangle_count(lotr_graph, "Frodo") assert lotr_clustering_coefficient == 0.1984313726425171 assert lotr_local_triangle_count == 253 def test_graph_time_api(): - g = create_graph(1) + g = create_graph() - earliest_time = g.earliest_time() - latest_time = g.latest_time() + earliest_time = g.earliest_time + latest_time = g.latest_time assert len(list(g.rolling(1))) == latest_time - earliest_time + 1 - assert len(list(g.expanding(2))) == (latest_time - earliest_time) / 2 + assert len(list(g.expanding(2))) == math.ceil((latest_time + 1 - earliest_time) / 2) w = g.window(2, 6) - assert len(list(w.rolling(window=10, step=3))) == 1 + assert len(list(w.rolling(window=10, step=3))) == 2 def test_save_load_graph(): - g = create_graph(1) + g = create_graph() g.add_vertex(1, 11, {"type": "wallet", "balance": 99.5}) g.add_vertex(2, 12, {"type": "wallet", "balance": 10.0}) - g.add_vertex(3, 13, {"type": "wallet", "balance": 76}) + g.add_vertex(3, 13, {"type": "wallet", "balance": 76.0}) g.add_edge(4, 11, 12, {"prop1": 1, "prop2": 9.8, "prop3": "test"}) g.add_edge(5, 12, 13, {"prop1": 1321, "prop2": 9.8, "prop3": "test"}) g.add_edge(6, 13, 11, {"prop1": 645, "prop2": 9.8, "prop3": "test"}) tmpdirname = tempfile.TemporaryDirectory() - g.save_to_file(tmpdirname.name) + graph_path = tmpdirname.name + "/test_graph.bin" + g.save_to_file(graph_path) - del (g) + del g - g = Graph.load_from_file(tmpdirname.name) + g = Graph.load_from_file(graph_path) view = g.window(0, 10) assert g.has_vertex(13) @@ -582,17 +851,19 @@ def test_save_load_graph(): assert view.vertex(13).out_degree() == 1 assert view.vertex(13).degree() == 2 - triangles = algorithms.local_triangle_count(view, 13) # How many triangles is 13 involved in + triangles = algorithms.local_triangle_count( + view, 13 + ) # How many triangles is 13 involved in assert triangles == 1 v = view.vertex(11) - assert v.property_histories() == {'type': [(1, 'wallet')], 'balance': [(1, 99.5)]} + assert v.properties.temporal == {"type": [(1, "wallet")], "balance": [(1, 99.5)]} tmpdirname.cleanup() def test_graph_at(): - g = create_graph(1) + g = create_graph() view = g.at(2) assert view.vertex(1).degree() == 3 @@ -603,7 +874,7 @@ def test_graph_at(): def test_add_node_string(): - g = Graph(1) + g = Graph() g.add_vertex(0, 1, {}) g.add_vertex(1, "haaroon", {}) @@ -614,7 +885,7 @@ def test_add_node_string(): def test_add_edge_string(): - g = Graph(1) + g = Graph() g.add_edge(0, 1, 2, {}) g.add_edge(1, "haaroon", "ben", {}) @@ -629,7 +900,7 @@ def test_add_edge_string(): def test_all_neighbours_window(): - g = Graph(4) + g = Graph() g.add_edge(1, 1, 2, {}) g.add_edge(1, 2, 3, {}) g.add_edge(2, 3, 2, {}) @@ -638,13 +909,13 @@ def test_all_neighbours_window(): view = g.at(2) v = view.vertex(2) - assert list(v.window(0, 2).in_neighbours().id()) == [1] - assert list(v.window(0, 2).out_neighbours().id()) == [3] - assert list(v.window(0, 2).neighbours().id()) == [1, 3] + assert list(v.window(0, 2).in_neighbours.id) == [1] + assert list(v.window(0, 2).out_neighbours.id) == [3] + assert list(v.window(0, 2).neighbours.id) == [1, 3] def test_all_degrees_window(): - g = Graph(4) + g = Graph() g.add_edge(1, 1, 2, {}) g.add_edge(1, 2, 3, {}) g.add_edge(2, 3, 2, {}) @@ -667,7 +938,7 @@ def test_all_degrees_window(): def test_all_edge_window(): - g = Graph(4) + g = Graph() g.add_edge(1, 1, 2, {}) g.add_edge(1, 2, 3, {}) g.add_edge(2, 3, 2, {}) @@ -678,24 +949,38 @@ def test_all_edge_window(): view = g.at(4) v = view.vertex(2) - assert sorted(v.window(0, 4).in_edges().src().id()) == [1, 3, 4] - assert sorted(v.window(t_end=4).in_edges().src().id()) == [1, 3, 4] - assert sorted(v.window(t_start=2).in_edges().src().id()) == [3, 4] - assert sorted(v.window(0, 4).out_edges().dst().id()) == [3] - assert sorted(v.window(t_end=3).out_edges().dst().id()) == [3] - assert sorted(v.window(t_start=2).out_edges().dst().id()) == [4] - assert sorted((e.src().id(), e.dst().id()) for e in v.window(0, 4).edges()) == [(1, 2), (2, 3), (3, 2), (4, 2)] - assert sorted((e.src().id(), e.dst().id()) for e in v.window(t_end=4).edges()) == [(1, 2), (2, 3), (3, 2), (4, 2)] - assert sorted((e.src().id(), e.dst().id()) for e in v.window(t_start=1).edges()) == [(1, 2), (2, 3), (2, 4), (3, 2), - (4, 2)] + assert sorted(v.window(0, 4).in_edges.src.id) == [1, 3, 4] + assert sorted(v.window(t_end=4).in_edges.src.id) == [1, 3, 4] + assert sorted(v.window(t_start=2).in_edges.src.id) == [3, 4] + assert sorted(v.window(0, 4).out_edges.dst.id) == [3] + assert sorted(v.window(t_end=3).out_edges.dst.id) == [3] + assert sorted(v.window(t_start=2).out_edges.dst.id) == [4] + assert sorted((e.src.id, e.dst.id) for e in v.window(0, 4).edges) == [ + (1, 2), + (2, 3), + (3, 2), + (4, 2), + ] + assert sorted((e.src.id, e.dst.id) for e in v.window(t_end=4).edges) == [ + (1, 2), + (2, 3), + (3, 2), + (4, 2), + ] + assert sorted((e.src.id, e.dst.id) for e in v.window(t_start=1).edges) == [ + (1, 2), + (2, 3), + (2, 4), + (3, 2), + (4, 2), + ] def test_static_prop_change(): # with pytest.raises(Exception): - g = Graph(1) - - g.add_edge(0, 1, 2, {}) - g.add_vertex_properties(1, {"name": "value1"}) + g = Graph() + v = g.add_vertex(0, 1) + v.add_constant_properties({"name": "value1"}) expected_msg = ( """Exception: Failed to mutate graph\n""" @@ -707,11 +992,11 @@ def test_static_prop_change(): # with pytest.raises(Exception, match=re.escape(expected_msg)): with pytest.raises(Exception): - g.add_vertex_properties(1, {"name": "value2"}) + v.add_constant_properties({"name": "value2"}) def test_triplet_count(): - g = Graph(1) + g = Graph() g.add_edge(0, 1, 2, {}) g.add_edge(0, 2, 3, {}) @@ -722,7 +1007,7 @@ def test_triplet_count(): def test_global_clustering_coeffficient(): - g = Graph(1) + g = Graph() g.add_edge(0, 1, 2, {}) g.add_edge(0, 2, 3, {}) @@ -736,7 +1021,7 @@ def test_global_clustering_coeffficient(): def test_edge_time_apis(): - g = Graph(1) + g = Graph() g.add_edge(1, 1, 2, {"prop2": 10}) g.add_edge(2, 2, 4, {"prop2": 11}) @@ -747,34 +1032,34 @@ def test_edge_time_apis(): e = g.edge(1, 2) for e in e.expanding(1): - assert e.src().name() == '1' - assert e.dst().name() == '2' + assert e.src.name == "1" + assert e.dst.name == "2" ls = [] - for e in v.edges(): - ls.append(e.src().name()) - ls.append(e.dst().name()) + for e in v.edges: + ls.append(e.src.name) + ls.append(e.dst.name) - assert ls == ['1', '2', '1', '5'] + assert ls == ["1", "2", "1", "5"] v = g.vertex(2) ls = [] - for e in v.in_edges(): - ls.append(e.src().name()) - ls.append(e.dst().name()) + for e in v.in_edges: + ls.append(e.src.name) + ls.append(e.dst.name) - assert ls == ['1', '2'] + assert ls == ["1", "2"] ls = [] - for e in v.out_edges(): - ls.append(e.src().name()) - ls.append(e.dst().name()) + for e in v.out_edges: + ls.append(e.src.name) + ls.append(e.dst.name) - assert ls == ['2', '4'] + assert ls == ["2", "4"] def test_edge_earliest_latest_time(): - g = Graph(1) + g = Graph() g.add_edge(0, 1, 2, {}) g.add_edge(1, 1, 2, {}) g.add_edge(2, 1, 2, {}) @@ -782,31 +1067,31 @@ def test_edge_earliest_latest_time(): g.add_edge(1, 1, 3, {}) g.add_edge(2, 1, 3, {}) - assert g.edge(1, 2).earliest_time() == 0 - assert g.edge(1, 2).latest_time() == 2 + assert g.edge(1, 2).earliest_time == 0 + assert g.edge(1, 2).latest_time == 2 - assert list(g.vertex(1).edges().earliest_time()) == [0, 0] - assert list(g.vertex(1).edges().latest_time()) == [2, 2] - assert list(g.vertex(1).at(1).edges().earliest_time()) == [0, 0] - assert list(g.vertex(1).at(1).edges().latest_time()) == [1, 1] + assert list(g.vertex(1).edges.earliest_time) == [0, 0] + assert list(g.vertex(1).edges.latest_time) == [2, 2] + assert list(g.vertex(1).at(1).edges.earliest_time) == [0, 0] + assert list(g.vertex(1).at(1).edges.latest_time) == [1, 1] def test_vertex_earliest_time(): - g = Graph(1) + g = Graph() g.add_vertex(0, 1, {}) g.add_vertex(1, 1, {}) g.add_vertex(2, 1, {}) view = g.at(1) - assert view.vertex(1).earliest_time() == 0 - assert view.vertex(1).latest_time() == 1 + assert view.vertex(1).earliest_time == 0 + assert view.vertex(1).latest_time == 1 view = g.at(3) - assert view.vertex(1).earliest_time() == 0 - assert view.vertex(1).latest_time() == 2 + assert view.vertex(1).earliest_time == 0 + assert view.vertex(1).latest_time == 2 def test_vertex_history(): - g = Graph(1) + g = Graph() g.add_vertex(1, 1, {}) g.add_vertex(2, 1, {}) @@ -819,17 +1104,17 @@ def test_vertex_history(): g.add_vertex(7, "Lord Farquaad", {}) g.add_vertex(8, "Lord Farquaad", {}) - assert (g.vertex(1).history() == [1, 2, 3, 4, 8]) - assert (g.vertex("Lord Farquaad").history() == [4, 6, 7, 8]) + assert g.vertex(1).history() == [1, 2, 3, 4, 8] + assert g.vertex("Lord Farquaad").history() == [4, 6, 7, 8] view = g.window(1, 8) - assert (view.vertex(1).history() == [1, 2, 3, 4]) - assert (view.vertex("Lord Farquaad").history() == [4, 6, 7]) + assert view.vertex(1).history() == [1, 2, 3, 4] + assert view.vertex("Lord Farquaad").history() == [4, 6, 7] def test_edge_history(): - g = Graph(1) + g = Graph() g.add_edge(1, 1, 2) g.add_edge(2, 1, 3) @@ -838,30 +1123,86 @@ def test_edge_history(): view = g.window(1, 5) - assert (g.edge(1, 2).history() == [1, 3]) - - # also needs to be fixed in Pedros PR - # assert(view.edge(1, 4).history() == [4]) + assert g.edge(1, 2).history() == [1, 3] + assert view.edge(1, 4).history() == [4] def test_lotr_edge_history(): g = graph_loader.lotr_graph() - assert (g.edge('Frodo', 'Gandalf').history() == [329, 555, 861, 1056, 1130, 1160, 1234, 1241, 1390, 1417, 1656, - 1741, 1783, 1785, 1792, 1804, 1809, 1999, 2056, 2254, 2925, 2999, - 3703, 3914, 4910, 5620, 5775, 6381, 6531, 6578, 6661, 6757, 7041, - 7356, 8183, 8190, 8276, 8459, 8598, 8871, 9098, 9343, 9903, 11189, - 11192, 11279, 11365, 14364, 21551, 21706, 23212, 26958, 27060, - 29024, 30173, 30737, 30744, 31023, 31052, 31054, 31103, 31445, - 32656]) - assert (g.at(1000).edge('Frodo', 'Gandalf').history() == [329, 555, 861]) - assert (g.edge('Frodo', 'Gandalf').at(1000).history() == [329, 555, 861]) - assert (g.window(100, 1000).edge('Frodo', 'Gandalf').history() == [329, 555, 861]) - assert (g.edge('Frodo', 'Gandalf').window(100, 1000).history() == [329, 555, 861]) + assert g.edge("Frodo", "Gandalf").history() == [ + 329, + 555, + 861, + 1056, + 1130, + 1160, + 1234, + 1241, + 1390, + 1417, + 1656, + 1741, + 1783, + 1785, + 1792, + 1804, + 1809, + 1999, + 2056, + 2254, + 2925, + 2999, + 3703, + 3914, + 4910, + 5620, + 5775, + 6381, + 6531, + 6578, + 6661, + 6757, + 7041, + 7356, + 8183, + 8190, + 8276, + 8459, + 8598, + 8871, + 9098, + 9343, + 9903, + 11189, + 11192, + 11279, + 11365, + 14364, + 21551, + 21706, + 23212, + 26958, + 27060, + 29024, + 30173, + 30737, + 30744, + 31023, + 31052, + 31054, + 31103, + 31445, + 32656, + ] + assert g.at(1000).edge("Frodo", "Gandalf").history() == [329, 555, 861] + assert g.edge("Frodo", "Gandalf").at(1000).history() == [329, 555, 861] + assert g.window(100, 1000).edge("Frodo", "Gandalf").history() == [329, 555, 861] + assert g.edge("Frodo", "Gandalf").window(100, 1000).history() == [329, 555, 861] -def test_connected_components(): - g = Graph(1) +def gen_graph(): + g = Graph() g.add_edge(10, 1, 3, {}) g.add_edge(11, 1, 2, {}) g.add_edge(12, 1, 2, {}) @@ -874,66 +1215,112 @@ def test_connected_components(): g.add_edge(15, 4, 7, {}) g.add_edge(10, 4, 7, {}) g.add_edge(10, 5, 8, {}) + return g + +def test_connected_components(): + g = gen_graph() actual = algorithms.weakly_connected_components(g, 20) - expected = {'1': 1, '2': 1, '3': 1, '4': 1, '5': 1, '6': 1, '7': 1, '8': 1} - assert (actual == expected) + expected = {"1": 1, "2": 1, "3": 1, "4": 1, "5": 1, "6": 1, "7": 1, "8": 1} + assert actual.get_all() == expected + assert actual.get("1") == 1 -def test_page_rank(): - g = Graph(1) - g.add_edge(10, 1, 3, {}) - g.add_edge(11, 1, 2, {}) - g.add_edge(12, 1, 2, {}) - g.add_edge(9, 1, 2, {}) - g.add_edge(12, 2, 4, {}) - g.add_edge(13, 2, 5, {}) - g.add_edge(14, 5, 5, {}) - g.add_edge(14, 5, 4, {}) - g.add_edge(5, 4, 6, {}) - g.add_edge(15, 4, 7, {}) - g.add_edge(10, 4, 7, {}) - g.add_edge(10, 5, 8, {}) +def test_algo_result(): + g = gen_graph() + + actual = algorithms.weakly_connected_components(g, 20) + expected = {"1": 1, "2": 1, "3": 1, "4": 1, "5": 1, "6": 1, "7": 1, "8": 1} + assert actual.get_all() == expected + assert actual.get("1") == 1 + assert actual.get("not a node") == None + expected_array = [ + ("1", 1), + ("2", 1), + ("3", 1), + ("4", 1), + ("5", 1), + ("6", 1), + ("7", 1), + ("8", 1), + ] + assert sorted(actual.sort_by_value()) == expected_array + assert actual.sort_by_key() == sorted(expected_array, reverse=True) + assert actual.sort_by_key(reverse=False) == expected_array + assert sorted(actual.top_k(8)) == expected_array + assert len(actual.group_by()[1]) == 8 + assert type(actual.to_df()) == pandas.core.frame.DataFrame + df = actual.to_df() + expected_result = pd.DataFrame({"Key": ["1"], "Value": [1]}) + row_with_one = df[df["Key"] == "1"] + row_with_one.reset_index(inplace=True, drop=True) + assert row_with_one.equals(expected_result) + # Algo Str u64 + actual = algorithms.weakly_connected_components(g) + all_res = actual.get_all() + sorted_res = {k: all_res[k] for k in sorted(all_res)} + assert sorted_res == { + "1": 1, + "2": 1, + "3": 1, + "4": 1, + "5": 1, + "6": 1, + "7": 1, + "8": 1, + } + # algo str f64 + actual = algorithms.pagerank(g) + expected_result = { + "3": 0.10274080842110422, + "2": 0.10274080842110422, + "4": 0.1615298183542792, + "6": 0.14074777909144864, + "1": 0.07209850165402759, + "5": 0.1615298183542792, + "7": 0.14074777909144864, + "8": 0.11786468661230831, + } + assert actual.get_all() == expected_result + assert actual.get("Not a node") == None + assert len(actual.to_df()) == 8 + # algo str vector + actual = algorithms.temporally_reachable_nodes(g, 20, 11, [1, 2], [4, 5]) + assert sorted(actual.get_all()) == ["1", "2", "3", "4", "5", "6", "7", "8"] - actual = algorithms.pagerank(g, 20) + +def test_page_rank(): + g = gen_graph() + actual = algorithms.pagerank(g) expected = { - '1': 0.07209850165402759, - '2': 0.10274080842110422, - '3': 0.10274080842110422, - '4': 0.1615298183542792, - '5': 0.1615298183542792, - '6': 0.14074777909144864, - '7': 0.14074777909144864, - '8': 0.11786468661230831, + "1": 0.07209850165402759, + "2": 0.10274080842110422, + "3": 0.10274080842110422, + "4": 0.1615298183542792, + "5": 0.1615298183542792, + "6": 0.14074777909144864, + "7": 0.14074777909144864, + "8": 0.11786468661230831, } - assert (actual == expected) + assert actual.get_all() == expected -def test_generic_taint(): - g = Graph(1) - g.add_edge(10, 1, 3, {}) - g.add_edge(11, 1, 2, {}) - g.add_edge(12, 1, 2, {}) - g.add_edge(9, 1, 2, {}) - g.add_edge(12, 2, 4, {}) - g.add_edge(13, 2, 5, {}) - g.add_edge(14, 5, 5, {}) - g.add_edge(14, 5, 4, {}) - g.add_edge(5, 4, 6, {}) - g.add_edge(15, 4, 7, {}) - g.add_edge(10, 4, 7, {}) - g.add_edge(10, 5, 8, {}) +def test_temporal_reachability(): + g = gen_graph() - actual = algorithms.generic_taint(g, 20, 11, [1, 2], [4, 5]) + actual = algorithms.temporally_reachable_nodes(g, 20, 11, [1, 2], [4, 5]) expected = { - '1': [(11, 'start')], - '2': [(11, 'start'), (12, '1'), (11, '1')], - '3': [], - '4': [(12, '2')], - '5': [(13, '2')], + "1": [(11, "start")], + "2": [(11, "start"), (12, "1"), (11, "1")], + "3": [], + "4": [(12, "2")], + "5": [(13, "2")], + "6": [], + "7": [], + "8": [], } - assert (actual == expected) + assert actual.get_all() == expected # def test_generic_taint_loader(): @@ -956,19 +1343,36 @@ def test_generic_taint(): def test_layer(): - g = Graph(1) + g = Graph() g.add_edge(0, 1, 2) - g.add_edge(0, 1, 3, layer='layer1') - g.add_edge(0, 1, 4, layer='layer2') + g.add_edge(0, 1, 3, layer="layer1") + g.add_edge(0, 1, 4, layer="layer2") + + assert g.default_layer().count_edges() == 1 + assert g.layers(["layer1"]).count_edges() == 1 + assert g.layers(["layer2"]).count_edges() == 1 + - assert (g.default_layer().num_edges() == 1) - assert (g.layer('layer1').num_edges() == 1) - assert (g.layer('layer2').num_edges() == 1) +def test_layer_vertex(): + g = Graph() + + g.add_edge(0, 1, 2, layer="layer1") + g.add_edge(0, 2, 3, layer="layer2") + g.add_edge(3, 2, 4, layer="layer1") + neighbours = g.layers(["layer1", "layer2"]).vertex(1).neighbours.collect() + assert sorted(neighbours[0].layers(["layer2"]).edges.id) == [(2, 3)] + assert sorted(g.layers(["layer2"]).vertex(neighbours[0].name).edges.id) == [(2, 3)] + assert sorted(g.layers(["layer1"]).vertex(neighbours[0].name).edges.id) == [ + (1, 2), + (2, 4), + ] + assert sorted(g.layers(["layer1"]).edges().id) == [(1, 2), (2, 4)] + assert sorted(g.layers(["layer1", "layer2"]).edges().id) == [(1, 2), (2, 3), (2, 4)] def test_rolling_as_iterable(): - g = Graph(1) + g = Graph() g.add_vertex(1, 1) g.add_vertex(4, 4) @@ -977,25 +1381,25 @@ def test_rolling_as_iterable(): # a normal operation is reusing the object returned by rolling twice, to get both results and an index. # So the following should work fine: - n_vertices = [w.num_vertices() for w in rolling] - time_index = [w.start() for w in rolling] + n_vertices = [w.count_vertices() for w in rolling] + time_index = [w.start for w in rolling] assert n_vertices == [1, 0, 0, 1] assert time_index == [1, 2, 3, 4] def test_layer_name(): - g = Graph(4) + g = Graph() g.add_edge(0, 0, 1) g.add_edge(0, 0, 2, layer="awesome layer") - assert g.edge(0, 1).layer_name() == "default layer" - assert g.edge(0, 2, "awesome layer").layer_name() == "awesome layer" + assert g.edge(0, 1).layer_names == ["_default"] + assert g.edge(0, 2).layer_names == ["awesome layer"] def test_window_size(): - g = Graph(4) + g = Graph() g.add_vertex(1, 1) g.add_vertex(4, 4) @@ -1003,12 +1407,15 @@ def test_window_size(): def test_time_index(): - g = Graph(4) + g = Graph() w = g.window("2020-01-01", "2020-01-03") rolling = w.rolling("1 day") time_index = rolling.time_index() - assert list(time_index) == [datetime.datetime(2020, 1, 1, 23, 59, 59, 999000), datetime.datetime(2020, 1, 2, 23, 59, 59, 999000)] + assert list(time_index) == [ + datetime.datetime(2020, 1, 1, 23, 59, 59, 999000), + datetime.datetime(2020, 1, 2, 23, 59, 59, 999000), + ] w = g.window(1, 3) rolling = w.rolling(1) @@ -1022,89 +1429,663 @@ def test_time_index(): def test_datetime_props(): - g = Graph(4) + g = Graph() dt1 = datetime.datetime(2020, 1, 1, 23, 59, 59, 999000) g.add_vertex(0, 0, {"time": dt1}) - assert g.vertex(0).property("time") == dt1 + assert g.vertex(0).properties.get("time") == dt1 dt2 = datetime.datetime(2020, 1, 1, 23, 59, 59, 999999) g.add_vertex(0, 1, {"time": dt2}) - assert g.vertex(1).property("time") == dt2 + assert g.vertex(1).properties.get("time") == dt2 + - def test_date_time(): - g = Graph(1) + g = Graph() - g.add_edge('2014-02-02', 1, 2) - g.add_edge('2014-02-03', 1, 3) - g.add_edge('2014-02-04', 1, 4) - g.add_edge('2014-02-05', 1, 2) + g.add_edge("2014-02-02", 1, 2) + g.add_edge("2014-02-03", 1, 3) + g.add_edge("2014-02-04", 1, 4) + g.add_edge("2014-02-05", 1, 2) - assert g.earliest_date_time() == datetime.datetime(2014, 2, 2, 0, 0) - assert g.latest_date_time() == datetime.datetime(2014, 2, 5, 0, 0) + assert g.earliest_date_time == datetime.datetime(2014, 2, 2, 0, 0) + assert g.latest_date_time == datetime.datetime(2014, 2, 5, 0, 0) e = g.edge(1, 3) exploded_edges = [] for edge in e.explode(): - exploded_edges.append(edge.date_time()) + exploded_edges.append(edge.date_time) assert exploded_edges == [datetime.datetime(2014, 2, 3)] - assert g.edge(1, 2).earliest_date_time() == datetime.datetime(2014, 2, 2, 0, 0) - assert g.edge(1, 2).latest_date_time() == datetime.datetime(2014, 2, 5, 0, 0) + assert g.edge(1, 2).earliest_date_time == datetime.datetime(2014, 2, 2, 0, 0) + assert g.edge(1, 2).latest_date_time == datetime.datetime(2014, 2, 5, 0, 0) + + assert g.vertex(1).earliest_date_time == datetime.datetime(2014, 2, 2, 0, 0) + assert g.vertex(1).latest_date_time == datetime.datetime(2014, 2, 5, 0, 0) - assert g.vertex(1).earliest_date_time() == datetime.datetime(2014, 2, 2, 0, 0) - assert g.vertex(1).latest_date_time() == datetime.datetime(2014, 2, 5, 0, 0) def test_date_time_window(): - g = Graph(1) + g = Graph() - g.add_edge('2014-02-02', 1, 2) - g.add_edge('2014-02-03', 1, 3) - g.add_edge('2014-02-04', 1, 4) - g.add_edge('2014-02-05', 1, 2) - g.add_edge('2014-02-06', 1, 2) + g.add_edge("2014-02-02", 1, 2) + g.add_edge("2014-02-03", 1, 3) + g.add_edge("2014-02-04", 1, 4) + g.add_edge("2014-02-05", 1, 2) + g.add_edge("2014-02-06", 1, 2) - view = g.window('2014-02-02', '2014-02-04') - view2 = g.window('2014-02-02', '2014-02-05') + view = g.window("2014-02-02", "2014-02-04") + view2 = g.window("2014-02-02", "2014-02-05") - assert view.start_date_time() == datetime.datetime(2014, 2, 2, 0, 0) - assert view.end_date_time() == datetime.datetime(2014, 2, 4, 0, 0) + assert view.start_date_time == datetime.datetime(2014, 2, 2, 0, 0) + assert view.end_date_time == datetime.datetime(2014, 2, 4, 0, 0) - assert view.earliest_date_time() == datetime.datetime(2014, 2, 2, 0, 0) - assert view.latest_date_time() == datetime.datetime(2014, 2, 4, 0, 0) + assert view.earliest_date_time == datetime.datetime(2014, 2, 2, 0, 0) + assert view.latest_date_time == datetime.datetime(2014, 2, 3, 0, 0) - assert view2.edge(1, 2).start_date_time() == datetime.datetime(2014, 2, 2, 0, 0) - assert view2.edge(1, 2).end_date_time() == datetime.datetime(2014, 2, 5, 0, 0) + assert view2.edge(1, 2).start_date_time == datetime.datetime(2014, 2, 2, 0, 0) + assert view2.edge(1, 2).end_date_time == datetime.datetime(2014, 2, 5, 0, 0) - assert view.vertex(1).earliest_date_time() == datetime.datetime(2014, 2, 2, 0, 0) - assert view.vertex(1).latest_date_time() == datetime.datetime(2014, 2, 3, 0, 0) + assert view.vertex(1).earliest_date_time == datetime.datetime(2014, 2, 2, 0, 0) + assert view.vertex(1).latest_date_time == datetime.datetime(2014, 2, 3, 0, 0) e = view.edge(1, 2) exploded_edges = [] for edge in e.explode(): - exploded_edges.append(edge.date_time()) + exploded_edges.append(edge.date_time) assert exploded_edges == [datetime.datetime(2014, 2, 2)] def test_datetime_add_vertex(): - g = Graph(1) + g = Graph() g.add_vertex(datetime.datetime(2014, 2, 2), 1) g.add_vertex(datetime.datetime(2014, 2, 3), 2) g.add_vertex(datetime.datetime(2014, 2, 4), 2) g.add_vertex(datetime.datetime(2014, 2, 5), 4) g.add_vertex(datetime.datetime(2014, 2, 6), 5) - view = g.window('2014-02-02', '2014-02-04') - view2 = g.window('2014-02-02', '2014-02-05') + view = g.window("2014-02-02", "2014-02-04") + view2 = g.window("2014-02-02", "2014-02-05") + + assert view.start_date_time == datetime.datetime(2014, 2, 2, 0, 0) + assert view.end_date_time == datetime.datetime(2014, 2, 4, 0, 0) + + assert view2.earliest_date_time == datetime.datetime(2014, 2, 2, 0, 0) + assert view2.latest_date_time == datetime.datetime(2014, 2, 4, 0, 0) + + assert view2.vertex(1).start_date_time == datetime.datetime(2014, 2, 2, 0, 0) + assert view2.vertex(1).end_date_time == datetime.datetime(2014, 2, 5, 0, 0) + + assert view.vertex(2).earliest_date_time == datetime.datetime(2014, 2, 3, 0, 0) + assert view.vertex(2).latest_date_time == datetime.datetime(2014, 2, 3, 0, 0) + + +def test_equivalent_vertices_edges_and_sets(): + g = Graph() + g.add_vertex(1, 1) + g.add_vertex(1, 2) + g.add_vertex(1, 3) + + g.add_edge(1, 1, 2) + g.add_edge(1, 2, 3) + + assert g.vertex(1) == g.vertex(1) + assert list(g.vertex(1).neighbours)[0] == list(g.vertex(3).neighbours)[0] + assert set(g.vertex(1).neighbours) == set(g.vertex(3).neighbours) + assert set(g.vertex(1).out_edges) == set(g.vertex(2).in_edges) + + assert g.edge(1, 1) == g.edge(1, 1) + + +def test_subgraph(): + g = create_graph() + empty_graph = g.subgraph([]) + assert empty_graph.vertices.collect() == [] + + vertex1 = g.vertices[1] + subgraph = g.subgraph([vertex1]) + assert subgraph.vertices.collect() == [vertex1] + + subgraph_from_str = g.subgraph(["1"]) + assert subgraph_from_str.vertices.collect() == [vertex1] + + subgraph_from_int = g.subgraph([1]) + assert subgraph_from_int.vertices.collect() == [vertex1] + + mg = subgraph.materialize() + assert mg.vertices.collect()[0].properties["type"] == "wallet" + assert mg.vertices.collect()[0].name == "1" + + props = {"prop 4": 11, "prop 5": "world", "prop 6": False} + mg.add_property(1, props) + + props = {"prop 1": 1, "prop 2": "hi", "prop 3": True} + mg.add_constant_properties(props) + x = mg.properties.keys() + x.sort() + assert x == ["prop 1", "prop 2", "prop 3", "prop 4", "prop 5", "prop 6"] + + +def test_materialize_graph(): + g = Graph() + + edges = [(1, 1, 2), (2, 1, 3), (-1, 2, 1), (0, 1, 1), (7, 3, 2), (1, 1, 1)] + + g.add_vertex(0, 1, {"type": "wallet", "cost": 99.5}) + g.add_vertex(-1, 2, {"type": "wallet", "cost": 10.0}) + g.add_vertex(6, 3, {"type": "wallet", "cost": 76.0}) + g.add_vertex(6, 4).add_constant_properties({"abc": "xyz"}) + + for e in edges: + g.add_edge(e[0], e[1], e[2], {"prop1": 1, "prop2": 9.8, "prop3": "test"}) + + g.add_edge(8, 2, 4) + + sprop = {"sprop 1": "kaggle", "sprop 2": True} + g.add_constant_properties(sprop) + assert g.properties.constant == sprop + + mg = g.materialize() + + assert mg.vertex(1).properties.get("type") == "wallet" + assert mg.vertex(4).properties == {"abc": "xyz"} + assert mg.vertex(4).properties.constant.get("abc") == "xyz" + assert mg.vertex(1).history() == [-1, 0, 1, 2] + assert mg.vertex(4).history() == [6, 8] + assert mg.vertices.id.collect() == [1, 2, 3, 4] + assert set(mg.edges().id) == {(1, 1), (1, 2), (1, 3), (2, 1), (3, 2), (2, 4)} + assert g.vertices.id.collect() == mg.vertices.id.collect() + assert set(g.edges().id) == set(mg.edges().id) + assert mg.vertex(1).properties.constant == {} + assert mg.vertex(4).properties.constant == {"abc": "xyz"} + assert g.edge(1, 2).id == (1, 2) + assert mg.edge(1, 2).id == (1, 2) + assert mg.has_edge(1, 2) + assert g.has_edge(1, 2) + assert mg.has_edge(2, 1) + assert g.has_edge(2, 1) + + sprop2 = {"sprop 3": 11, "sprop 4": 10} + mg.add_constant_properties(sprop2) + sprop.update(sprop2) + assert mg.properties.constant == sprop + + +def test_deletions(): + g = create_graph_with_deletions() + for e in edges: + assert g.at(e[0]).has_edge(e[1], e[2]) + + assert not g.window(start=11).has_edge(edges[0][1], edges[0][2]) + for e in edges[1:]: + assert g.window(start=11).has_edge(e[1], e[2]) + + assert list(g.edge(edges[0][1], edges[0][2]).explode().latest_time) == [10] + + +def test_load_from_pandas(): + import pandas as pd + + df = pd.DataFrame( + { + "src": [1, 2, 3, 4, 5], + "dst": [2, 3, 4, 5, 6], + "time": [1, 2, 3, 4, 5], + "weight": [1.0, 2.0, 3.0, 4.0, 5.0], + "marbles": ["red", "blue", "green", "yellow", "purple"], + } + ) + + g = Graph.load_from_pandas(df, "src", "dst", "time", ["weight", "marbles"]) + + assert g.vertices.id.collect() == [1, 2, 3, 4, 5, 6] + edges = [] + for e in g.edges(): + weight = e["weight"] + marbles = e["marbles"] + edges.append((e.src.id, e.dst.id, weight, marbles)) + + assert edges == [ + (1, 2, 1.0, "red"), + (2, 3, 2.0, "blue"), + (3, 4, 3.0, "green"), + (4, 5, 4.0, "yellow"), + (5, 6, 5.0, "purple"), + ] + + +def test_load_from_pandas_into_existing_graph(): + edges_df = pd.DataFrame( + { + "src": [1, 2, 3, 4, 5], + "dst": [2, 3, 4, 5, 6], + "time": [1, 2, 3, 4, 5], + "weight": [1.0, 2.0, 3.0, 4.0, 5.0], + "marbles": ["red", "blue", "green", "yellow", "purple"], + } + ) + + vertices_df = pd.DataFrame( + { + "id": [1, 2, 3, 4, 5, 6], + "name": ["Alice", "Bob", "Carol", "Dave", "Eve", "Frank"], + "time": [1, 2, 3, 4, 5, 6], + } + ) + + g = Graph() + + g.load_vertices_from_pandas(vertices_df, "id", "time", ["name"]) + + g.load_edges_from_pandas(edges_df, "src", "dst", "time", ["weight", "marbles"]) + + assert g.vertices.id.collect() == [1, 2, 3, 4, 5, 6] + edges = [] + for e in g.edges(): + weight = e["weight"] + marbles = e["marbles"] + edges.append((e.src.id, e.dst.id, weight, marbles)) + + assert edges == [ + (1, 2, 1.0, "red"), + (2, 3, 2.0, "blue"), + (3, 4, 3.0, "green"), + (4, 5, 4.0, "yellow"), + (5, 6, 5.0, "purple"), + ] + + vertices = [] + for v in g.vertices: + name = v["name"] + vertices.append((v.id, name)) + + assert vertices == [ + (1, "Alice"), + (2, "Bob"), + (3, "Carol"), + (4, "Dave"), + (5, "Eve"), + (6, "Frank"), + ] + + +def test_load_from_pandas_vertices(): + edges_df = pd.DataFrame( + { + "src": [1, 2, 3, 4, 5], + "dst": [2, 3, 4, 5, 6], + "time": [1, 2, 3, 4, 5], + "weight": [1.0, 2.0, 3.0, 4.0, 5.0], + "marbles": ["red", "blue", "green", "yellow", "purple"], + } + ) + + vertices_df = pd.DataFrame( + { + "id": [1, 2, 3, 4, 5, 6], + "name": ["Alice", "Bob", "Carol", "Dave", "Eve", "Frank"], + "time": [1, 2, 3, 4, 5, 6], + } + ) + + g = Graph.load_from_pandas( + edges_df, + src="src", + dst="dst", + time="time", + props=["weight", "marbles"], + vertex_df=vertices_df, + vertex_col="id", + vertex_time_col="time", + vertex_props=["name"], + ) + + assert g.vertices.id.collect() == [1, 2, 3, 4, 5, 6] + edges = [] + for e in g.edges(): + weight = e["weight"] + marbles = e["marbles"] + edges.append((e.src.id, e.dst.id, weight, marbles)) + + assert edges == [ + (1, 2, 1.0, "red"), + (2, 3, 2.0, "blue"), + (3, 4, 3.0, "green"), + (4, 5, 4.0, "yellow"), + (5, 6, 5.0, "purple"), + ] + + vertices = [] + for v in g.vertices: + name = v["name"] + vertices.append((v.id, name)) + + assert vertices == [ + (1, "Alice"), + (2, "Bob"), + (3, "Carol"), + (4, "Dave"), + (5, "Eve"), + (6, "Frank"), + ] + + +def test_load_from_pandas_with_types(): + edges_df = pd.DataFrame( + { + "src": [1, 2, 3, 4, 5], + "dst": [2, 3, 4, 5, 6], + "time": [1, 2, 3, 4, 5], + "weight": [1.0, 2.0, 3.0, 4.0, 5.0], + "marbles": ["red", "blue", "green", "yellow", "purple"], + "marbles_const": ["red", "blue", "green", "yellow", "purple"], + "layers": ["layer 1", "layer 2", "layer 3", "layer 4", "layer 5"], + } + ) + vertices_df = pd.DataFrame( + { + "id": [1, 2, 3, 4, 5, 6], + "name": ["Alice", "Bob", "Carol", "Dave", "Eve", "Frank"], + "time": [1, 2, 3, 4, 5, 6], + "type": [ + "Person 1", + "Person 2", + "Person 3", + "Person 4", + "Person 5", + "Person 6", + ], + } + ) + g = Graph() + g.load_vertices_from_pandas( + vertices_df, + "id", + "time", + ["name"], + shared_const_props={"type": "Person", "tag": "test_tag"}, + ) + assert g.vertices.properties.constant.get("type").collect() == [ + "Person", + "Person", + "Person", + "Person", + "Person", + "Person", + ] + assert g.vertices.properties.constant.get("tag").collect() == [ + "test_tag", + "test_tag", + "test_tag", + "test_tag", + "test_tag", + "test_tag", + ] + + g = Graph() + g.load_vertices_from_pandas( + vertices_df, "id", "time", ["name"], const_props=["type"] + ) + assert g.vertices.properties.constant.get("type").collect() == [ + "Person 1", + "Person 2", + "Person 3", + "Person 4", + "Person 5", + "Person 6", + ] + + g = Graph() + g.load_edges_from_pandas( + edges_df, + "src", + "dst", + "time", + ["weight", "marbles"], + const_props=["marbles_const"], + shared_const_props={"type": "Edge", "tag": "test_tag"}, + layer="test_layer", + ) + + assert g.layers(["test_layer"]).edges().src.id.collect() == [1, 2, 3, 4, 5] + assert g.edges().properties.constant.get("type").collect() == [ + {"test_layer": "Edge"}, + {"test_layer": "Edge"}, + {"test_layer": "Edge"}, + {"test_layer": "Edge"}, + {"test_layer": "Edge"}, + ] + assert g.edges().properties.constant.get("tag").collect() == [ + {"test_layer": "test_tag"}, + {"test_layer": "test_tag"}, + {"test_layer": "test_tag"}, + {"test_layer": "test_tag"}, + {"test_layer": "test_tag"}, + ] + assert g.edges().properties.constant.get("marbles_const").collect() == [ + {"test_layer": "red"}, + {"test_layer": "blue"}, + {"test_layer": "green"}, + {"test_layer": "yellow"}, + {"test_layer": "purple"}, + ] + + g = Graph() + g.load_edges_from_pandas( + edges_df, "src", "dst", "time", ["weight", "marbles"], layer_in_df="layers" + ) + assert g.layers(["layer 1"]).edges().src.id.collect() == [1] + assert g.layers(["layer 1", "layer 2"]).edges().src.id.collect() == [1, 2] + assert g.layers(["layer 1", "layer 2", "layer 3"]).edges().src.id.collect() == [ + 1, + 2, + 3, + ] + assert g.layers(["layer 1", "layer 4", "layer 5"]).edges().src.id.collect() == [ + 1, + 4, + 5, + ] + + g = Graph.load_from_pandas( + edges_df, + "src", + "dst", + "time", + layer="test_layer", + vertex_df=vertices_df, + vertex_col="id", + vertex_time_col="time", + vertex_props=["name"], + vertex_shared_const_props={"type": "Person"}, + ) + assert g.vertices.properties.constant.get("type").collect() == [ + "Person", + "Person", + "Person", + "Person", + "Person", + "Person", + ] + assert g.layers(["test_layer"]).edges().src.id.collect() == [1, 2, 3, 4, 5] + + g = Graph.load_from_pandas( + edges_df, + "src", + "dst", + "time", + layer_in_df="layers", + vertex_df=vertices_df, + vertex_col="id", + vertex_time_col="time", + vertex_props=["name"], + vertex_const_props=["type"], + ) + assert g.vertices.properties.constant.get("type").collect() == [ + "Person 1", + "Person 2", + "Person 3", + "Person 4", + "Person 5", + "Person 6", + ] + assert g.layers(["layer 1"]).edges().src.id.collect() == [1] + assert g.layers(["layer 1", "layer 2"]).edges().src.id.collect() == [1, 2] + assert g.layers(["layer 1", "layer 2", "layer 3"]).edges().src.id.collect() == [ + 1, + 2, + 3, + ] + assert g.layers(["layer 1", "layer 4", "layer 5"]).edges().src.id.collect() == [ + 1, + 4, + 5, + ] + + g = Graph.load_from_pandas( + edges_df, + src="src", + dst="dst", + time="time", + props=["weight", "marbles"], + vertex_df=vertices_df, + vertex_col="id", + vertex_time_col="time", + vertex_props=["name"], + layer_in_df="layers", + ) + + g.load_vertex_props_from_pandas( + vertices_df, "id", const_props=["type"], shared_const_props={"tag": "test_tag"} + ) + assert g.vertices.properties.constant.get("type").collect() == [ + "Person 1", + "Person 2", + "Person 3", + "Person 4", + "Person 5", + "Person 6", + ] + assert g.vertices.properties.constant.get("tag").collect() == [ + "test_tag", + "test_tag", + "test_tag", + "test_tag", + "test_tag", + "test_tag", + ] + + g.load_edge_props_from_pandas( + edges_df, + "src", + "dst", + const_props=["marbles_const"], + shared_const_props={"tag": "test_tag"}, + layer_in_df="layers", + ) + assert g.layers(["layer 1", "layer 2", "layer 3"]).edges().properties.constant.get( + "marbles_const" + ).collect() == [{"layer 1": "red"}, {"layer 2": "blue"}, {"layer 3": "green"}] + assert g.edges().properties.constant.get("tag").collect() == [ + {"layer 1": "test_tag"}, + {"layer 2": "test_tag"}, + {"layer 3": "test_tag"}, + {"layer 4": "test_tag"}, + {"layer 5": "test_tag"}, + ] + + +def test_edge_layer(): + g = Graph() + g.add_edge(1, 1, 2, layer="layer 1").add_constant_properties( + {"test_prop": "test_val"} + ) + g.add_edge(1, 2, 3, layer="layer 2").add_constant_properties( + {"test_prop": "test_val 2"} + ) + assert g.edges().properties.constant.get("test_prop") == [ + {"layer 1": "test_val"}, + {"layer 2": "test_val 2"}, + ] + + +def test_layers_earliest_time(): + g = Graph() + e = g.add_edge(1, 1, 2, layer="test") + e = g.edge(1, 2) + print(e) + assert e.earliest_time == 1 + + +def test_layers_earliest_time(): + g = Graph() + e = g.add_edge(1, 1, 2, layer="test") + e = g.edge(1, 2) + print(e) + assert e.earliest_time == 1 + + +def test_edge_explode_layers(): + g = Graph() + g.add_edge(1, 1, 2, {"layer": 1}, layer="1") + g.add_edge(1, 1, 2, {"layer": 2}, layer="2") + g.add_edge(1, 2, 1, {"layer": 1}, layer="1") + g.add_edge(1, 2, 1, {"layer": 2}, layer="2") + + layered_edges = g.edge(1, 2).explode_layers() + e_layers = [ee.layer_names for ee in layered_edges] + e_layer_prop = [[str(ee.properties["layer"])] for ee in layered_edges] + assert e_layers == e_layer_prop + print(e_layers) + + nested_layered_edges = g.vertices.out_edges.explode_layers() + e_layers = [[ee.layer_names for ee in edges] for edges in nested_layered_edges] + e_layer_prop = [ + [[str(ee.properties["layer"])] for ee in layered_edges] + for layered_edges in nested_layered_edges + ] + assert e_layers == e_layer_prop + print(e_layers) + + print(g.vertices.out_neighbours.collect) + nested_layered_edges = g.vertices.out_neighbours.out_edges.explode_layers() + print(nested_layered_edges) + e_layers = [ + [ee.layer_names for ee in layered_edges] + for layered_edges in nested_layered_edges + ] + e_layer_prop = [ + [[str(ee.properties["layer"])] for ee in layered_edges] + for layered_edges in nested_layered_edges + ] + assert e_layers == e_layer_prop + print(e_layers) - assert view.start_date_time() == datetime.datetime(2014, 2, 2, 0, 0) - assert view.end_date_time() == datetime.datetime(2014, 2, 4, 0, 0) - assert view2.earliest_date_time() == datetime.datetime(2014, 2, 2, 0, 0) - assert view2.latest_date_time() == datetime.datetime(2014, 2, 5, 0, 0) +def test_hits_algorithm(): + g = graph_loader.lotr_graph() + assert algorithms.hits(g).get("Aldor") == ( + 0.0035840950440615416, + 0.007476256228983402, + ) + - assert view2.vertex(1).start_date_time() == datetime.datetime(2014, 2, 2, 0, 0) - assert view2.vertex(1).end_date_time() == datetime.datetime(2014, 2, 5, 0, 0) +def test_balance_algorithm(): + g = Graph() + edges_str = [ + ("1", "2", 10.0, 1), + ("1", "4", 20.0, 2), + ("2", "3", 5.0, 3), + ("3", "2", 2.0, 4), + ("3", "1", 1.0, 5), + ("4", "3", 10.0, 6), + ("4", "1", 5.0, 7), + ("1", "5", 2.0, 8), + ] + for src, dst, val, time in edges_str: + g.add_edge(time, src, dst, {"value_dec": val}) + result = algorithms.balance(g, "value_dec", PyDirection("BOTH"), None).get_all() + assert result == {"1": -26.0, "2": 7.0, "3": 12.0, "4": 5.0, "5": 2.0} - assert view.vertex(2).earliest_date_time() == datetime.datetime(2014, 2, 3, 0, 0) - assert view.vertex(2).latest_date_time() == datetime.datetime(2014, 2, 3, 0, 0) + result = algorithms.balance(g, "value_dec", PyDirection("IN"), None).get_all() + assert result == {"1": 6.0, "2": 12.0, "3": 15.0, "4": 20.0, "5": 2.0} + result = algorithms.balance(g, "value_dec", PyDirection("OUT"), None).get_all() + assert result == {"1": -32.0, "2": -5.0, "3": -3.0, "4": -15.0, "5": 0.0} diff --git a/python/tests/test_graphql.py b/python/tests/test_graphql.py new file mode 100644 index 0000000000..552e9901d5 --- /dev/null +++ b/python/tests/test_graphql.py @@ -0,0 +1,159 @@ +import sys +import tempfile + + +def test_graphQL(): + from raphtory import Graph + from raphtory import graphqlserver + import random + import string + import os + + g1 = Graph() + g1.add_edge(1, "ben", "hamza") + g1.add_edge(2, "haaroon", "hamza") + g1.add_edge(3, "ben", "haaroon") + + g2 = Graph() + g2 = Graph() + g2.add_edge(1, "Naomi", "Shivam") + g2.add_edge(2, "Shivam", "Pedro") + g2.add_edge(3, "Pedro", "Rachel") + graphs = {"g1": g1, "g2": g2} + + g3 = Graph() + g3.add_edge(1, "ben_saved", "hamza_saved") + g3.add_edge(2, "haaroon_saved", "hamza_saved") + g3.add_edge(3, "ben_saved", "haaroon_saved") + + g4 = Graph() + g4.add_edge(1, "Naomi_saved", "Shivam_saved") + g4.add_edge(2, "Shivam_saved", "Pedro_saved") + g4.add_edge(3, "Pedro_saved", "Rachel_saved") + + temp_dir = tempfile.mkdtemp() + + g3.save_to_file(temp_dir + "/g3") + g4.save_to_file(temp_dir + "/g4") + + map_server = graphqlserver.run_server(graphs=graphs, port=1736, daemon=True) + dir_server = graphqlserver.run_server(graph_dir=temp_dir, port=1737, daemon=True) + map_dir_server = graphqlserver.run_server( + graphs=graphs, graph_dir=temp_dir, port=1738, daemon=True + ) + + query_g1 = """{graph(name: "g1") {nodes {name}}}""" + query_g2 = """{graph(name: "g2") {nodes {name}}}""" + query_g3 = """{graph(name: "g3") {nodes {name}}}""" + query_g4 = """{graph(name: "g4") {nodes {name}}}""" + + assert str(map_server.query(query_g1)).replace( + " ", "" + ) == "{'graph': {'nodes': [{'name': 'ben'}, {'name': 'hamza'}, {'name': 'haaroon'}]}}".replace( + " ", "" + ) + assert str(map_server.query(query_g2)).replace( + " ", "" + ) == "{'graph': {'nodes': [{'name': 'Naomi'}, {'name': 'Shivam'}, {'name': 'Pedro'}, {'name': 'Rachel'}]}}".replace( + " ", "" + ) + assert str(dir_server.query(query_g3)).replace( + " ", "" + ) == "{'graph': {'nodes': [{'name': 'ben_saved'}, {'name': 'hamza_saved'}, {'name': 'haaroon_saved'}]}}".replace( + " ", "" + ) + assert str(dir_server.query(query_g4)).replace( + " ", "" + ) == "{'graph': {'nodes': [{'name': 'Naomi_saved'}, {'name': 'Shivam_saved'}, {'name': 'Pedro_saved'}, {'name': 'Rachel_saved'}]}}".replace( + " ", "" + ) + + assert str(map_dir_server.query(query_g1)).replace( + " ", "" + ) == "{'graph': {'nodes': [{'name': 'ben'}, {'name': 'hamza'}, {'name': 'haaroon'}]}}".replace( + " ", "" + ) + assert str(map_dir_server.query(query_g2)).replace( + " ", "" + ) == "{'graph': {'nodes': [{'name': 'Naomi'}, {'name': 'Shivam'}, {'name': 'Pedro'}, {'name': 'Rachel'}]}}".replace( + " ", "" + ) + assert str(map_dir_server.query(query_g4)).replace( + " ", "" + ) == "{'graph': {'nodes': [{'name': 'Naomi_saved'}, {'name': 'Shivam_saved'}, {'name': 'Pedro_saved'}, {'name': 'Rachel_saved'}]}}".replace( + " ", "" + ) + assert str(map_dir_server.query(query_g3)).replace( + " ", "" + ) == "{'graph': {'nodes': [{'name': 'ben_saved'}, {'name': 'hamza_saved'}, {'name': 'haaroon_saved'}]}}".replace( + " ", "" + ) + + +def test_graphqlclient(): + from raphtory import Graph + from raphtory import graphqlserver + from raphtory import graphqlclient + import os + + temp_dir = tempfile.mkdtemp() + + g1 = Graph() + g1.add_edge(1, "ben", "hamza") + g1.add_edge(2, "haaroon", "hamza") + g1.add_edge(3, "ben", "haaroon") + g1.save_to_file(temp_dir + "/g1.bincode") + + dir_server = graphqlserver.run_server(graph_dir=temp_dir, port=1739, daemon=True) + + # create a client + raphtory_client = graphqlclient.RaphtoryGraphQLClient(url="http://localhost:1739/") + + # load a graph into the client from a path + res = raphtory_client.load_graphs_from_path(temp_dir) + assert res == {"loadGraphsFromPath": ["g1.bincode"]} + + # run a get nodes query and check the results + query = """query GetNodes($graphname: String!) { + graph(name: $graphname) { + nodes { + name + } + } + }""" + variables = {"graphname": "g1.bincode"} + res = raphtory_client.query(query, variables) + assert res == { + "graph": {"nodes": [{"name": "ben"}, {"name": "hamza"}, {"name": "haaroon"}]} + } + + # load a new graph into the client from a path + multi_graph_temp_dir = tempfile.mkdtemp() + g2 = Graph() + g2.add_edge(1, "ben", "hamza") + g2.add_edge(2, "haaroon", "hamza") + g2.save_to_file(multi_graph_temp_dir + "/g2.bincode") + g3 = Graph() + g3.add_edge(1, "shivam", "rachel") + g3.add_edge(2, "lucas", "shivam") + g3.save_to_file(multi_graph_temp_dir + "/g3.bincode") + res = raphtory_client.load_new_graphs_from_path(multi_graph_temp_dir) + result_sorted = {"loadNewGraphsFromPath": sorted(res["loadNewGraphsFromPath"])} + assert result_sorted == {"loadNewGraphsFromPath": ["g2.bincode", "g3.bincode"]} + + # upload a graph + g4 = Graph() + g4.add_vertex(0, 1) + res = raphtory_client.send_graph("hello", g4) + assert res == {"sendGraph": "hello"} + # Ensure the sent graph can be queried + query = """query GetNodes($graphname: String!) { + graph(name: $graphname) { + nodes { + name + } + } + }""" + variables = {"graphname": "hello"} + res = raphtory_client.query(query, variables) + assert res == {"graph": {"nodes": [{"name": "1"}]}} diff --git a/python/tests/test_iterables.py b/python/tests/test_iterables.py new file mode 100644 index 0000000000..7a0045b700 --- /dev/null +++ b/python/tests/test_iterables.py @@ -0,0 +1,253 @@ +import math +import sys + +import pandas as pd +import pandas.core.frame +import pytest +from raphtory import Graph, GraphWithDeletions, PyDirection +from raphtory import algorithms +from raphtory import graph_loader +import tempfile +from math import isclose +import datetime + + +def test_pyprophistvaluelist(): + g = Graph() + edges_str = [ + ("1", "2", 10, 1), + ("1", "2", 10, 1), + ("1", "4", 20, 2), + ("2", "3", 5, 3), + ("3", "2", 2, 4), + ("3", "1", 1, 5), + ("4", "3", 10, 6), + ("4", "1", 5, 7), + ("1", "5", 2, 8), + ] + for src, dst, val, time in edges_str: + g.add_edge(time, src, dst, {"value_dec": val}) + + v = g.vertex("1") + res = sorted(v.out_edges.properties.temporal.get("value_dec").values().sum()) + assert res == [2, 20, 20] + + res = sorted(v.out_edges.properties.temporal.get("value_dec").values().count()) + assert res == [1, 1, 2] + + res = v.out_edges.properties.temporal.get("value_dec").values().sum().sum() + assert res == 42 + + res = v.out_edges.properties.temporal.get("value_dec").values().count().sum() + assert res == 4 + + g = Graph() + edges_str = [ + ("1", "2", 10, 1), + ("1", "2", 10, 2), + ("1", "2", 100, 3), + ("1", "4", 20, 2), + ("2", "3", 5, 3), + ("3", "2", 2, 4), + ("3", "1", 1, 5), + ("4", "3", 10, 6), + ("4", "1", 5, 7), + ("1", "5", 2, 8), + ("1", "5", 1, 9), + ("1", "5", 5, 10), + ] + for src, dst, val, time in edges_str: + g.add_edge(time, src, dst, {"value_dec": val}) + v = g.vertex("1") + res = v.out_edges.properties.temporal.get( + "value_dec" + ).values() # PyPropHistValueList([[10, 10, 10], [20], [2]]) + assert res.sum() == [120, 20, 8] + assert res.min() == [10, 20, 1] + assert res.max() == [100, 20, 5] + assert sorted(res.count()) == [1, 3, 3] + assert res.median() == [10, 20, 2] + assert list(res.mean()) == [40, 20, 8 / 3] + assert list(res.average()) == [40, 20, 8 / 3] + + +def test_empty_lists(): + # This checks that empty lists are handled correctly on all python property types + g = Graph() + edges_str = [ + ("1", "2", 10, 1), + ("1", "2", 10, 1), + ("1", "4", 20, 2), + ("2", "3", 5, 3), + ("3", "2", 2, 4), + ("3", "1", 1, 5), + ("4", "3", 10, 6), + ("4", "1", 5, 7), + ("1", "5", 2, 8), + ] + for src, dst, val, time in edges_str: + g.add_edge(time, src, dst, {"value_dec": val}) + assert ( + g.vertices + .out_edges.properties.temporal.get("value_dec") + .values() + .median() + .median() + .median() + == 5 + ) + assert ( + g.vertices + .out_edges.properties.temporal.get("value_dec") + .values() + .mean() + .mean() + .mean() + == 1.3333333333333335 + ) + + +def test_propiterable(): + import raphtory + + g = raphtory.Graph() + edges_str = [ + ("1", "2", 10, 1), + ("1", "2", 10, 1), + ("1", "2", 10, 1), + ("1", "4", 20, 2), + ("2", "3", 5, 3), + ("3", "2", 2, 4), + ("3", "1", 1, 5), + ("4", "3", 10, 6), + ("4", "1", 5, 7), + ("1", "5", 2, 8), + ] + for src, dst, val, time in edges_str: + g.add_edge(time, src, dst, {"value_dec": val}) + + v = g.vertex("1") + result = v.out_edges.properties.temporal.get("value_dec").values().flatten() + assert sorted(result) == [2, 10, 10, 10, 20] + assert result.sum() == 52 + assert result.median() == 10 + assert result.mean() == 10.4 + assert result.average() == 10.4 + assert result.min() == 2 + assert result.max() == 20 + assert result.count() == 5 + + assert v.out_edges.properties.get("value_dec").sum() == 32 + assert v.out_edges.properties.get("value_dec").median() == 10 + + total = g.vertices.in_edges.properties.get("value_dec").sum() + assert sorted(total) == [2, 6, 12, 15, 20] + + total = g.vertices.edges.properties.get("value_dec").sum() + assert sorted(total) == [2, 17, 18, 35, 38] + + total = dict( + zip(g.vertices.id, g.vertices.out_edges.properties.get("value_dec").sum()) + ) + assert total == {1: 32, 2: 5, 3: 3, 4: 15, 5: None} + + total = g.vertices.out_edges.properties.get("value_dec").sum().sum() + assert total == 55 + + total = g.vertices.out_edges.properties.get("value_dec").sum().median() + assert total == 5 + + total = g.vertices.out_edges.properties.get("value_dec").sum().drop_none() + assert sorted(total) == [3, 5, 15, 32] + + total = g.vertices.out_edges.properties.get("value_dec").median() + assert list(total) == [10, 5, 10, 2, None] + + total = g.vertex("1").in_edges.properties.get("value_dec").sum() + assert total == 6 + + total = g.vertex("1").in_edges.properties.get("value_dec").median() + assert total == 5 + + +def test_pypropvalue_list_listlist(): + g = Graph() + edges_str = [ + ("1", "2", 10, 1), + ("1", "2", 10, 2), + ("1", "2", 100, 3), + ("1", "4", 20, 2), + ("2", "3", 5, 3), + ("3", "2", 2, 4), + ("3", "1", 1, 5), + ("4", "3", 10, 6), + ("4", "1", 5, 7), + ("1", "5", 2, 8), + ("1", "5", 1, 9), + ("1", "5", 5, 10), + ] + for src, dst, val, time in edges_str: + g.add_edge(time, src, dst, {"value_dec": val}) + v = g.vertex("1") + res = g.edges().properties.get( + "value_dec" + ) # PyPropValueList([100, 20, 5, 5, 5, 10, 1, 2]) + res_v = v.edges.properties.get("value_dec") # PyPropValueList([100, 5, 20, 1, 5]) + res_ll = g.vertices.edges.properties.get("value_dec") + + assert res.sum() == 148 + assert res_v.sum() == 131 + assert res_ll.sum() == [131, 107, 35, 18, 5] + + assert res.median() == 5 + assert res_v.median() == 5 + assert res_ll.median() == [5, 5, 10, 5, 5] + + assert res.min() == 1 + assert res_v.min() == 1 + assert res_ll.min() == [1, 2, 5, 1, 5] + + assert res.max() == 100 + assert res_v.max() == 100 + assert res_ll.max() == [100, 100, 20, 10, 5] + + assert res.count() == 8 + assert res_v.count() == 5 + assert res_ll.count() == [5, 3, 3, 4, 1] + + assert res.mean() == res.average() == 18.5 + assert res_v.mean() == res_v.average() == 26.2 + assert ( + res_ll.mean() + == res_ll.average() + == [26.2, 35.666666666666664, 11.666666666666666, 4.5, 5.0] + ) + + +def test_pytemporalprops(): + g = Graph() + edges_str = [ + ("1", "2", 10, 1), + ("1", "2", 10, 2), + ("1", "2", 100, 3), + ("1", "4", 20, 2), + ("2", "3", 5, 3), + ("3", "2", 2, 4), + ("3", "1", 1, 5), + ("4", "3", 10, 6), + ("4", "1", 5, 7), + ("1", "5", 2, 8), + ("1", "5", 1, 9), + ("1", "5", 5, 10), + ] + for src, dst, val, time in edges_str: + g.add_edge(time, src, dst, {"value_dec": val}) + v = g.vertex("1") + res = list(v.out_edges)[0].properties.temporal.get("value_dec") + + assert res.sum() == 120 + assert res.min() == (1, 10) + assert res.max() == (3, 100) + assert res.count() == 3 + assert res.mean() == res.average() == 40.0 + assert res.median() == (2, 10) diff --git a/raphtory-benchmark/Cargo.toml b/raphtory-benchmark/Cargo.toml index 92ecb9c65f..bc89d1c4eb 100644 --- a/raphtory-benchmark/Cargo.toml +++ b/raphtory-benchmark/Cargo.toml @@ -7,8 +7,7 @@ edition = "2021" [dependencies] criterion = "0.4" -raphtory = { path = "../raphtory" } -raphtory-io = { path = "../raphtory-io" } +raphtory = { path = "../raphtory" , features=["io"]} sorted_vector_map = "0.1" rand = "0.8.5" rayon = "1" diff --git a/raphtory-benchmark/benches/algobench.rs b/raphtory-benchmark/benches/algobench.rs index 0e9663d35a..fae8bd1eb2 100644 --- a/raphtory-benchmark/benches/algobench.rs +++ b/raphtory-benchmark/benches/algobench.rs @@ -1,10 +1,14 @@ use crate::common::bench; use criterion::{criterion_group, criterion_main, Criterion}; -use raphtory::algorithms::local_clustering_coefficient::local_clustering_coefficient; -use raphtory::algorithms::local_triangle_count::local_triangle_count; -use raphtory::db::graph::Graph; -use raphtory::db::view_api::*; +use raphtory::{ + algorithms::{ + local_clustering_coefficient::local_clustering_coefficient, + local_triangle_count::local_triangle_count, + }, + prelude::*, +}; use rayon::prelude::*; + mod common; //TODO swap to new trianglecount @@ -26,7 +30,7 @@ pub fn local_triangle_count_analysis(c: &mut Criterion) { let mut group = c.benchmark_group("local_triangle_count"); group.sample_size(10); bench(&mut group, "local_triangle_count", None, |b| { - let g = raphtory_io::graph_loader::example::lotr_graph::lotr_graph(1); + let g = raphtory::graph_loader::example::lotr_graph::lotr_graph(); let windowed_graph = g.window(i64::MIN, i64::MAX); b.iter(|| { @@ -45,7 +49,7 @@ pub fn local_clustering_coefficient_analysis(c: &mut Criterion) { let mut group = c.benchmark_group("local_clustering_coefficient"); bench(&mut group, "local_clustering_coefficient", None, |b| { - let g: Graph = Graph::new(1); + let g: Graph = Graph::new(); let windowed_graph = g.window(0, 5); let vs = vec![ @@ -75,7 +79,7 @@ pub fn local_clustering_coefficient_analysis(c: &mut Criterion) { ]; for (src, dst, t) in &vs { - g.add_edge(*t, *src, *dst, &vec![], None).unwrap(); + g.add_edge(*t, *src, *dst, NO_PROPS, None).unwrap(); } b.iter(|| local_clustering_coefficient(&windowed_graph, 1)) diff --git a/raphtory-benchmark/benches/base.rs b/raphtory-benchmark/benches/base.rs index 8a984dc0e3..29bcd29543 100644 --- a/raphtory-benchmark/benches/base.rs +++ b/raphtory-benchmark/benches/base.rs @@ -1,5 +1,6 @@ -use crate::common::{bootstrap_graph, run_large_ingestion_benchmarks}; +use crate::common::{bootstrap_graph, run_analysis_benchmarks, run_large_ingestion_benchmarks}; use criterion::{criterion_group, criterion_main, Criterion, Throughput}; +use raphtory::{graph_loader::example::lotr_graph::lotr_graph, prelude::*}; mod common; @@ -18,8 +19,31 @@ pub fn base(c: &mut Criterion) { large_group.throughput(Throughput::Elements(1_000)); large_group.measurement_time(std::time::Duration::from_secs(3)); // Make an option of None - run_large_ingestion_benchmarks(&mut large_group, || bootstrap_graph(4, 10000), None); + run_large_ingestion_benchmarks(&mut large_group, || bootstrap_graph(10000), None); large_group.finish(); + let mut graph_group = c.benchmark_group("lotr_graph"); + let graph = lotr_graph(); + run_analysis_benchmarks(&mut graph_group, || graph.clone(), None); + graph_group.finish(); + let mut graph_window_group_100 = c.benchmark_group("lotr_graph_window_100"); + graph_window_group_100.sample_size(10); + run_analysis_benchmarks( + &mut graph_window_group_100, + || graph.window(i64::MIN, i64::MAX), + None, + ); + graph_window_group_100.finish(); + let mut graph_window_group_10 = c.benchmark_group("lotr_graph_window_10"); + let latest = graph.end().expect("non-empty graph"); + let earliest = graph.start().expect("non-empty graph"); + let start = latest - (latest - earliest) / 10; + graph_window_group_10.sample_size(10); + run_analysis_benchmarks( + &mut graph_window_group_10, + || graph.window(start, latest + 1), + None, + ); + graph_window_group_10.finish(); } criterion_group!(benches, base); diff --git a/raphtory-benchmark/benches/common/mod.rs b/raphtory-benchmark/benches/common/mod.rs index c15390c148..1103a0c47e 100644 --- a/raphtory-benchmark/benches/common/mod.rs +++ b/raphtory-benchmark/benches/common/mod.rs @@ -1,8 +1,8 @@ +#![allow(dead_code)] + use criterion::{measurement::WallTime, BatchSize, Bencher, BenchmarkGroup, BenchmarkId}; -use rand::seq::*; -use rand::{distributions::Uniform, Rng}; -use raphtory::db::graph::Graph; -use raphtory::db::view_api::*; +use rand::{distributions::Uniform, seq::*, Rng}; +use raphtory::prelude::*; use std::collections::HashSet; fn make_index_gen() -> Box> { @@ -17,8 +17,8 @@ fn make_time_gen() -> Box> { Box::new(rng.sample_iter(range)) } -pub fn bootstrap_graph(num_shards: usize, num_vertices: usize) -> Graph { - let graph = Graph::new(num_shards); +pub fn bootstrap_graph(num_vertices: usize) -> Graph { + let graph = Graph::new(); let mut indexes = make_index_gen(); let mut times = make_time_gen(); let num_edges = num_vertices / 2; @@ -26,7 +26,9 @@ pub fn bootstrap_graph(num_shards: usize, num_vertices: usize) -> Graph { let source = indexes.next().unwrap(); let target = indexes.next().unwrap(); let time = times.next().unwrap(); - graph.add_edge(time, source, target, &vec![], None).unwrap(); + graph + .add_edge(time, source, target, NO_PROPS, None) + .unwrap(); } graph } @@ -68,7 +70,7 @@ pub fn run_ingestion_benchmarks( |b: &mut Bencher| { b.iter_batched_ref( || (make_graph(), time_sample()), - |(g, t): &mut (Graph, i64)| g.add_vertex(*t, 0, &vec![]), + |(g, t): &mut (Graph, i64)| g.add_vertex(*t, 0, NO_PROPS), BatchSize::SmallInput, ) }, @@ -80,7 +82,7 @@ pub fn run_ingestion_benchmarks( |b: &mut Bencher| { b.iter_batched_ref( || (make_graph(), index_sample()), - |(g, v): &mut (Graph, u64)| g.add_vertex(0, *v, &vec![]), + |(g, v): &mut (Graph, u64)| g.add_vertex(0, *v, NO_PROPS), BatchSize::SmallInput, ) }, @@ -92,7 +94,7 @@ pub fn run_ingestion_benchmarks( |b: &mut Bencher| { b.iter_batched_ref( || (make_graph(), time_sample()), - |(g, t)| g.add_edge(*t, 0, 0, &vec![], None), + |(g, t)| g.add_edge(*t, 0, 0, NO_PROPS, None), BatchSize::SmallInput, ) }, @@ -104,7 +106,7 @@ pub fn run_ingestion_benchmarks( |b: &mut Bencher| { b.iter_batched_ref( || (make_graph(), index_sample(), index_sample()), - |(g, s, d)| g.add_edge(0, *s, *d, &vec![], None), + |(g, s, d)| g.add_edge(0, *s, *d, NO_PROPS, None), BatchSize::SmallInput, ) }, @@ -122,9 +124,6 @@ pub fn run_large_ingestion_benchmarks( ) where F: FnMut() -> Graph, { - let mut times_gen = make_time_gen(); - let mut time_sample = || times_gen.next().unwrap(); - let updates = 1000; bench( @@ -141,7 +140,7 @@ pub fn run_large_ingestion_benchmarks( }, |(g, times)| { for t in times.iter() { - g.add_edge(*t, 0, 0, &vec![], None).unwrap() + g.add_edge(*t, 0, 0, NO_PROPS, None).unwrap(); } }, BatchSize::SmallInput, @@ -163,7 +162,7 @@ pub fn run_large_ingestion_benchmarks( }, |(g, times)| { for t in times.iter() { - g.add_edge(*t, "0", "0", &vec![], None).unwrap() + g.add_edge(*t, "0", "0", NO_PROPS, None).unwrap(); } }, BatchSize::SmallInput, @@ -185,7 +184,7 @@ pub fn run_large_ingestion_benchmarks( }, |(g, times)| { for t in times.iter() { - g.add_edge(*t, "test", "other", &vec![], None).unwrap() + g.add_edge(*t, "test", "other", NO_PROPS, None).unwrap(); } }, BatchSize::SmallInput, @@ -213,10 +212,10 @@ pub fn run_large_ingestion_benchmarks( *t, src_gen.next().unwrap(), dst_gen.next().unwrap(), - &vec![], + NO_PROPS, None, ) - .unwrap() + .unwrap(); } }, BatchSize::SmallInput, @@ -244,10 +243,10 @@ pub fn run_large_ingestion_benchmarks( *t, src_gen.next().unwrap(), dst_gen.next().unwrap(), - &vec![], + NO_PROPS, None, ) - .unwrap() + .unwrap(); } }, BatchSize::SmallInput, @@ -273,13 +272,13 @@ pub fn run_analysis_benchmarks( let vertices: HashSet = graph.vertices().id().collect(); bench(group, "num_edges", parameter, |b: &mut Bencher| { - b.iter(|| graph.num_edges()) + b.iter(|| graph.count_edges()) }); bench(group, "has_edge_existing", parameter, |b: &mut Bencher| { let mut rng = rand::thread_rng(); let edge = edges.iter().choose(&mut rng).expect("non-empty graph"); - b.iter(|| graph.has_edge(edge.0, edge.1, None)) + b.iter(|| graph.has_edge(edge.0, edge.1, Layer::All)) }); bench( @@ -297,12 +296,12 @@ pub fn run_analysis_benchmarks( break edge; } }; - b.iter(|| graph.has_edge(edge.0, edge.1, None)) + b.iter(|| graph.has_edge(edge.0, edge.1, Layer::All)) }, ); bench(group, "num_vertices", parameter, |b: &mut Bencher| { - b.iter(|| graph.num_vertices()) + b.iter(|| graph.count_vertices()) }); bench( diff --git a/raphtory-benchmark/benches/edge_add.rs b/raphtory-benchmark/benches/edge_add.rs index d6c9572a83..6f8c3118e7 100644 --- a/raphtory-benchmark/benches/edge_add.rs +++ b/raphtory-benchmark/benches/edge_add.rs @@ -1,10 +1,11 @@ use criterion::{criterion_group, criterion_main, Criterion}; -use raphtory::core::vertex::InputVertex; -use raphtory::db::graph::Graph; +use raphtory::{core::entities::vertices::input_vertex::InputVertex, prelude::*}; mod common; -use rand::distributions::{Alphanumeric, DistString}; -use rand::{thread_rng, Rng}; +use rand::{ + distributions::{Alphanumeric, DistString}, + thread_rng, Rng, +}; fn random_string(n: usize) -> String { Alphanumeric.sample_string(&mut rand::thread_rng(), n) @@ -30,12 +31,12 @@ pub fn graph(c: &mut Criterion) { id_group.finish(); let mut graph_group = c.benchmark_group("edge_add"); - let mut g = Graph::new(1); + let g = Graph::new(); graph_group.bench_function("string input", |bencher| { let src: String = random_string(16); let dst: String = random_string(16); let t: i64 = thread_rng().gen(); - bencher.iter(|| g.add_edge(t, src.clone(), dst.clone(), &vec![], None)) + bencher.iter(|| g.add_edge(t, src.clone(), dst.clone(), NO_PROPS, None)) }); graph_group.finish(); } diff --git a/raphtory-benchmark/benches/graph_ops.rs b/raphtory-benchmark/benches/graph_ops.rs index bb9eefbddc..962d009b42 100644 --- a/raphtory-benchmark/benches/graph_ops.rs +++ b/raphtory-benchmark/benches/graph_ops.rs @@ -1,13 +1,12 @@ use common::run_analysis_benchmarks; use criterion::{criterion_group, criterion_main, Criterion}; -use raphtory::db::view_api::*; -use raphtory_io::graph_loader::example::sx_superuser_graph::sx_superuser_graph; +use raphtory::{db::api::view::*, graph_loader::example::sx_superuser_graph::sx_superuser_graph}; mod common; pub fn graph(c: &mut Criterion) { let mut graph_group = c.benchmark_group("analysis_graph"); - let graph = sx_superuser_graph(2).unwrap(); + let graph = sx_superuser_graph().unwrap(); run_analysis_benchmarks(&mut graph_group, || graph.clone(), None); graph_group.finish(); let mut graph_window_group_100 = c.benchmark_group("analysis_graph_window_100"); diff --git a/raphtory-benchmark/benches/parameterized.rs b/raphtory-benchmark/benches/parameterized.rs index d23057f8db..843a04fe8f 100644 --- a/raphtory-benchmark/benches/parameterized.rs +++ b/raphtory-benchmark/benches/parameterized.rs @@ -12,7 +12,7 @@ pub fn parameterized(c: &mut Criterion) { let mut ingestion_group = c.benchmark_group("ingestion-num_vertices"); ingestion_group.plot_config(PlotConfiguration::default().summary_scale(AxisScale::Logarithmic)); for num_vertices in vertices { - let make_graph = || bootstrap_graph(4, num_vertices); + let make_graph = || bootstrap_graph(num_vertices); ingestion_group.throughput(Throughput::Elements(num_vertices as u64)); ingestion_group.sample_size(10); ingestion_group.warm_up_time(std::time::Duration::from_secs(1)); diff --git a/raphtory-benchmark/benches/tgraph_benchmarks.rs b/raphtory-benchmark/benches/tgraph_benchmarks.rs index d4b2009ff4..93f0f54897 100644 --- a/raphtory-benchmark/benches/tgraph_benchmarks.rs +++ b/raphtory-benchmark/benches/tgraph_benchmarks.rs @@ -1,9 +1,8 @@ -use std::collections::BTreeSet; - use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; use rand::{distributions::Uniform, Rng}; -use raphtory::core::{lsm::LSMSet, tadjset::TAdjSet}; +use raphtory::core::entities::vertices::structure::adjset::AdjSet; use sorted_vector_map::SortedVectorSet; +use std::collections::BTreeSet; fn btree_set_u64(c: &mut Criterion) { let mut group = c.benchmark_group("btree_set_u64_range_insert"); @@ -28,20 +27,6 @@ fn btree_set_u64(c: &mut Criterion) { }, ); - group.bench_with_input( - BenchmarkId::new("LSMTree with u64", size), - &init_vals, - |b, vals| { - b.iter(|| { - let mut bs = LSMSet::default(); - for v in vals.iter() { - bs.find(*v); - bs.insert(*v); - } - }); - }, - ); - group.bench_with_input( BenchmarkId::new("SortedVec with u64", size), &init_vals, @@ -80,7 +65,7 @@ fn bm_tadjset(c: &mut Criterion) { .take(*size as usize) .collect(); - let mut tadjset = TAdjSet::default(); + let mut tadjset = AdjSet::default(); group.bench_with_input( BenchmarkId::new("TAdjSet insert", size), diff --git a/raphtory-graphql/Cargo.toml b/raphtory-graphql/Cargo.toml index c34d79eebf..1b6192e23e 100644 --- a/raphtory-graphql/Cargo.toml +++ b/raphtory-graphql/Cargo.toml @@ -13,29 +13,35 @@ readme.workspace = true homepage.workspace = true [dependencies] -raphtory = { path = "../raphtory", version = "0.4.0" } - +raphtory = { path = "../raphtory", version = "0.5.7", features = ['vectors'] } +bincode = "1" +base64 = "0.21.2" +thiserror = "1.0.44" dotenv = "0.15.0" itertools = "0.10" serde = {version = "1.0.147", features = ["derive"]} once_cell = "1.17.2" - poem = "1.3.48" tokio = {version = "1.18.2", features = ["full"]} - async-graphql = {version = "5.0.5", features = ["dynamic-schema"]} dynamic-graphql = "0.7.3" async-graphql-poem = "5.0.5" - +parking_lot = { version = "0.12" , features = ["serde", "arc_lock", "send_guard"] } futures-util = "0.3.0" async-stream = "0.3.0" - opentelemetry = {version = "0.18.0", features = ["rt-tokio"]} opentelemetry-jaeger = {version = "0.17.0", features = ["rt-tokio"]} tracing = "0.1.37" tracing-opentelemetry = "0.18.0" tracing-subscriber = {version = "0.3.16", features = ["std", "env-filter"]} +walkdir = "2" +ordered-float = "3.7.0" +uuid = "1.4.1" +async-openai = "0.14.0" +clap = { version = "4.3.11", features = ["derive"] } +chrono = { version = "0.4", features = ["serde"] } [dev-dependencies] serde_json = "1.0" +tempfile = "3.6.0" diff --git a/raphtory-graphql/src/data.rs b/raphtory-graphql/src/data.rs index aeedb3128b..87c353b026 100644 --- a/raphtory-graphql/src/data.rs +++ b/raphtory-graphql/src/data.rs @@ -1,32 +1,116 @@ -use raphtory::db::graph::Graph; -use std::collections::HashMap; -use std::fs; -use std::path::{Path, PathBuf}; +use parking_lot::RwLock; +use raphtory::{ + core::Prop, + prelude::{Graph, GraphViewOps, PropertyAdditionOps}, + search::IndexedGraph, + vectors::VectorizedGraph, +}; +use std::{ + collections::{HashMap, HashSet}, + path::Path, +}; +use walkdir::WalkDir; +#[derive(Default)] pub(crate) struct Data { - pub(crate) graphs: HashMap, + pub(crate) graphs: RwLock>>, + pub(crate) vector_stores: RwLock>>, } impl Data { - pub fn load(directory_path: &str) -> Self { - let paths = fs::read_dir(directory_path).unwrap_or_else(|_| { - panic!("path '{directory_path}' doesn't exist or it is not a directory") - }); - - let graphs = paths - .filter_map(|entry| { - let path:PathBuf = entry.unwrap().path(); - if path.is_dir(){ - let graph = Graph::load_from_file(&path).ok()?; - let filename = path.file_name()?.to_str()?.to_string(); - Some((filename, graph)) - } - else{ - None + pub fn from_map(graphs: HashMap) -> Self { + let graphs = RwLock::new(Self::convert_graphs(graphs)); + let vector_stores = RwLock::new(HashMap::new()); + Self { + graphs, + vector_stores, + } + } + + pub fn from_directory(directory_path: &str) -> Self { + let graphs = RwLock::new(Self::load_from_file(directory_path)); + let vector_stores = RwLock::new(HashMap::new()); + Self { + graphs, + vector_stores, + } + } + + pub fn from_map_and_directory(graphs: HashMap, directory_path: &str) -> Self { + let mut graphs = Self::convert_graphs(graphs); + graphs.extend(Self::load_from_file(directory_path)); + let graphs = RwLock::new(graphs); + let vector_stores = RwLock::new(HashMap::new()); + Self { + graphs, + vector_stores, + } + } + + fn convert_graphs(graphs: HashMap) -> HashMap> { + graphs + .into_iter() + .map(|(name, g)| { + ( + name, + IndexedGraph::from_graph(&g).expect("Unable to index graph"), + ) + }) + .collect() + } + + pub fn load_from_file(path: &str) -> HashMap> { + let mut valid_paths = HashSet::::new(); + + for entry in WalkDir::new(path).into_iter().filter_map(|e| e.ok()) { + let path = entry.path(); + let path_string = path.display().to_string(); + let filename = path.file_name().and_then(|name| name.to_str()); + if let Some(filename) = filename { + if path.is_file() && !filename.starts_with('.') { + valid_paths.insert(path_string); } + } + } + + let mut graphs_loaded: Vec = vec![]; + let mut is_graph_already_loaded = |graph_name: String| { + if graphs_loaded.contains(&graph_name) { + panic!("Graph by name {} is already loaded", graph_name); + } else { + graphs_loaded.push(graph_name); + } + }; + + let graphs: HashMap> = valid_paths + .into_iter() + .map(|path| { + println!("loading graph from {path}"); + let graph = Graph::load_from_file(&path).expect("Unable to load from graph"); + graph + .add_constant_properties([("path".to_string(), Prop::str(path.clone()))]) + .expect("Failed to add static property"); + let maybe_graph_name = graph.properties().get("name"); + + return match maybe_graph_name { + None => { + let graph_name = Path::new(&path).file_name().unwrap().to_str().unwrap(); + is_graph_already_loaded(graph_name.to_string()); + (graph_name.to_string(), graph) + } + Some(graph_name) => { + is_graph_already_loaded(graph_name.to_string()); + (graph_name.to_string(), graph) + } + }; + }) + .map(|(name, g)| { + ( + name, + IndexedGraph::from_graph(&g).expect("Unable to index graph"), + ) }) .collect(); - - Self { graphs } + graphs } } diff --git a/raphtory-graphql/src/embeddings.rs b/raphtory-graphql/src/embeddings.rs new file mode 100644 index 0000000000..439c7f3ae1 --- /dev/null +++ b/raphtory-graphql/src/embeddings.rs @@ -0,0 +1,19 @@ +use async_openai::{ + types::{CreateEmbeddingRequest, EmbeddingInput}, + Client, +}; +use itertools::Itertools; +use raphtory::vectors::Embedding; + +pub async fn openai_embedding(texts: Vec) -> Vec { + println!("computing embeddings for {} texts", texts.len()); + let client = Client::new(); + let request = CreateEmbeddingRequest { + model: "text-embedding-ada-002".to_owned(), + input: EmbeddingInput::StringArray(texts), + user: None, + }; + let response = client.embeddings().create(request).await.unwrap(); + println!("Generated embeddings successfully"); + response.data.into_iter().map(|e| e.embedding).collect_vec() +} diff --git a/raphtory-graphql/src/lib.rs b/raphtory-graphql/src/lib.rs index 80488e566e..64619c5a0c 100644 --- a/raphtory-graphql/src/lib.rs +++ b/raphtory-graphql/src/lib.rs @@ -1,31 +1,101 @@ -pub use crate::model::algorithm::Algorithm; -pub use crate::server::RaphtoryServer; +pub use crate::{model::algorithm::Algorithm, server::RaphtoryServer}; +use base64::{prelude::BASE64_URL_SAFE_NO_PAD, DecodeError, Engine}; +use raphtory::{core::utils::errors::GraphError, db::api::view::internal::MaterializedGraph}; +pub mod embeddings; mod model; mod observability; mod routes; -mod server; +pub mod server; mod data; +#[derive(thiserror::Error, Debug)] +pub enum UrlDecodeError { + #[error("Bincode operation failed")] + BincodeError { + #[from] + source: Box, + }, + #[error("Base64 decoding failed")] + DecodeError { + #[from] + source: DecodeError, + }, +} + +pub fn url_encode_graph>(graph: G) -> Result { + let g: MaterializedGraph = graph.into(); + Ok(BASE64_URL_SAFE_NO_PAD.encode(bincode::serialize(&g)?)) +} + +pub fn url_decode_graph>(graph: T) -> Result { + Ok(bincode::deserialize( + &BASE64_URL_SAFE_NO_PAD.decode(graph)?, + )?) +} #[cfg(test)] mod graphql_test { use super::*; - use dynamic_graphql::dynamic::DynamicRequestExt; - use dynamic_graphql::{App, FieldValue}; - use raphtory::db::graph::Graph; + use crate::{data::Data, model::App}; + use async_graphql::UploadValue; + use dynamic_graphql::{Request, Variables}; + use raphtory::{db::api::view::internal::IntoDynamic, prelude::*}; + use serde_json::json; use std::collections::HashMap; - use std::env; + use tempfile::tempdir; + + #[tokio::test] + async fn search_for_gandalf_query() { + let graph = Graph::new(); + graph + .add_vertex(0, "Gandalf", [("kind".to_string(), Prop::str("wizard"))]) + .expect("Could not add vertex!"); + graph + .add_vertex(0, "Frodo", [("kind".to_string(), Prop::str("Hobbit"))]) + .expect("Could not add vertex!"); + + let graphs = HashMap::from([("lotr".to_string(), graph)]); + let data = data::Data::from_map(graphs); + let schema = App::create_schema().data(data).finish().unwrap(); + + let query = r#" + { + graph(name: "lotr") { + search(query: "kind:wizard", limit: 10, offset: 0) { + name + } + } + } + "#; + let req = Request::new(query); + let res = schema.execute(req).await; + let data = res.data.into_json().unwrap(); + + assert_eq!( + data, + json!({ + "graph": { + "search": [ + { + "name": "Gandalf" + } + ] + } + }), + ); + } #[tokio::test] async fn basic_query() { - let graph = Graph::new(1); - graph.add_vertex(0, 11, &vec![]); + let graph = Graph::new(); + graph + .add_vertex(0, 11, NO_PROPS) + .expect("Could not add vertex!"); + let graphs = HashMap::from([("lotr".to_string(), graph)]); - let data = data::Data { graphs }; + let data = data::Data::from_map(graphs); - #[derive(App)] - struct App(model::QueryRoot); let schema = App::create_schema().data(data).finish().unwrap(); let query = r#" @@ -37,10 +107,7 @@ mod graphql_test { } } "#; - - let root = model::QueryRoot; - let req = dynamic_graphql::Request::new(query).root_value(FieldValue::owned_any(root)); - + let req = Request::new(query); let res = schema.execute(req).await; let data = res.data.into_json().unwrap(); @@ -57,4 +124,359 @@ mod graphql_test { }), ); } + + #[tokio::test] + async fn query_nodefilter() { + let graph = Graph::new(); + if let Err(err) = graph.add_vertex(0, "gandalf", NO_PROPS) { + panic!("Could not add vertex! {:?}", err); + } + if let Err(err) = graph.add_vertex(0, "bilbo", NO_PROPS) { + panic!("Could not add vertex! {:?}", err); + } + if let Err(err) = graph.add_vertex(0, "frodo", NO_PROPS) { + panic!("Could not add vertex! {:?}", err); + } + + let graphs = HashMap::from([("lotr".to_string(), graph)]); + let data = Data::from_map(graphs); + + let schema = App::create_schema().data(data).finish().unwrap(); + + let gandalf_query = r#" + { + graph(name: "lotr") { + nodes(filter: { name: { eq: "gandalf" } }) { + name + } + } + } + "#; + + let req = Request::new(gandalf_query); + let res = schema.execute(req).await; + let data = res.data.into_json().unwrap(); + + assert_eq!( + data, + json!({ + "graph": { + "nodes": [ + { + "name": "gandalf" + } + ] + } + }), + ); + + let not_gandalf_query = r#" + { + graph(name: "lotr") { + nodes(filter: { name: { ne: "gandalf" } }) { + name + } + } + } + "#; + + let req = Request::new(not_gandalf_query); + let res = schema.execute(req).await; + let data = res.data.into_json().unwrap(); + + assert_eq!( + data, + json!({ + "graph": { + "nodes": [ + { "name": "bilbo" }, + { "name": "frodo" } + ] + } + }), + ); + } + + #[tokio::test] + async fn query_properties() { + let graph = Graph::new(); + if let Err(err) = graph.add_vertex(0, "gandalf", NO_PROPS) { + panic!("Could not add vertex! {:?}", err); + } + if let Err(err) = graph.add_vertex(0, "bilbo", [("food".to_string(), Prop::str("lots"))]) { + panic!("Could not add vertex! {:?}", err); + } + if let Err(err) = graph.add_vertex(0, "frodo", [("food".to_string(), Prop::str("some"))]) { + panic!("Could not add vertex! {:?}", err); + } + + let graphs = HashMap::from([("lotr".to_string(), graph)]); + let data = data::Data::from_map(graphs); + + let schema = App::create_schema().data(data).finish().unwrap(); + + let prop_has_key_filter = r#" + { + graph(name: "lotr") { + nodes(filter: { propertyHas: { + key: "food" + }}) { + name + } + } + } + "#; + + let req = Request::new(prop_has_key_filter); + let res = schema.execute(req).await; + let data = res.data.into_json().unwrap(); + + assert_eq!( + data, + json!({ + "graph": { + "nodes": [ + { "name": "bilbo" }, + { "name": "frodo" }, + ] + } + }), + ); + + let prop_has_value_filter = r#" + { + graph(name: "lotr") { + nodes(filter: { propertyHas: { + valueStr: "lots" + }}) { + name + } + } + } + "#; + + let req = Request::new(prop_has_value_filter); + let res = schema.execute(req).await; + let data = res.data.into_json().unwrap(); + + assert_eq!( + data, + json!({ + "graph": { + "nodes": [ + { "name": "bilbo" }, + ] + } + }), + ); + } + + #[tokio::test] + async fn test_mutation() { + let test_dir = tempdir().unwrap(); + let g0 = Graph::new(); + let test_dir_path = test_dir.path().to_str().unwrap().replace(r#"\"#, r#"\\"#); + let f0 = &test_dir.path().join("g0"); + let f1 = &test_dir.path().join("g1"); + g0.save_to_file(f0).unwrap(); + + let g1 = Graph::new(); + g1.add_vertex(0, 1, NO_PROPS).unwrap(); + + let g2 = Graph::new(); + g2.add_vertex(0, 2, NO_PROPS).unwrap(); + + let data = Data::default(); + let schema = App::create_schema().data(data).finish().unwrap(); + + let list_graphs = r#" + { + subgraphs { + name + } + }"#; + + let list_nodes = |name: &str| { + format!( + r#"{{ + graph(name: "{}") {{ + nodes {{ + id + }} + }} + }}"#, + name + ) + }; + + let load_all = &format!( + r#"mutation {{ + loadGraphsFromPath(path: "{}") + }}"#, + test_dir_path + ); + + let load_new = &format!( + r#"mutation {{ + loadNewGraphsFromPath(path: "{}") + }}"#, + test_dir_path + ); + + // only g0 which is empty + let req = Request::new(load_all); + let res = schema.execute(req).await; + let res_json = res.data.into_json().unwrap(); + assert_eq!(res_json, json!({"loadGraphsFromPath": ["g0"]})); + + let req = Request::new(list_graphs); + let res = schema.execute(req).await; + let res_json = res.data.into_json().unwrap(); + assert_eq!(res_json, json!({"subgraphs": [{"name": "g0"}]})); + + let req = Request::new(list_nodes("g0")); + let res = schema.execute(req).await; + let res_json = res.data.into_json().unwrap(); + assert_eq!(res_json, json!({"graph": {"nodes": []}})); + + // add g1 to folder and replace g0 with g2 and load new graphs + g1.save_to_file(f1).unwrap(); + g2.save_to_file(f0).unwrap(); + let req = Request::new(load_new); + let res = schema.execute(req).await; + let res_json = res.data.into_json().unwrap(); + assert_eq!(res_json, json!({"loadNewGraphsFromPath": ["g1"]})); + + // g0 is still empty + let req = Request::new(list_nodes("g0")); + let res = schema.execute(req).await; + let res_json = res.data.into_json().unwrap(); + assert_eq!(res_json, json!({"graph": {"nodes": []}})); + + // g1 has node 1 + let req = Request::new(list_nodes("g1")); + let res = schema.execute(req).await; + let res_json = res.data.into_json().unwrap(); + assert_eq!(res_json, json!({"graph": {"nodes": [{"id": 1}]}})); + + // reload all graphs from folder + let req = Request::new(load_all); + schema.execute(req).await; + + // g0 now has node 2 + let req = Request::new(list_nodes("g0")); + let res = schema.execute(req).await; + let res_json = res.data.into_json().unwrap(); + assert_eq!(res_json, json!({"graph": {"nodes": [{"id": 2}]}})); + + // g1 still has node 1 + let req = Request::new(list_nodes("g1")); + let res = schema.execute(req).await; + let res_json = res.data.into_json().unwrap(); + assert_eq!(res_json, json!({"graph": {"nodes": [{"id": 1}]}})); + } + + #[tokio::test] + async fn test_graph_injection() { + let g = Graph::new(); + g.add_vertex(0, 1, NO_PROPS).unwrap(); + let tmp_file = tempfile::NamedTempFile::new().unwrap(); + let path = tmp_file.path(); + g.save_to_file(path).unwrap(); + let file = std::fs::File::open(path).unwrap(); + let upload_val = UploadValue { + filename: "test".into(), + content_type: Some("application/octet-stream".into()), + content: file, + }; + + let data = Data::default(); + let schema = App::create_schema().data(data).finish().unwrap(); + + let query = r##" + mutation($file: Upload!) { + uploadGraph(name: "test", graph: $file) + } + "##; + + let variables = serde_json::json!({ "file": null }); + let mut req = + dynamic_graphql::Request::new(query).variables(Variables::from_json(variables)); + req.set_upload("variables.file", upload_val); + let res = schema.execute(req).await; + println!("{:?}", res); + assert_eq!(res.errors.len(), 0); + let res_json = res.data.into_json().unwrap(); + assert_eq!(res_json, json!({"uploadGraph": "test"})); + + let list_nodes = r#" + query { + graph(name: "test") { + nodes { + id + } + } + } + "#; + + let req = Request::new(list_nodes); + let res = schema.execute(req).await; + assert_eq!(res.errors.len(), 0); + let res_json = res.data.into_json().unwrap(); + assert_eq!(res_json, json!({"graph": {"nodes": [{"id": 1}]}})); + } + + #[tokio::test] + async fn test_graph_send_receive_base64() { + let g = Graph::new(); + g.add_vertex(0, 1, NO_PROPS).unwrap(); + + let graph_str = url_encode_graph(g.clone()).unwrap(); + + let data = Data::default(); + let schema = App::create_schema().data(data).finish().unwrap(); + + let query = r#" + mutation($graph: String!) { + sendGraph(name: "test", graph: $graph) + } + "#; + let req = + Request::new(query).variables(Variables::from_json(json!({ "graph": graph_str }))); + + let res = schema.execute(req).await; + assert_eq!(res.errors.len(), 0); + let res_json = res.data.into_json().unwrap(); + assert_eq!(res_json, json!({"sendGraph": "test"})); + + let list_nodes = r#" + query { + graph(name: "test") { + nodes { + id + } + } + } + "#; + + let req = Request::new(list_nodes); + let res = schema.execute(req).await; + assert_eq!(res.errors.len(), 0); + let res_json = res.data.into_json().unwrap(); + assert_eq!(res_json, json!({"graph": {"nodes": [{"id": 1}]}})); + + let receive_graph = r#" + query { + receiveGraph(name: "test") + } + "#; + + let req = Request::new(receive_graph); + let res = schema.execute(req).await; + assert_eq!(res.errors.len(), 0); + let res_json = res.data.into_json().unwrap(); + let graph_encoded = res_json.get("receiveGraph").unwrap().as_str().unwrap(); + let graph_roundtrip = url_decode_graph(graph_encoded).unwrap().into_dynamic(); + assert_eq!(g, graph_roundtrip); + } } diff --git a/raphtory-graphql/src/main.rs b/raphtory-graphql/src/main.rs index 40ec59c0cb..c03084bac5 100644 --- a/raphtory-graphql/src/main.rs +++ b/raphtory-graphql/src/main.rs @@ -1,16 +1,52 @@ use crate::server::RaphtoryServer; +use clap::Parser; use dotenv::dotenv; use std::env; mod data; +mod embeddings; mod model; mod observability; mod routes; mod server; +#[derive(Parser, Debug)] +struct Args { + /// graphs to vectorize for similarity search + #[arg(short, long, num_args = 0.., value_delimiter = ' ')] + vectorize: Vec, + + /// directory to use to store the embbeding cache + // parenthesis are actually necessary or this does not compile! + #[arg(short, long, default_value_t = ("".to_string()))] + cache: String, +} + #[tokio::main] async fn main() { + let args = Args::parse(); + + let graphs_to_vectorize = args.vectorize; + let cache_dir = args.cache; + // let graphs_to_vectorize = vec!["jira".to_owned()]; + // let cache_dir = "/tmp/jira-cache-gte-small-batching"; + assert!( + graphs_to_vectorize.is_empty() || !cache_dir.is_empty(), + "Setting up a cache directory is mandatory if some graphs need to be vectorized" + ); + dotenv().ok(); let graph_directory = env::var("GRAPH_DIRECTORY").unwrap_or("/tmp/graphs".to_string()); - RaphtoryServer::new(&graph_directory).run().await.unwrap() + RaphtoryServer::from_directory(&graph_directory) + // .with_vectorized( + // graphs_to_vectorize, + // embeddings::openai_embedding, + // &PathBuf::from(cache_dir), + // None, + // ) + // .await // FIXME: re-enable, probably have two separate methods: with_vectorized, with_templates + // FIXME: maybe we should vectorize the graphs only when run() is called + .run() + .await + .unwrap() } diff --git a/raphtory-graphql/src/model/algorithm.rs b/raphtory-graphql/src/model/algorithm.rs index 5f00f368d4..de1cf2a2af 100644 --- a/raphtory-graphql/src/model/algorithm.rs +++ b/raphtory-graphql/src/model/algorithm.rs @@ -1,16 +1,18 @@ -use crate::model::DynamicGraph; -use async_graphql::dynamic::{ - Field, FieldFuture, FieldValue, InputValue, Object, ResolverContext, TypeRef, +use async_graphql::{ + dynamic::{Field, FieldFuture, FieldValue, InputValue, Object, ResolverContext, TypeRef}, + Context, FieldResult, +}; +use dynamic_graphql::{ + internal::{OutputTypeName, Register, Registry, ResolveOwned, TypeName}, + SimpleObject, }; -use async_graphql::{Context, FieldResult}; -use dynamic_graphql::internal::{OutputTypeName, Register, Registry, ResolveOwned, TypeName}; -use dynamic_graphql::SimpleObject; use once_cell::sync::Lazy; -use raphtory::algorithms::pagerank::unweighted_page_rank; -use raphtory::db::view_api::GraphViewOps; -use std::borrow::Cow; -use std::collections::HashMap; -use std::sync::Mutex; +use ordered_float::OrderedFloat; +use raphtory::{ + algorithms::pagerank::unweighted_page_rank, + db::api::view::{internal::DynamicGraph, GraphViewOps}, +}; +use std::{borrow::Cow, collections::HashMap, sync::Mutex}; type RegisterFunction = fn(&str, Registry, Object) -> (Registry, Object); @@ -94,6 +96,22 @@ impl From<(String, f64)> for Pagerank { } } +impl From<(String, OrderedFloat)> for Pagerank { + fn from((name, rank): (String, OrderedFloat)) -> Self { + let rank = rank.into_inner(); + Self { name, rank } + } +} + +impl From<(&String, &OrderedFloat)> for Pagerank { + fn from((name, rank): (&String, &OrderedFloat)) -> Self { + Self { + name: name.to_string(), + rank: rank.into_inner(), + } + } +} + impl Algorithm for Pagerank { fn output_type() -> TypeRef { // first _nn means that the list is never null, second _nn means no element is null @@ -114,7 +132,8 @@ impl Algorithm for Pagerank { let threads = ctx.args.get("threads").map(|v| v.u64()).transpose()?; let threads = threads.map(|v| v as usize); let tol = ctx.args.get("tol").map(|v| v.f64()).transpose()?; - let result = unweighted_page_rank(graph, iter_count, threads, tol, true) + let binding = unweighted_page_rank(graph, iter_count, threads, tol, true); + let result = binding .into_iter() .map(|pair| FieldValue::owned_any(Pagerank::from(pair))); Ok(Some(FieldValue::list(result))) diff --git a/raphtory-graphql/src/model/filters/edge_filter.rs b/raphtory-graphql/src/model/filters/edge_filter.rs new file mode 100644 index 0000000000..0ef6dfda89 --- /dev/null +++ b/raphtory-graphql/src/model/filters/edge_filter.rs @@ -0,0 +1,57 @@ +use crate::model::{ + filters::{ + primitive_filter::{StringFilter, StringVecFilter}, + property_filter::PropertyHasFilter, + }, + graph::edge::Edge, +}; +use dynamic_graphql::InputObject; +use raphtory::db::api::view::{EdgeViewOps, VertexViewOps}; + +#[derive(InputObject, Clone)] +pub struct EdgeFilter { + node_names: Option, + src: Option, + dst: Option, + property_has: Option, + pub(crate) layer_names: Option, +} + +impl EdgeFilter { + pub(crate) fn matches(&self, edge: &Edge) -> bool { + if let Some(names_filter) = &self.node_names { + let src = edge.ee.src().name(); + let dst = edge.ee.dst().name(); + if !names_filter.contains(&src) || !names_filter.contains(&dst) { + return false; + } + } + + if let Some(name_filter) = &self.src { + if !name_filter.matches(&edge.ee.src().name()) { + return false; + } + } + + if let Some(name_filter) = &self.dst { + if !name_filter.matches(&edge.ee.dst().name()) { + return false; + } + } + + if let Some(name_filter) = &self.layer_names { + return edge + .ee + .layer_names() + .any(|name| name_filter.contains(&name)); + } + + if let Some(property_has_filter) = &self.property_has { + if !property_has_filter.matches_edge_properties(&edge) { + return false; + } + } + + true + } +} diff --git a/raphtory-graphql/src/model/filters/mod.rs b/raphtory-graphql/src/model/filters/mod.rs new file mode 100644 index 0000000000..a3b093b581 --- /dev/null +++ b/raphtory-graphql/src/model/filters/mod.rs @@ -0,0 +1,4 @@ +pub(crate) mod edge_filter; +pub(crate) mod node_filter; +pub(crate) mod primitive_filter; +pub(crate) mod property_filter; diff --git a/raphtory-graphql/src/model/filters/node_filter.rs b/raphtory-graphql/src/model/filters/node_filter.rs new file mode 100644 index 0000000000..fa1b50fbe9 --- /dev/null +++ b/raphtory-graphql/src/model/filters/node_filter.rs @@ -0,0 +1,78 @@ +use crate::model::{ + filters::{ + primitive_filter::{NumberFilter, StringFilter, StringVecFilter}, + property_filter::PropertyHasFilter, + }, + graph::node::Node, +}; +use dynamic_graphql::InputObject; +use raphtory::db::api::view::VertexViewOps; + +#[derive(InputObject)] +pub struct NodeFilter { + names: Option, + name: Option, + node_type: Option, + in_degree: Option, + out_degree: Option, + property_has: Option, +} + +impl NodeFilter { + pub(crate) fn new(names: Vec) -> NodeFilter { + return NodeFilter { + names: Some(StringVecFilter { contains: names }), + name: None, + node_type: None, + in_degree: None, + out_degree: None, + property_has: None, + }; + } + + pub(crate) fn matches(&self, node: &Node) -> bool { + if let Some(names_filter) = &self.names { + if !names_filter.contains(&node.vv.name()) { + return false; + } + } + + if let Some(name_filter) = &self.name { + if !name_filter.matches(&node.vv.name()) { + return false; + } + } + + if let Some(type_filter) = &self.node_type { + let node_type = node + .vv + .properties() + .get("type") + .map(|v| v.to_string()) + .unwrap_or("NONE".to_string()); + if !type_filter.matches(&node_type) { + return false; + } + } + + if let Some(in_degree_filter) = &self.in_degree { + if !in_degree_filter.matches(node.vv.in_degree()) { + return false; + } + } + + if let Some(out_degree_filter) = &self.out_degree { + if !out_degree_filter.matches(node.vv.out_degree()) { + return false; + } + } + + if let Some(property_has_filter) = &self.property_has { + if !property_has_filter.matches_node_properties(&node) { + return false; + } + } + + true + } +} diff --git a/raphtory-graphql/src/model/filters/primitive_filter.rs b/raphtory-graphql/src/model/filters/primitive_filter.rs new file mode 100644 index 0000000000..8262bf9448 --- /dev/null +++ b/raphtory-graphql/src/model/filters/primitive_filter.rs @@ -0,0 +1,79 @@ +use dynamic_graphql::InputObject; + +#[derive(InputObject, Clone)] +pub(crate) struct StringVecFilter { + pub(crate) contains: Vec, +} + +impl StringVecFilter { + pub(crate) fn contains(&self, value: &str) -> bool { + self.contains.contains(&value.to_string()) + } +} + +#[derive(InputObject, Clone)] +pub(crate) struct StringFilter { + pub(crate) eq: Option, + pub(crate) ne: Option, +} + +impl StringFilter { + pub(crate) fn matches(&self, value: &str) -> bool { + if !self.eq.as_ref().map_or(true, |eq| value == eq) { + return false; + } + self.ne.as_ref().map_or(true, |ne| value != ne) + } +} + +#[derive(InputObject, Clone)] +pub(crate) struct NumberFilter { + gt: Option, + lt: Option, + eq: Option, + ne: Option, + gte: Option, + lte: Option, +} + +impl NumberFilter { + pub(crate) fn matches(&self, value: usize) -> bool { + if let Some(gt) = self.gt { + if value <= gt { + return false; + } + } + + if let Some(lt) = self.lt { + if value >= lt { + return false; + } + } + + if let Some(eq) = self.eq { + if value != eq { + return false; + } + } + + if let Some(ne) = self.ne { + if value == ne { + return false; + } + } + + if let Some(gte) = self.gte { + if value < gte { + return false; + } + } + + if let Some(lte) = self.lte { + if value > lte { + return false; + } + } + + true + } +} diff --git a/raphtory-graphql/src/model/filters/property_filter.rs b/raphtory-graphql/src/model/filters/property_filter.rs new file mode 100644 index 0000000000..0547661fa7 --- /dev/null +++ b/raphtory-graphql/src/model/filters/property_filter.rs @@ -0,0 +1,116 @@ +use crate::model::{ + filters::primitive_filter::NumberFilter, + graph::{edge::Edge, node::Node}, +}; +use dynamic_graphql::InputObject; +use raphtory::{core::Prop, db::api::view::VertexViewOps, prelude::EdgeViewOps}; + +#[derive(InputObject, Clone)] +pub(crate) struct PropertyHasFilter { + key: Option, + value_str: Option, + value_num: Option, +} + +impl PropertyHasFilter { + pub(crate) fn matches_node_properties(&self, node: &Node) -> bool { + let valid_prop = |prop| valid_prop(prop, &self.value_str, &self.value_num); + + return match &self.key { + Some(key) => { + if let Some(prop) = node.vv.properties().get(key) { + valid_prop(prop) + } else { + false + } + } + None => node.vv.properties().values().any(valid_prop), + }; + } + + pub(crate) fn matches_edge_properties(&self, edge: &Edge) -> bool { + let valid_prop = |prop| valid_prop(prop, &self.value_str, &self.value_num); + + return match &self.key { + Some(key) => { + if let Some(prop) = EdgeViewOps::properties(&edge.ee).get(key) { + valid_prop(prop) + } else { + false + } + } + None => EdgeViewOps::properties(&edge.ee).values().any(valid_prop), + }; + } +} + +fn valid_prop(prop: Prop, value_str: &Option, num_filter: &Option) -> bool { + if let Some(value_str) = value_str { + if value_neq_str_prop(value_str, &prop) { + return false; + } + } + + if let Some(num_filter) = num_filter { + if value_neq_num_prop(num_filter, &prop) { + return false; + } + } + + true +} + +fn value_neq_str_prop(value: &str, prop: &Prop) -> bool { + if let Prop::Str(prop_str) = prop { + return prop_str != value; + } + + false +} + +fn value_neq_num_prop(num_filter: &NumberFilter, prop: &Prop) -> bool { + match prop { + Prop::I32(i32_prop) => match_signed_num(num_filter, i64::from(*i32_prop)), + Prop::I64(i64_prop) => match_signed_num(num_filter, *i64_prop), + Prop::U8(u8_prop) => match_unsigned_num(num_filter, u64::from(*u8_prop)), + Prop::U16(u16_prop) => match_unsigned_num(num_filter, u64::from(*u16_prop)), + Prop::U32(u32_prop) => match_unsigned_num(num_filter, u64::from(*u32_prop)), + Prop::U64(u64_prop) => match_unsigned_num(num_filter, *u64_prop), + Prop::F32(f32_prop) => match_float(num_filter, f64::from(*f32_prop)), + Prop::F64(f64_prop) => match_float(num_filter, *f64_prop), + _ => false, + } +} + +fn match_signed_num(num_filter: &NumberFilter, signed_num: i64) -> bool { + if signed_num < 0 { + return false; + } + + let as_usize = signed_num as usize; + + if !num_filter.matches(as_usize) { + return true; + } + false +} + +fn match_unsigned_num(num_filter: &NumberFilter, unsigned_num: u64) -> bool { + let as_usize = unsigned_num as usize; + if !num_filter.matches(as_usize) { + return true; + } + false +} + +fn match_float(num_filter: &NumberFilter, float_num: f64) -> bool { + if float_num < 0.0 { + return false; + } + let rounded = float_num.round(); + let as_usize = rounded as usize; + if !num_filter.matches(as_usize) { + return true; + } + false +} diff --git a/raphtory-graphql/src/model/graph/edge.rs b/raphtory-graphql/src/model/graph/edge.rs new file mode 100644 index 0000000000..b1542880e3 --- /dev/null +++ b/raphtory-graphql/src/model/graph/edge.rs @@ -0,0 +1,57 @@ +use crate::model::graph::node::Node; +use dynamic_graphql::{ResolvedObject, ResolvedObjectFields}; +use itertools::Itertools; +use raphtory::db::{ + api::view::{ + internal::{DynamicGraph, IntoDynamic}, + EdgeViewOps, GraphViewOps, + }, + graph::edge::EdgeView, +}; + +#[derive(ResolvedObject)] +pub(crate) struct Edge { + pub(crate) ee: EdgeView, +} + +impl From> for Edge { + fn from(value: EdgeView) -> Self { + Self { + ee: EdgeView { + graph: value.graph.clone().into_dynamic(), + edge: value.edge, + }, + } + } +} + +#[ResolvedObjectFields] +impl Edge { + async fn earliest_time(&self) -> Option { + self.ee.earliest_time() + } + + async fn latest_time(&self) -> Option { + self.ee.latest_time() + } + + async fn src(&self) -> Node { + self.ee.src().into() + } + + async fn dst(&self) -> Node { + self.ee.dst().into() + } + + async fn property(&self, name: &str) -> Option { + self.ee.properties().get(name).map(|prop| prop.to_string()) + } + + async fn layers(&self) -> Vec { + self.ee.layer_names().map_into().collect() + } + + async fn history(&self) -> Vec { + self.ee.history() + } +} diff --git a/raphtory-graphql/src/model/graph/graph.rs b/raphtory-graphql/src/model/graph/graph.rs new file mode 100644 index 0000000000..7dcc19ddb3 --- /dev/null +++ b/raphtory-graphql/src/model/graph/graph.rs @@ -0,0 +1,222 @@ +use std::{ + collections::{HashMap, HashSet}, + ops::Deref, +}; + +use crate::model::{ + algorithm::Algorithms, + filters::{edge_filter::EdgeFilter, node_filter::NodeFilter}, + graph::{edge::Edge, get_expanded_edges, node::Node, property::Property}, + schema::graph_schema::GraphSchema, +}; +use dynamic_graphql::{ResolvedObject, ResolvedObjectFields}; +use itertools::Itertools; +use raphtory::{ + db::{ + api::view::{ + internal::{DynamicGraph, IntoDynamic}, + GraphViewOps, TimeOps, VertexViewOps, + }, + graph::edge::EdgeView, + }, + prelude::EdgeViewOps, + search::IndexedGraph, +}; + +#[derive(ResolvedObject)] +pub(crate) struct GraphMeta { + name: String, + graph: DynamicGraph, +} + +impl GraphMeta { + pub fn new(name: String, graph: DynamicGraph) -> Self { + Self { name, graph } + } +} + +#[ResolvedObjectFields] +impl GraphMeta { + async fn name(&self) -> String { + self.name.clone() + } + + async fn static_properties(&self) -> Vec { + self.graph + .properties() + .constant() + .into_iter() + .map(|(k, v)| Property::new(k.into(), v)) + .collect() + } + + async fn node_names(&self) -> Vec { + self.graph + .vertices() + .into_iter() + .map(|v| v.name()) + .collect_vec() + } +} + +#[derive(ResolvedObject)] +pub(crate) struct GqlGraph { + graph: IndexedGraph, +} + +impl From> for GqlGraph { + fn from(value: IndexedGraph) -> Self { + Self { + graph: value.into_dynamic_indexed(), + } + } +} + +impl GqlGraph { + pub(crate) fn new(graph: IndexedGraph) -> Self { + Self { graph } + } +} + +#[ResolvedObjectFields] +impl GqlGraph { + /// Return a graph containing only the activity between `start` and `end` measured as milliseconds from epoch + async fn window(&self, start: i64, end: i64) -> GqlGraph { + let w = self.graph.window(start, end); + w.into_dynamic_indexed().into() + } + + async fn layer_names(&self) -> Vec { + self.graph.unique_layers().map_into().collect() + } + + async fn static_properties(&self) -> Vec { + self.graph + .properties() + .constant() + .into_iter() + .map(|(k, v)| Property::new(k.into(), v)) + .collect() + } + + async fn nodes(&self, filter: Option) -> Vec { + match filter { + Some(filter) => self + .graph + .vertices() + .iter() + .map(|vv| vv.into()) + .filter(|n| filter.matches(n)) + .collect(), + None => self.graph.vertices().iter().map(|vv| vv.into()).collect(), + } + } + + /// Returns the schema of this graph + async fn schema(&self) -> GraphSchema { + GraphSchema::new(&self.graph) + } + + async fn search(&self, query: String, limit: usize, offset: usize) -> Vec { + self.graph + .search(&query, limit, offset) + .into_iter() + .flat_map(|vv| vv) + .map(|vv| vv.into()) + .collect() + } + + async fn search_edges(&self, query: String, limit: usize, offset: usize) -> Vec { + self.graph + .search_edges(&query, limit, offset) + .into_iter() + .flat_map(|vv| vv) + .map(|vv| vv.into()) + .collect() + } + + async fn edges<'a>(&self, filter: Option) -> Vec { + match filter { + Some(filter) => self + .graph + .edges() + .into_iter() + .map(|ev| ev.into()) + .filter(|ev| filter.matches(ev)) + .collect(), + None => self.graph.edges().into_iter().map(|ev| ev.into()).collect(), + } + } + + async fn expanded_edges( + &self, + nodes_to_expand: Vec, + graph_nodes: Vec, + filter: Option, + ) -> Vec { + if nodes_to_expand.is_empty() { + return vec![]; + } + + let nodes: Vec = self + .graph + .vertices() + .iter() + .map(|vv| vv.into()) + .filter(|n| NodeFilter::new(nodes_to_expand.clone()).matches(n)) + .collect(); + + let mut all_graph_nodes: HashSet = graph_nodes.into_iter().collect(); + let mut all_expanded_edges: HashMap> = HashMap::new(); + + let mut maybe_layers: Option> = None; + if filter.is_some() { + maybe_layers = filter.clone().unwrap().layer_names.map(|l| l.contains); + } + + for node in nodes { + let expanded_edges = + get_expanded_edges(all_graph_nodes.clone(), node.vv, maybe_layers.clone()); + expanded_edges.clone().into_iter().for_each(|e| { + let src = e.src().name(); + let dst = e.dst().name(); + all_expanded_edges.insert(src.to_owned() + &dst, e); + all_graph_nodes.insert(src); + all_graph_nodes.insert(dst); + }); + } + + let fetched_edges = all_expanded_edges + .values() + .map(|ee| ee.clone().into()) + .collect_vec(); + + match filter { + Some(filter) => fetched_edges + .into_iter() + .filter(|ev| filter.matches(ev)) + .collect(), + None => fetched_edges, + } + } + + async fn node(&self, name: String) -> Option { + self.graph + .vertices() + .iter() + .find(|vv| &vv.name() == &name) + .map(|vv| vv.into()) + } + + async fn node_id(&self, id: u64) -> Option { + self.graph + .vertices() + .iter() + .find(|vv| vv.id() == id) + .map(|vv| vv.into()) + } + + async fn algorithms(&self) -> Algorithms { + self.graph.deref().clone().into() + } +} diff --git a/raphtory-graphql/src/model/graph/mod.rs b/raphtory-graphql/src/model/graph/mod.rs new file mode 100644 index 0000000000..d192304832 --- /dev/null +++ b/raphtory-graphql/src/model/graph/mod.rs @@ -0,0 +1,101 @@ +use itertools::Itertools; +use raphtory::{ + core::ArcStr, + db::{ + api::view::internal::DynamicGraph, + graph::{edge::EdgeView, vertex::VertexView}, + }, + prelude::{EdgeViewOps, VertexViewOps}, +}; +use std::collections::HashSet; + +pub(crate) mod edge; +pub(crate) mod graph; +pub(crate) mod node; +pub(crate) mod property; +pub(crate) mod property_update; + +fn get_expanded_edges( + graph_nodes: HashSet, + vv: VertexView, + maybe_layers: Option>, +) -> Vec> { + let node_found_in_graph_nodes = + |node_name: String| -> bool { graph_nodes.iter().contains(&node_name) }; + + let fetched_edges = vv.clone().edges().into_iter().map(|ee| ee.clone()); + + let mut filtered_fetched_edges = match maybe_layers { + Some(layers) => { + let layer_set: HashSet = layers.into_iter().map_into().collect(); + fetched_edges + .filter(|e| { + e.layer_names() + .into_iter() + .any(|name| layer_set.contains(&name)) + }) + .collect_vec() + } + None => fetched_edges.collect_vec(), + }; + + let first_hop_edges = filtered_fetched_edges + .clone() + .into_iter() + .filter(|e| { + !node_found_in_graph_nodes((*e).src().name()) + || !node_found_in_graph_nodes((*e).dst().name()) + }) + .collect_vec(); + + let mut first_hop_nodes: HashSet = HashSet::new(); + first_hop_edges.clone().into_iter().for_each(|e| { + first_hop_nodes.insert(e.src().name()); + first_hop_nodes.insert(e.dst().name()); + }); + + let first_hop_nodes = first_hop_nodes + .into_iter() + .filter(|e| e != &vv.name()) + .collect_vec(); + + let node_found_in_first_hop_nodes = + |node_name: String| -> bool { first_hop_nodes.contains(&node_name) }; + + let mut first_hop_node_edges: Vec> = vec![]; + + first_hop_edges.into_iter().for_each(|e| { + if node_found_in_graph_nodes(e.src().name()) { + // Return only those edges whose either src or dst already exist + let mut r = e + .dst() + .edges() + .filter(|e| { + (node_found_in_first_hop_nodes(e.src().name()) + && node_found_in_first_hop_nodes(e.dst().name())) + || node_found_in_graph_nodes(e.src().name()) + || node_found_in_graph_nodes(e.dst().name()) + }) + .collect_vec(); + + first_hop_node_edges.append(&mut r); + } else { + let mut r = e + .src() + .edges() + .filter(|e| { + (node_found_in_first_hop_nodes(e.src().name()) + && node_found_in_first_hop_nodes(e.dst().name())) + || node_found_in_graph_nodes(e.src().name()) + || node_found_in_graph_nodes(e.dst().name()) + }) + .collect_vec(); + + first_hop_node_edges.append(&mut r); + } + }); + + filtered_fetched_edges.append(&mut first_hop_node_edges); + + filtered_fetched_edges +} diff --git a/raphtory-graphql/src/model/graph/node.rs b/raphtory-graphql/src/model/graph/node.rs new file mode 100644 index 0000000000..739d5a1101 --- /dev/null +++ b/raphtory-graphql/src/model/graph/node.rs @@ -0,0 +1,265 @@ +use crate::model::{ + filters::edge_filter::EdgeFilter, + graph::{edge::Edge, get_expanded_edges, property::Property, property_update::PropertyUpdate}, +}; +use dynamic_graphql::{ResolvedObject, ResolvedObjectFields}; +use itertools::Itertools; +use raphtory::db::{ + api::view::{ + internal::{DynamicGraph, IntoDynamic}, + *, + }, + graph::vertex::VertexView, +}; +use std::collections::HashSet; + +use super::property_update::PropertyUpdateGroup; + +#[derive(ResolvedObject)] +pub(crate) struct Node { + pub(crate) vv: VertexView, +} + +impl From> for Node { + fn from(value: VertexView) -> Self { + Self { + vv: VertexView { + graph: value.graph.clone().into_dynamic(), + vertex: value.vertex, + }, + } + } +} + +#[ResolvedObjectFields] +impl Node { + async fn id(&self) -> u64 { + self.vv.id() + } + + pub async fn name(&self) -> String { + self.vv.name() + } + + pub async fn node_type(&self) -> String { + self.vv + .properties() + .get("type") + .map(|p| p.to_string()) + .unwrap_or("NONE".to_string()) + } + + /// Returns all the property names this node has a value for + async fn property_names(&self) -> Vec { + self.vv.properties().keys().map_into().collect() + } + + /// Returns all the properties of the node + async fn properties(&self) -> Option> { + Some( + self.vv + .properties() + .iter() + .map(|(k, v)| Property::new(k.into(), v)) + .collect(), + ) + } + + /// Returns the value for the property with name `name` + async fn property(&self, name: &str) -> Option { + self.vv.properties().get(name).map(|v| v.to_string()) + } + + /// Returns the history as a vector of updates for the property with name `name` + async fn property_history(&self, name: String) -> Vec { + self.vv + .properties() + .temporal() + .get(&name) + .into_iter() + .flat_map(|p| { + p.iter() + .map(|(time, prop)| PropertyUpdate::new(time, prop.to_string())) + }) + .collect() + } + + /// Returns the history as a vectory of updates for any properties which are included in param names + async fn properties_history(&self, names: Vec) -> Vec { + names + .iter() + .filter_map(|name| match self.vv.properties().temporal().get(name) { + Some(prop) => Option::Some(PropertyUpdateGroup::new( + name.to_string(), + prop.iter() + .map(|(time, prop)| PropertyUpdate::new(time, prop.to_string())) + .collect_vec(), + )), + None => None, + }) + .collect_vec() + } + + async fn in_neighbours<'a>(&self, layer: Option) -> Vec { + match layer.as_deref() { + None => self.vv.in_neighbours().iter().map(|vv| vv.into()).collect(), + Some(layer) => match self.vv.layer(layer) { + None => { + vec![] + } + Some(vvv) => vvv.in_neighbours().iter().map(|vv| vv.into()).collect(), + }, + } + } + + async fn out_neighbours(&self, layer: Option) -> Vec { + match layer.as_deref() { + None => self + .vv + .out_neighbours() + .iter() + .map(|vv| vv.into()) + .collect(), + Some(layer) => match self.vv.layer(layer) { + None => { + vec![] + } + Some(vvv) => vvv.out_neighbours().iter().map(|vv| vv.into()).collect(), + }, + } + } + + async fn neighbours<'a>(&self, layer: Option) -> Vec { + match layer.as_deref() { + None => self.vv.neighbours().iter().map(|vv| vv.into()).collect(), + Some(layer) => match self.vv.layer(layer) { + None => { + vec![] + } + Some(vvv) => vvv.neighbours().iter().map(|vv| vv.into()).collect(), + }, + } + } + + /// Returns the number of edges connected to this node + async fn degree(&self, layers: Option>) -> usize { + match layers { + None => self.vv.degree(), + Some(layers) => layers + .iter() + .map(|layer| { + let degree = match self.vv.layer(layer) { + None => 0, + Some(vvv) => vvv.degree(), + }; + degree + }) + .sum(), + } + } + + /// Returns the number edges with this node as the source + async fn out_degree(&self, layer: Option) -> usize { + match layer.as_deref() { + None => self.vv.out_degree(), + Some(layer) => match self.vv.layer(layer) { + None => 0, + Some(vvv) => vvv.out_degree(), + }, + } + } + + /// Returns the number edges with this node as the destination + async fn in_degree(&self, layer: Option) -> usize { + match layer.as_deref() { + None => self.vv.in_degree(), + Some(layer) => match self.vv.layer(layer) { + None => 0, + Some(vvv) => vvv.in_degree(), + }, + } + } + + async fn out_edges(&self, layer: Option) -> Vec { + match layer.as_deref() { + None => self.vv.out_edges().map(|ee| ee.into()).collect(), + Some(layer) => match self.vv.layer(layer) { + None => { + vec![] + } + Some(vvv) => vvv.out_edges().map(|ee| ee.into()).collect(), + }, + } + } + + async fn in_edges(&self, layer: Option) -> Vec { + match layer.as_deref() { + None => self.vv.in_edges().map(|ee| ee.into()).collect(), + Some(layer) => match self.vv.layer(layer) { + None => { + vec![] + } + Some(vvv) => vvv.in_edges().map(|ee| ee.into()).collect(), + }, + } + } + + async fn edges(&self, filter: Option) -> Vec { + match filter { + Some(filter) => self + .vv + .edges() + .map(|ev| ev.into()) + .filter(|ev| filter.matches(ev)) + .collect(), + None => self.vv.edges().map(|ee| ee.into()).collect(), + } + } + + async fn expanded_edges( + &self, + graph_nodes: Vec, + filter: Option, + ) -> Vec { + let all_graph_nodes: HashSet = graph_nodes.into_iter().collect(); + + match filter { + Some(edge_filter) => { + let maybe_layers = edge_filter.clone().layer_names.map(|l| l.contains); + let fetched_edges = + get_expanded_edges(all_graph_nodes, self.vv.clone(), maybe_layers) + .iter() + .map(|ee| ee.clone().into()) + .collect_vec(); + fetched_edges + .into_iter() + .filter(|ev| edge_filter.matches(ev)) + .collect() + } + None => get_expanded_edges(all_graph_nodes, self.vv.clone(), None) + .iter() + .map(|ee| ee.clone().into()) + .collect_vec(), + } + } + + async fn exploded_in_edges(&self) -> Vec { + self.vv.in_edges().explode().map(|ee| ee.into()).collect() + } + + async fn exploded_out_edges(&self) -> Vec { + self.vv.out_edges().explode().map(|ee| ee.into()).collect() + } + + async fn exploded_edges(&self) -> Vec { + self.vv.edges().explode().map(|ee| ee.into()).collect() + } + + async fn start_date(&self) -> Option { + self.vv.earliest_time() + } + + async fn end_date(&self) -> Option { + self.vv.latest_time() + } +} diff --git a/raphtory-graphql/src/model/graph/property.rs b/raphtory-graphql/src/model/graph/property.rs new file mode 100644 index 0000000000..f69e2eba19 --- /dev/null +++ b/raphtory-graphql/src/model/graph/property.rs @@ -0,0 +1,25 @@ +use dynamic_graphql::{ResolvedObject, ResolvedObjectFields}; +use raphtory::core::Prop; + +#[derive(ResolvedObject)] +pub(crate) struct Property { + key: String, + value: Prop, +} + +impl Property { + pub(crate) fn new(key: String, value: Prop) -> Self { + Self { key, value } + } +} + +#[ResolvedObjectFields] +impl Property { + async fn key(&self) -> String { + self.key.to_string() + } + + async fn value(&self) -> String { + self.value.to_string() + } +} diff --git a/raphtory-graphql/src/model/graph/property_update.rs b/raphtory-graphql/src/model/graph/property_update.rs new file mode 100644 index 0000000000..3a7dddd1e7 --- /dev/null +++ b/raphtory-graphql/src/model/graph/property_update.rs @@ -0,0 +1,30 @@ +use dynamic_graphql::SimpleObject; + +/// A single property at a given `time` with a given `value` +#[derive(SimpleObject)] +pub(crate) struct PropertyUpdate { + pub(crate) time: i64, + pub(crate) value: String, +} + +// A collection of `PropertyUpdate`s under their `propertyName` +#[derive(SimpleObject)] +pub(crate) struct PropertyUpdateGroup { + pub(crate) property_name: String, + pub(crate) property_updates: Vec, +} + +impl PropertyUpdate { + pub fn new(time: i64, value: String) -> Self { + Self { time, value } + } +} + +impl PropertyUpdateGroup { + pub fn new(property_name: String, property_updates: Vec) -> Self { + Self { + property_name, + property_updates, + } + } +} diff --git a/raphtory-graphql/src/model/mod.rs b/raphtory-graphql/src/model/mod.rs index d87e0bc979..4e2215bfff 100644 --- a/raphtory-graphql/src/model/mod.rs +++ b/raphtory-graphql/src/model/mod.rs @@ -1,19 +1,45 @@ -use crate::data::Data; +use crate::{ + data::Data, + model::graph::graph::{GqlGraph, GraphMeta}, +}; use async_graphql::Context; -use dynamic_graphql::{ResolvedObject, ResolvedObjectFields}; +use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; +use chrono::Utc; +use dynamic_graphql::{ + App, Mutation, MutationFields, MutationRoot, ResolvedObject, ResolvedObjectFields, Result, + Upload, +}; use itertools::Itertools; -use raphtory::core::Prop; -use raphtory::db::edge::EdgeView; -use raphtory::db::vertex::VertexView; -use raphtory::db::view_api::internal::{GraphViewInternalOps, WrappedGraph}; -use raphtory::db::view_api::EdgeListOps; -use raphtory::db::view_api::EdgeViewOps; -use raphtory::db::view_api::{GraphViewOps, TimeOps, VertexViewOps}; -use std::sync::Arc; - -use crate::model::algorithm::Algorithms; +use raphtory::{ + core::{ArcStr, Prop}, + db::api::view::internal::{IntoDynamic, MaterializedGraph}, + prelude::{Graph, GraphViewOps, PropertyAdditionOps, VertexViewOps}, + search::IndexedGraph, +}; +use std::{ + collections::HashMap, + error::Error, + fmt::{Display, Formatter}, + io::BufReader, + ops::Deref, +}; +use uuid::Uuid; pub(crate) mod algorithm; +pub(crate) mod filters; +pub(crate) mod graph; +pub(crate) mod schema; + +#[derive(Debug)] +pub struct MissingGraph; + +impl Display for MissingGraph { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "Graph does not exist") + } +} + +impl Error for MissingGraph {} #[derive(ResolvedObject)] #[graphql(root)] @@ -25,219 +51,325 @@ impl QueryRoot { "Hello world from raphtory-graphql" } - /// Returns a view including all events between `t_start` (inclusive) and `t_end` (exclusive) + /// Returns a graph async fn graph<'a>(ctx: &Context<'a>, name: &str) -> Option { let data = ctx.data_unchecked::(); - let g = data.graphs.get(name)?; - Some(g.clone().into()) + let g = data.graphs.read().get(name).cloned()?; + Some(GqlGraph::new(g.into_dynamic_indexed())) } -} -#[derive(Clone)] -pub struct DynamicGraph(Arc); - -impl WrappedGraph for DynamicGraph { - type Internal = dyn GraphViewInternalOps + Send + Sync + 'static; - fn as_graph(&self) -> &(dyn GraphViewInternalOps + Send + Sync + 'static) { - &*self.0 - } -} - -#[derive(ResolvedObject)] -pub(crate) struct GqlGraph { - graph: DynamicGraph, -} - -impl From for GqlGraph { - fn from(value: G) -> Self { - let graph = DynamicGraph(Arc::new(value)); - Self { graph } - } -} - -#[ResolvedObjectFields] -impl GqlGraph { - async fn window(&self, t_start: i64, t_end: i64) -> GqlGraph { - let w = self.graph.window(t_start, t_end); - w.into() - } - - async fn nodes(&self) -> Vec { - self.graph.vertices().iter().map(|vv| vv.into()).collect() - } - - async fn edges<'a>(&self) -> Vec { - self.graph.edges().into_iter().map(|ev| ev.into()).collect() - } - - async fn node(&self, name: String) -> Option { - self.graph - .vertices() - .iter() - .find(|vv| &vv.name() == &name) - .map(|vv| vv.into()) + async fn subgraph<'a>(ctx: &Context<'a>, name: &str) -> Option { + let data = ctx.data_unchecked::(); + let g = data.graphs.read().get(name).cloned()?; + Some(GraphMeta::new( + name.to_string(), + g.deref().clone().into_dynamic(), + )) } - async fn node_id(&self, id: u64) -> Option { - self.graph - .vertices() + async fn subgraphs<'a>(ctx: &Context<'a>) -> Vec { + let data = ctx.data_unchecked::(); + data.graphs + .read() .iter() - .find(|vv| vv.id() == id) - .map(|vv| vv.into()) - } - - async fn algorithms(&self) -> Algorithms { - self.graph.clone().into() - } -} - -#[derive(ResolvedObject)] -pub(crate) struct Property { - key: String, - value: Prop, -} - -impl Property { - fn new(key: String, value: Prop) -> Self { - Self { key, value } - } -} - -#[ResolvedObjectFields] -impl Property { - async fn key(&self, _ctx: &Context<'_>) -> String { - self.key.to_string() - } - - async fn value(&self, _ctx: &Context<'_>) -> String { - self.value.to_string() - } -} - -#[derive(ResolvedObject)] -pub(crate) struct Node { - vv: VertexView, -} - -impl From> for Node { - fn from(vv: VertexView) -> Self { - Self { vv } - } -} - -#[ResolvedObjectFields] -impl Node { - async fn id(&self) -> u64 { - self.vv.id() - } - - async fn name(&self) -> String { - self.vv.name() + .map(|(name, g)| GraphMeta::new(name.clone(), g.deref().clone().into_dynamic())) + .collect_vec() } - async fn property_names<'a>(&self, _ctx: &Context<'a>) -> Vec { - self.vv.property_names(true) - } - - async fn properties(&self) -> Option> { + async fn receive_graph<'a>(ctx: &Context<'a>, name: &str) -> Result { + let data = ctx.data_unchecked::(); + let g = data + .graphs + .read() + .get(name) + .cloned() + .ok_or(MissingGraph)? + .materialize()?; + let bincode = bincode::serialize(&g)?; + Ok(URL_SAFE_NO_PAD.encode(bincode)) + } + + async fn similarity_search<'a>( + ctx: &Context<'a>, + graph: &str, + query: &str, + init: Option, + min_nodes: Option, + min_edges: Option, + limit: Option, + window_start: Option, + window_end: Option, + ) -> Option> { + let init = init.unwrap_or(1); + let min_nodes = min_nodes.unwrap_or(0); + let min_edges = min_edges.unwrap_or(0); + let limit = limit.unwrap_or(1); + let data = ctx.data_unchecked::(); + let binding = data.vector_stores.read(); + let vec_store = binding.get(graph)?; + println!("running similarity search for {query}"); Some( - self.vv - .properties(true) - .into_iter() - .map(|(k, v)| Property::new(k, v)) - .collect_vec(), + vec_store + .similarity_search( + query, + init, + min_nodes, + min_edges, + limit, + window_start, + window_end, + ) + .await, ) } - - async fn property(&self, name: String) -> Option { - let prop = self.vv.property(name.clone(), true)?; - Some(Property::new(name, prop)) - } - - async fn in_neighbours<'a>(&self, _ctx: &Context<'a>) -> Vec { - self.vv.in_neighbours().iter().map(|vv| vv.into()).collect() - } - - async fn out_neighbours(&self) -> Vec { - self.vv - .out_neighbours() - .iter() - .map(|vv| vv.into()) - .collect() - } - - async fn neighbours<'a>(&self, _ctx: &Context<'a>) -> Vec { - self.vv.neighbours().iter().map(|vv| vv.into()).collect() - } - - async fn degree(&self) -> usize { - self.vv.degree() - } - - async fn out_degree(&self) -> usize { - self.vv.out_degree() - } - - async fn in_degree(&self) -> usize { - self.vv.in_degree() - } - - async fn out_edges(&self) -> Vec { - self.vv.out_edges().map(|ee| ee.clone().into()).collect() - } - - async fn in_edges(&self) -> Vec { - self.vv.in_edges().map(|ee| ee.into()).collect() - } - - async fn exploded_edges(&self) -> Vec { - self.vv.out_edges().explode().map(|ee| ee.into()).collect() - } - - async fn start_date(&self) -> Option { - self.vv.earliest_time() - } - - async fn end_date(&self) -> Option { - self.vv.latest_time() - } -} - -#[derive(ResolvedObject)] -pub(crate) struct Edge { - ee: EdgeView, } -impl From> for Edge { - fn from(ee: EdgeView) -> Self { - Self { ee } +#[derive(MutationRoot)] +pub(crate) struct MutRoot; + +#[derive(Mutation)] +pub(crate) struct Mut(MutRoot); + +#[MutationFields] +impl Mut { + /// Load graphs from a directory of bincode files (existing graphs with the same name are overwritten) + /// + /// # Returns: + /// list of names for newly added graphs + async fn load_graphs_from_path<'a>(ctx: &Context<'a>, path: String) -> Vec { + let new_graphs = Data::load_from_file(&path); + let keys: Vec<_> = new_graphs.keys().cloned().collect(); + let mut data = ctx.data_unchecked::().graphs.write(); + data.extend(new_graphs); + keys + } + + async fn rename_graph<'a>( + ctx: &Context<'a>, + parent_graph_name: String, + graph_name: String, + new_graph_name: String, + ) -> Result { + if new_graph_name.ne(&graph_name) && parent_graph_name.ne(&graph_name) { + let mut data = ctx.data_unchecked::().graphs.write(); + + let subgraph = data.get(&graph_name).ok_or("Graph not found")?; + let path = subgraph + .properties() + .constant() + .get("path") + .ok_or("Path is missing")? + .to_string(); + + let parent_graph = data.get(&parent_graph_name).ok_or("Graph not found")?; + let new_subgraph = parent_graph + .subgraph(subgraph.vertices().iter().map(|v| v.name()).collect_vec()) + .materialize()?; + + let static_props_without_name: Vec<(ArcStr, Prop)> = subgraph + .properties() + .into_iter() + .filter(|(a, _)| a != "name") + .collect_vec(); + + new_subgraph.add_constant_properties(static_props_without_name)?; + new_subgraph + .add_constant_properties([("name", Prop::Str(new_graph_name.clone().into()))])?; + + let dt = Utc::now(); + let timestamp: i64 = dt.timestamp(); + new_subgraph.add_constant_properties([("lastUpdated", Prop::I64(timestamp * 1000))])?; + + new_subgraph.save_to_file(path)?; + + let gi: IndexedGraph = new_subgraph + .into_events() + .ok_or("Graph with deletions not supported")? + .into(); + + data.insert(new_graph_name, gi); + data.remove(&graph_name); + } + + Ok(true) + } + + async fn save_graph<'a>( + ctx: &Context<'a>, + parent_graph_name: String, + graph_name: String, + new_graph_name: String, + props: String, + graph_nodes: Vec, + ) -> Result { + let mut data = ctx.data_unchecked::().graphs.write(); + + let subgraph = data.get(&graph_name).ok_or("Graph not found")?; + let mut path = subgraph + .properties() + .constant() + .get("path") + .ok_or("Path is missing")? + .to_string(); + + if new_graph_name.ne(&graph_name) { + fn path_prefix(path: String) -> Result { + let elements: Vec<&str> = path.split('/').collect(); + let size = elements.len(); + return if size > 2 { + let delimiter = "/"; + let joined_string = elements + .iter() + .take(size - 1) + .copied() + .collect::>() + .join(delimiter); + Ok(joined_string) + } else { + Err("Invalid graph path".into()) + }; + } + + path = path_prefix(path)? + "/" + &Uuid::new_v4().hyphenated().to_string(); + } + + let parent_graph = data.get(&parent_graph_name).ok_or("Graph not found")?; + + let new_subgraph = parent_graph.subgraph(graph_nodes).materialize()?; + + new_subgraph.add_constant_properties([("name", Prop::str(new_graph_name.clone()))])?; + + // parent_graph_name == graph_name, means its a graph created from UI + if parent_graph_name.ne(&graph_name) { + // graph_name == new_graph_name, means its a "save" and not "save as" action + if graph_name.ne(&new_graph_name) { + let static_props: Vec<(ArcStr, Prop)> = subgraph + .properties() + .into_iter() + .filter(|(a, _)| a != "name" && a != "creationTime" && a != "uiProps") + .collect_vec(); + new_subgraph.add_constant_properties(static_props)?; + } else { + let static_props: Vec<(ArcStr, Prop)> = subgraph + .properties() + .into_iter() + .filter(|(a, _)| a != "name" && a != "lastUpdated" && a != "uiProps") + .collect_vec(); + new_subgraph.add_constant_properties(static_props)?; + } + } + + let dt = Utc::now(); + let timestamp: i64 = dt.timestamp(); + + if parent_graph_name.eq(&graph_name) || graph_name.ne(&new_graph_name) { + new_subgraph + .add_constant_properties([("creationTime", Prop::I64(timestamp * 1000))])?; + } + + new_subgraph.add_constant_properties([("lastUpdated", Prop::I64(timestamp * 1000))])?; + new_subgraph.add_constant_properties([("uiProps", Prop::Str(props.into()))])?; + + new_subgraph.save_to_file(path)?; + + let gi: IndexedGraph = new_subgraph + .into_events() + .ok_or("Graph with deletions not supported")? + .into(); + + data.insert(new_graph_name, gi); + + Ok(true) + } + + /// Load new graphs from a directory of bincode files (existing graphs will not been overwritten) + /// + /// # Returns: + /// list of names for newly added graphs + async fn load_new_graphs_from_path<'a>(ctx: &Context<'a>, path: String) -> Vec { + let mut data = ctx.data_unchecked::().graphs.write(); + let new_graphs: HashMap<_, _> = Data::load_from_file(&path) + .into_iter() + .filter(|(key, _)| !data.contains_key(key)) + .collect(); + let keys: Vec<_> = new_graphs.keys().cloned().collect(); + data.extend(new_graphs); + keys + } + + /// Use GQL multipart upload to send new graphs to server + /// + /// # Returns: + /// name of the new graph + async fn upload_graph<'a>(ctx: &Context<'a>, name: String, graph: Upload) -> Result { + let g: MaterializedGraph = + bincode::deserialize_from(BufReader::new(graph.value(ctx)?.content))?; + let gi: IndexedGraph = g + .into_events() + .ok_or("Graph with deletions not supported")? + .into(); + let mut data = ctx.data_unchecked::().graphs.write(); + data.insert(name.clone(), gi); + Ok(name) + } + + /// Send graph bincode as base64 encoded string + /// + /// # Returns: + /// name of the new graph + async fn send_graph<'a>(ctx: &Context<'a>, name: String, graph: String) -> Result { + let g: MaterializedGraph = bincode::deserialize(&URL_SAFE_NO_PAD.decode(graph)?)?; + let mut data = ctx.data_unchecked::().graphs.write(); + data.insert( + name.clone(), + g.into_events() + .ok_or("Graph with deletions not supported")? + .into(), + ); + Ok(name) + } + + async fn archive_graph<'a>( + ctx: &Context<'a>, + graph_name: String, + parent_graph_name: String, + is_archive: u8, + ) -> Result { + let mut data = ctx.data_unchecked::().graphs.write(); + + let subgraph = data.get(&graph_name).ok_or("Graph not found")?; + + let path = subgraph + .properties() + .constant() + .get("path") + .ok_or("Path is missing")? + .to_string(); + + let parent_graph = data.get(&parent_graph_name).ok_or("Graph not found")?; + let new_subgraph = parent_graph + .subgraph(subgraph.vertices().iter().map(|v| v.name()).collect_vec()) + .materialize()?; + + let static_props_without_isactive: Vec<(ArcStr, Prop)> = subgraph + .properties() + .into_iter() + .filter(|(a, _)| a != "isArchive") + .collect_vec(); + new_subgraph.add_constant_properties(static_props_without_isactive)?; + new_subgraph.add_constant_properties([("isArchive", Prop::U8(is_archive))])?; + new_subgraph.save_to_file(path)?; + + let gi: IndexedGraph = new_subgraph + .into_events() + .ok_or("Graph with deletions not supported")? + .into(); + + data.insert(graph_name, gi); + + Ok(true) } } -#[ResolvedObjectFields] -impl Edge { - async fn earliest_time(&self) -> Option { - self.ee.earliest_time() - } - - async fn latest_time(&self) -> Option { - self.ee.latest_time() - } - - async fn src(&self) -> Node { - self.ee.src().into() - } - - async fn dst(&self) -> Node { - self.ee.dst().into() - } - - async fn property(&self, name: String) -> Option { - let prop = self.ee.property(name.clone(), true)?; - Some(Property::new(name, prop)) - } - - async fn history(&self) -> Vec { - self.ee.history() - } -} +#[derive(App)] +pub(crate) struct App(QueryRoot, MutRoot, Mut); diff --git a/raphtory-graphql/src/model/schema/edge_schema.rs b/raphtory-graphql/src/model/schema/edge_schema.rs new file mode 100644 index 0000000000..19cc277a39 --- /dev/null +++ b/raphtory-graphql/src/model/schema/edge_schema.rs @@ -0,0 +1,65 @@ +use crate::model::schema::{ + get_vertex_type, merge_schemas, property_schema::PropertySchema, SchemaAggregate, +}; +use dynamic_graphql::{ResolvedObject, ResolvedObjectFields}; +use itertools::Itertools; +use raphtory::{ + db::graph::edge::EdgeView, + prelude::{EdgeViewOps, GraphViewOps}, +}; +use std::collections::{HashMap, HashSet}; + +#[derive(ResolvedObject)] +pub(crate) struct EdgeSchema { + graph: G, + src_type: String, + dst_type: String, +} + +impl EdgeSchema { + pub fn new(graph: G, src_type: String, dst_type: String) -> Self { + Self { + graph, + src_type, + dst_type, + } + } +} + +#[ResolvedObjectFields] +impl EdgeSchema { + /// Returns the type of source for these edges + async fn src_type(&self) -> String { + self.src_type.clone() + } + + /// Returns the type of destination for these edges + async fn dst_type(&self) -> String { + self.dst_type.clone() + } + + /// Returns the list of property schemas for edges connecting these types of nodes + async fn properties(&self) -> Vec { + let filter_types = |edge: &EdgeView| { + let src_type = get_vertex_type(edge.src()); + let dst_type = get_vertex_type(edge.dst()); + src_type == self.src_type && dst_type == self.dst_type + }; + + let filtered_edges = self.graph.edges().filter(filter_types); + + let schema: SchemaAggregate = filtered_edges + .map(collect_edge_schema) + .reduce(merge_schemas) + .unwrap_or_else(|| HashMap::new()); + + schema.into_iter().map(|prop| prop.into()).collect_vec() + } +} + +fn collect_edge_schema(edge: EdgeView) -> SchemaAggregate { + edge.properties() + .iter() + .map(|(key, value)| (key.to_string(), HashSet::from([value.to_string()]))) + .collect() +} diff --git a/raphtory-graphql/src/model/schema/graph_schema.rs b/raphtory-graphql/src/model/schema/graph_schema.rs new file mode 100644 index 0000000000..8254872075 --- /dev/null +++ b/raphtory-graphql/src/model/schema/graph_schema.rs @@ -0,0 +1,32 @@ +use crate::model::schema::{layer_schema::LayerSchema, node_schema::NodeSchema}; +use dynamic_graphql::SimpleObject; +use itertools::Itertools; +use raphtory::{ + db::api::view::internal::DynamicGraph, + prelude::{GraphViewOps, LayerOps, VertexViewOps}, +}; + +#[derive(SimpleObject)] +pub(crate) struct GraphSchema { + nodes: Vec, + layers: Vec>, +} + +impl GraphSchema { + pub fn new(graph: &DynamicGraph) -> Self { + let nodes = graph + .vertices() + .iter() + .filter_map(|vertex| vertex.properties().get("type").map(|p| p.to_string())) + .unique() + .map(|node_type| NodeSchema::new(node_type, graph.clone())) + .collect_vec(); + + let layers = graph + .unique_layers() + .map(|layer_name| graph.layer(layer_name).unwrap().into()) + .collect_vec(); + + GraphSchema { nodes, layers } + } +} diff --git a/raphtory-graphql/src/model/schema/layer_schema.rs b/raphtory-graphql/src/model/schema/layer_schema.rs new file mode 100644 index 0000000000..5467b568d6 --- /dev/null +++ b/raphtory-graphql/src/model/schema/layer_schema.rs @@ -0,0 +1,46 @@ +use crate::model::schema::{edge_schema::EdgeSchema, get_vertex_type}; +use dynamic_graphql::{ResolvedObject, ResolvedObjectFields}; +use itertools::Itertools; +use raphtory::{ + db::graph::views::layer_graph::LayeredGraph, + prelude::{EdgeViewOps, GraphViewOps}, +}; + +#[derive(ResolvedObject)] +pub(crate) struct LayerSchema { + graph: LayeredGraph, +} + +impl From> for LayerSchema { + fn from(value: LayeredGraph) -> Self { + Self { graph: value } + } +} + +#[ResolvedObjectFields] +impl LayerSchema { + /// Returns the name of the layer with this schema + async fn name(&self) -> String { + let mut layers = self.graph.unique_layers(); + let layer = layers.next().expect("Layered graph has a layer"); + debug_assert!( + layers.next().is_none(), + "Layered graph outputted more than one layer name" + ); + layer.into() + } + /// Returns the list of edge schemas for this edge layer + async fn edges(&self) -> Vec>> { + self.graph + .edges() + .into_iter() + .map(|edge| { + let src_type = get_vertex_type(edge.src()); + let dst_type = get_vertex_type(edge.dst()); + (src_type, dst_type) + }) + .unique() + .map(|(src_type, dst_type)| EdgeSchema::new(self.graph.clone(), src_type, dst_type)) + .collect_vec() + } +} diff --git a/raphtory-graphql/src/model/schema/mod.rs b/raphtory-graphql/src/model/schema/mod.rs new file mode 100644 index 0000000000..5d10db0d79 --- /dev/null +++ b/raphtory-graphql/src/model/schema/mod.rs @@ -0,0 +1,39 @@ +use raphtory::{ + db::graph::vertex::VertexView, + prelude::{GraphViewOps, VertexViewOps}, +}; +use std::collections::{HashMap, HashSet}; + +pub(crate) mod edge_schema; +pub(crate) mod graph_schema; +pub(crate) mod layer_schema; +pub(crate) mod node_schema; +pub(crate) mod property_schema; + +const ENUM_BOUNDARY: usize = 20; + +fn get_vertex_type(vertex: VertexView) -> String { + let prop = vertex.properties().get("type"); + prop.map(|prop| prop.to_string()) + .unwrap_or_else(|| "NONE".to_string()) +} + +type SchemaAggregate = HashMap>; + +fn merge_schemas(mut s1: SchemaAggregate, s2: SchemaAggregate) -> SchemaAggregate { + for (key, set2) in s2 { + if let Some(set1) = s1.get_mut(&key) { + // Here, an empty set means: too many values to be interpreted as an enumerated type + if set1.len() > 0 && set2.len() > 0 { + set1.extend(set2); + } + if set1.len() > ENUM_BOUNDARY { + set1.clear(); + } + } else { + s1.insert(key, set2); + } + } + + s1 +} diff --git a/raphtory-graphql/src/model/schema/node_schema.rs b/raphtory-graphql/src/model/schema/node_schema.rs new file mode 100644 index 0000000000..3516569f91 --- /dev/null +++ b/raphtory-graphql/src/model/schema/node_schema.rs @@ -0,0 +1,56 @@ +use crate::model::schema::{merge_schemas, property_schema::PropertySchema, SchemaAggregate}; +use dynamic_graphql::{ResolvedObject, ResolvedObjectFields}; +use itertools::Itertools; +use raphtory::{ + db::{api::view::internal::DynamicGraph, graph::vertex::VertexView}, + prelude::{GraphViewOps, VertexViewOps}, +}; +use std::collections::{HashMap, HashSet}; + +#[derive(ResolvedObject)] +pub(crate) struct NodeSchema { + type_name: String, + graph: DynamicGraph, +} + +impl NodeSchema { + pub fn new(node_type: String, graph: DynamicGraph) -> Self { + Self { + type_name: node_type, + graph, + } + } +} + +#[ResolvedObjectFields] +impl NodeSchema { + async fn type_name(&self) -> String { + self.type_name.clone() + } + + /// Returns the list of property schemas for this node + async fn properties(&self) -> Vec { + let filter_type = |vertex: &VertexView| match vertex.properties().get("type") + { + Some(node_type) => node_type.to_string() == self.type_name, + None => false, + }; + + let filtered_vertices = self.graph.vertices().iter().filter(filter_type); + + let schema: SchemaAggregate = filtered_vertices + .map(collect_vertex_schema) + .reduce(merge_schemas) + .unwrap_or_else(|| HashMap::new()); + + schema.into_iter().map(|prop| prop.into()).collect_vec() + } +} + +fn collect_vertex_schema(vertex: VertexView) -> SchemaAggregate { + vertex + .properties() + .iter() + .map(|(key, value)| (key.to_string(), HashSet::from([value.to_string()]))) + .collect() +} diff --git a/raphtory-graphql/src/model/schema/property_schema.rs b/raphtory-graphql/src/model/schema/property_schema.rs new file mode 100644 index 0000000000..edb40cfadf --- /dev/null +++ b/raphtory-graphql/src/model/schema/property_schema.rs @@ -0,0 +1,24 @@ +use dynamic_graphql::SimpleObject; +use std::collections::HashSet; + +#[derive(SimpleObject)] +pub(crate) struct PropertySchema { + key: String, + variants: Vec, +} + +// impl PropertySchema { +// pub fn new(key: String, values: Vec) -> Self { +// Self { key, values } +// } +// } + +impl From<(String, HashSet)> for PropertySchema { + fn from(value: (String, HashSet)) -> Self { + let (key, set) = value; + PropertySchema { + key, + variants: Vec::from_iter(set), + } + } +} diff --git a/raphtory-graphql/src/observability/tracing.rs b/raphtory-graphql/src/observability/tracing.rs index 7b4f1fb8c7..2e3986ede5 100644 --- a/raphtory-graphql/src/observability/tracing.rs +++ b/raphtory-graphql/src/observability/tracing.rs @@ -1,6 +1,10 @@ -use opentelemetry::sdk::trace::{self, Sampler}; use opentelemetry::{ - global, runtime::Tokio, sdk::propagation::TraceContextPropagator, sdk::trace::Tracer, + global, + runtime::Tokio, + sdk::{ + propagation::TraceContextPropagator, + trace::{self, Sampler, Tracer}, + }, }; use std::env; diff --git a/raphtory-graphql/src/routes.rs b/raphtory-graphql/src/routes.rs index 6669027a0c..22865da9c4 100644 --- a/raphtory-graphql/src/routes.rs +++ b/raphtory-graphql/src/routes.rs @@ -1,9 +1,11 @@ use async_graphql::http::{playground_source, GraphQLPlaygroundConfig}; -use poem::http::StatusCode; -use poem::web::{Html, Json}; -use poem::{handler, IntoResponse}; +use poem::{ + handler, + http::StatusCode, + web::{Html, Json}, + IntoResponse, +}; use serde::Serialize; -use tracing::{span, Instrument, Level}; #[derive(Serialize)] struct Health { diff --git a/raphtory-graphql/src/server.rs b/raphtory-graphql/src/server.rs index 12c2fabee0..d9410bde50 100644 --- a/raphtory-graphql/src/server.rs +++ b/raphtory-graphql/src/server.rs @@ -1,29 +1,86 @@ -use crate::data::Data; -use crate::model::algorithm::Algorithm; -use crate::model::QueryRoot; -use crate::observability::tracing::create_tracer_from_env; -use crate::routes::{graphql_playground, health}; +#![allow(dead_code)] + +use crate::{ + data::Data, + model::{algorithm::Algorithm, App}, + observability::tracing::create_tracer_from_env, + routes::{graphql_playground, health}, +}; use async_graphql_poem::GraphQL; -use dynamic_graphql::App; -use poem::listener::TcpListener; -use poem::middleware::Cors; -use poem::{get, EndpointExt, Route, Server}; -use tokio::io::Result as IoResult; -use tokio::signal; -use tracing_subscriber::layer::SubscriberExt; -use tracing_subscriber::util::SubscriberInitExt; -use tracing_subscriber::Registry; +use poem::{get, listener::TcpListener, middleware::Cors, EndpointExt, Route, Server}; +use raphtory::{ + db::graph::{edge::EdgeView, vertex::VertexView}, + prelude::Graph, + vectors::{Embedding, Vectorizable}, +}; +use std::{collections::HashMap, future::Future, ops::Deref, path::Path}; +use tokio::{io::Result as IoResult, signal}; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Registry}; pub struct RaphtoryServer { data: Data, } impl RaphtoryServer { - pub fn new(graph_directory: &str) -> Self { - let data = Data::load(graph_directory); + pub fn from_map(graphs: HashMap) -> Self { + let data = Data::from_map(graphs); + Self { data } + } + + pub fn from_directory(graph_directory: &str) -> Self { + let data = Data::from_directory(graph_directory); Self { data } } + pub fn from_map_and_directory(graphs: HashMap, graph_directory: &str) -> Self { + let data = Data::from_map_and_directory(graphs, graph_directory); + Self { data } + } + + pub async fn with_vectorized( + self, + graph_names: Vec, + embedding: F, + cache_dir: &Path, + templates: Option<(N, E)>, + ) -> Self + where + F: Fn(Vec) -> U + Send + Sync + Copy + 'static, + U: Future> + Send + 'static, + N: Fn(&VertexView) -> String + Sync + Send + Copy + 'static, + E: Fn(&EdgeView) -> String + Sync + Send + Copy + 'static, + { + { + let graphs_map = self.data.graphs.read(); + let mut stores_map = self.data.vector_stores.write(); + + for graph_name in graph_names { + let graph_cache = cache_dir.join(&graph_name); + let graph = graphs_map.get(&graph_name).unwrap().deref().clone(); + + println!("Loading embeddings for {graph_name} using cache from {graph_cache:?}"); + let vectorized = match templates { + Some((node_template, edge_template)) => { + graph + .vectorize_with_templates( + Box::new(embedding), + &graph_cache, + node_template, + edge_template, + ) + .await + } + None => graph.vectorize(Box::new(embedding), &graph_cache).await, + }; + stores_map.insert(graph_name, vectorized); + } + } + + println!("Embeddings were loaded successfully"); + + self + } + pub fn register_algorithm(self, name: &str) -> Self { crate::model::algorithm::PLUGIN_ALGOS .lock() @@ -38,19 +95,16 @@ impl RaphtoryServer { pub async fn run_with_port(self, port: u16) -> IoResult<()> { let registry = Registry::default().with(tracing_subscriber::fmt::layer().pretty()); + let env_filter = EnvFilter::try_from_default_env().unwrap_or(EnvFilter::new("INFO")); match create_tracer_from_env() { Some(tracer) => registry .with(tracing_opentelemetry::layer().with_tracer(tracer)) - .try_init() - .expect("Failed to register tracer with registry"), - None => registry - .try_init() - .expect("Failed to register tracer with registry"), + .with(env_filter) + .try_init(), + None => registry.with(env_filter).try_init(), } - - #[derive(App)] - struct App(QueryRoot); + .unwrap_or(()); // it is important that this runs after algorithms have been pushed to PLUGIN_ALGOS static variable let schema_builder = App::create_schema(); diff --git a/raphtory-io/src/graph_loader/source/polars_loader.rs b/raphtory-io/src/graph_loader/source/polars_loader.rs deleted file mode 100644 index 1f38ce7052..0000000000 --- a/raphtory-io/src/graph_loader/source/polars_loader.rs +++ /dev/null @@ -1 +0,0 @@ -//! TBD: Provides functionality for loading graph data from Polars DataFrames. diff --git a/raphtory-io/src/lib.rs b/raphtory-io/src/lib.rs deleted file mode 100644 index 4f0a6bef78..0000000000 --- a/raphtory-io/src/lib.rs +++ /dev/null @@ -1,81 +0,0 @@ -//! # raphtory -//! -//! `raphtory-io` is a module for loading graphs into raphtory from various sources, like csv, neo4j, etc. -//! -//! ## Examples -//! -//! Load a pre-built graph -//! ```rust -//! use raphtory::algorithms::degree::average_degree; -//! use raphtory::db::graph::Graph; -//! use raphtory::db::view_api::*; -//! use raphtory_io::graph_loader::example::lotr_graph::lotr_graph; -//! -//! let graph = lotr_graph(3); -//! -//! // Get the in-degree, out-degree of Gandalf -//! // The graph.vertex option returns a result of an option, -//! // so we need to unwrap the result and the option or -//! // we can use this if let instead -//! if let Some(gandalf) = graph.vertex("Gandalf") { -//! println!("Gandalf in degree: {:?}", gandalf.in_degree()); -//! println!("Gandalf out degree: {:?}", gandalf.out_degree()); -//! } -//! -//! // Run an average degree algorithm on the graph -//! println!("Average degree: {:?}", average_degree(&graph)); -//! ``` -//! -//! Load a graph from csv -//! -//! ```no_run -//! use raphtory::db::graph::Graph; -//! use raphtory::core::Prop; -//! use std::time::Instant; -//! use raphtory_io::graph_loader::source::csv_loader::CsvLoader; -//! use serde::Deserialize; -//! -//! let data_dir = "/tmp/lotr.csv"; -//! -//! #[derive(Deserialize, std::fmt::Debug)] -//! pub struct Lotr { -//! src_id: String, -//! dst_id: String, -//! time: i64, -//! } -//! -//! let g = Graph::new(2); -//! let now = Instant::now(); -//! -//! CsvLoader::new(data_dir) -//! .load_into_graph(&g, |lotr: Lotr, g: &Graph| { -//! g.add_vertex( -//! lotr.time, -//! lotr.src_id.clone(), -//! &vec![("type".to_string(), Prop::Str("Character".to_string()))], -//! ) -//! .expect("Failed to add vertex"); -//! -//! g.add_vertex( -//! lotr.time, -//! lotr.dst_id.clone(), -//! &vec![("type".to_string(), Prop::Str("Character".to_string()))], -//! ) -//! .expect("Failed to add vertex"); -//! -//! g.add_edge( -//! lotr.time, -//! lotr.src_id.clone(), -//! lotr.dst_id.clone(), -//! &vec![( -//! "type".to_string(), -//! Prop::Str("Character Co-occurrence".to_string()), -//! )], -//! None, -//! ) -//! .expect("Failed to add edge"); -//! }) -//! .expect("Failed to load graph from CSV data files"); -//! ``` -//! -pub mod graph_loader; diff --git a/raphtory/Cargo.toml b/raphtory/Cargo.toml index 42bdc866ec..4d71034241 100644 --- a/raphtory/Cargo.toml +++ b/raphtory/Cargo.toml @@ -12,7 +12,6 @@ license.workspace = true readme.workspace = true homepage.workspace = true - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] @@ -23,7 +22,7 @@ futures = {version = "0.3", features = ["thread-pool"] } genawaiter = "0.99" itertools="0.10" num-traits = "0.2" -parking_lot = { version = "0.12" , features = ["serde", "arc_lock"] } +parking_lot = { version = "0.12" , features = ["serde", "arc_lock", "send_guard"] } quickcheck = "1" quickcheck_macros = "1" once_cell = "1" @@ -37,13 +36,55 @@ rustc-hash = "1.1.0" serde = { version = "1", features = ["derive","rc"] } sorted_vector_map = "0.1" tempdir = "0.3" -# tokio = { version = "1.27.0", features = ["full"] } thiserror = "1" twox-hash = "1.6.3" uuid = { version = "1.3.0", features = ["v4"] } +lock_api = { version = "0.4", features = ["arc_lock", "serde"] } +dashmap = {version ="5", features = ["serde"] } +serde_with = "3.3.0" +enum_dispatch = "0.3" +kdam = "0.4.1" + +# io optional dependencies +csv = {version="1.1.6", optional=true} +zip = {version ="0.6.6", optional=true} +neo4rs = {version="0.6.1", optional=true} +bzip2 = {version="0.4", optional=true} +flate2 = {version="1.0", optional=true} +serde_json = {version="1", optional=true} +reqwest = { version = "0.11.14", features = ["blocking"], optional=true} +tokio = { version = "1.27.0", features = ["full"], optional=true} + +# search optional dependencies +tantivy = {version="0.20", optional=true} # 0.21 does not work (see https://github.com/quickwit-oss/tantivy/issues/2175) + +# vectors optional dependencies +futures-util = {version="0.3.0", optional=true} +async-trait = {version="0.1.73", optional=true} + +# python binding optional dependencies +pyo3 = {version= "0.19.2", features=["multiple-pymethods", "chrono"], optional=true} +pyo3-asyncio = { version = "0.19.0", features = ["tokio-runtime"], optional=true } +num = {version="0.4.0", optional=true} +display-error-chain = {version= "0.2.0", optional=true} +arrow2 = {version="0.17", optional=true} +ordered-float = "3.7.0" + [dev-dependencies] csv = "1" pretty_assertions = "1" quickcheck = "1" quickcheck_macros = "1" +tempfile = "3.2" + +[features] +default = ["search"] +# Enables the graph loader io module +io = ["dep:zip", "dep:neo4rs", "dep:bzip2", "dep:flate2", "dep:csv", "dep:serde_json", "dep:reqwest", "dep:tokio"] +# Enables generating the pyo3 python bindings +python = ["io", "vectors", "dep:pyo3", "dep:pyo3-asyncio", "dep:num", "dep:display-error-chain", "dep:arrow2"] +# search +search = ["dep:tantivy"] +# vectors +vectors = ["dep:futures-util", "dep:async-trait"] diff --git a/raphtory/src/algorithms/algorithm_result.rs b/raphtory/src/algorithms/algorithm_result.rs new file mode 100644 index 0000000000..2f4d3577b8 --- /dev/null +++ b/raphtory/src/algorithms/algorithm_result.rs @@ -0,0 +1,508 @@ +use itertools::Itertools; +use num_traits::Float; +use ordered_float::OrderedFloat; +use std::{ + borrow::Borrow, + collections::{hash_map::Iter, HashMap}, + fmt, + fmt::Debug, + hash::Hash, + marker::PhantomData, +}; + +pub trait AsOrd { + /// Converts reference of this type into reference of an ordered Type. + /// + /// This is the same as AsRef (with the additional constraint that the target type needs to be ordered). + /// + /// Importantly, unlike AsRef, this blanket-implements the trivial conversion from a type to itself! + fn as_ord(&self) -> &T; +} + +impl AsOrd for T { + fn as_ord(&self) -> &T { + self + } +} + +impl AsOrd> for T { + fn as_ord(&self) -> &OrderedFloat { + self.into() + } +} + +impl AsOrd<(OrderedFloat, OrderedFloat)> for (T, T) { + fn as_ord(&self) -> &(OrderedFloat, OrderedFloat) { + // Safety: OrderedFloat is #[repr(transparent)] and has no invalid values, i.e. there is no physical difference between OrderedFloat and Float. + unsafe { &*(self as *const (T, T) as *const (OrderedFloat, OrderedFloat)) } + } +} + +/// A generic `AlgorithmResult` struct that represents the result of an algorithm computation. +/// +/// The `AlgorithmResult` contains a hashmap, where keys (`H`) are cloneable, hashable, and comparable, +/// and values (`Y`) are cloneable. The keys and values can be of any type that satisfies the specified +/// trait bounds. +/// +/// This `AlgorithmResult` is returned for all algorithms that return a HashMap +/// +pub struct AlgorithmResult { + /// The result hashmap that stores keys of type `H` and values of type `Y`. + pub result: HashMap, + marker: PhantomData, +} + +impl AlgorithmResult +where + K: Clone + Hash + Eq + Ord, + V: Clone, +{ + /// Creates a new instance of `AlgorithmResult` with the provided hashmap. + /// + /// # Arguments + /// + /// * `result`: A `HashMap` with keys of type `H` and values of type `Y`. + pub fn new(result: HashMap) -> Self { + Self { + result, + marker: PhantomData, + } + } + + /// Returns a reference to the entire `result` hashmap. + pub fn get_all(&self) -> &HashMap { + &self.result + } + + /// Returns the value corresponding to the provided key in the `result` hashmap. + /// + /// # Arguments + /// + /// * `key`: The key of type `H` for which the value is to be retrieved. + pub fn get(&self, key: &Q) -> Option<&V> + where + Q: Hash + Eq + ?Sized, + K: Borrow, + { + self.result.get(key) + } + + /// Sorts the `AlgorithmResult` by its keys in ascending or descending order. + /// + /// # Arguments + /// + /// * `reverse`: If `true`, sorts the result in descending order; otherwise, sorts in ascending order. + /// + /// # Returns + /// + /// A sorted vector of tuples containing keys of type `H` and values of type `Y`. + pub fn sort_by_key(&self, reverse: bool) -> Vec<(K, V)> { + let mut sorted: Vec<(K, V)> = self.result.clone().into_iter().collect(); + sorted.sort_by(|(a, _), (b, _)| if reverse { b.cmp(a) } else { a.cmp(b) }); + sorted + } + + pub fn iter(&self) -> Iter<'_, K, V> { + self.result.iter() + } + + /// Sorts the `AlgorithmResult` by its values in ascending or descending order. + /// + /// # Arguments + /// + /// * `reverse`: If `true`, sorts the result in descending order; otherwise, sorts in ascending order. + /// + /// # Returns + /// + /// A sorted vector of tuples containing keys of type `H` and values of type `Y`. + pub fn sort_by std::cmp::Ordering>( + &self, + mut cmp: F, + reverse: bool, + ) -> Vec<(K, V)> { + let mut sorted: Vec<(K, V)> = self.result.clone().into_iter().collect(); + sorted.sort_by(|(_, a), (_, b)| if reverse { cmp(b, a) } else { cmp(a, b) }); + sorted + } + + /// Retrieves the top-k elements from the `AlgorithmResult` based on its values. + /// + /// # Arguments + /// + /// * `k`: The number of elements to retrieve. + /// * `percentage`: If `true`, the `k` parameter is treated as a percentage of total elements. + /// * `reverse`: If `true`, retrieves the elements in descending order; otherwise, in ascending order. + /// + /// # Returns + /// + /// An `a vector of tuples with keys of type `H` and values of type `Y`. + /// If `percentage` is `true`, the returned vector contains the top `k` percentage of elements. + /// If `percentage` is `false`, the returned vector contains the top `k` elements. + /// Returns empty vec if the result is empty or if `k` is 0. + pub fn top_k_by std::cmp::Ordering>( + &self, + cmp: F, + k: usize, + percentage: bool, + reverse: bool, + ) -> Vec<(K, V)> { + let k = if percentage { + let total_count = self.result.len(); + (total_count as f64 * (k as f64 / 100.0)) as usize + } else { + k + }; + self.sort_by(cmp, reverse).into_iter().take(k).collect() + } + + pub fn min_by std::cmp::Ordering>(&self, mut cmp: F) -> Option<(K, V)> { + self.result + .iter() + .min_by(|a, b| cmp(a.1, b.1)) + .map(|(k, v)| (k.clone(), v.clone())) + } + + pub fn max_by std::cmp::Ordering>(&self, mut cmp: F) -> Option<(K, V)> { + self.result + .iter() + .max_by(|a, b| cmp(a.1, b.1)) + .map(|(k, v)| (k.clone(), v.clone())) + } + + pub fn median_by std::cmp::Ordering>(&self, mut cmp: F) -> Option<(K, V)> { + let mut items: Vec<_> = self.result.iter().collect(); + let len = items.len(); + if len == 0 { + return None; + } + items.sort_by(|(_, a), (_, b)| cmp(a, b)); + let median_index = len / 2; + Some((items[median_index].0.clone(), items[median_index].1.clone())) + } +} + +impl IntoIterator for AlgorithmResult +where + K: Clone + Hash + Eq + Ord, + V: Clone, + for<'a> &'a O: From<&'a V>, +{ + type Item = (K, V); + type IntoIter = std::collections::hash_map::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.result.into_iter() + } +} + +impl<'a, K, V, O> IntoIterator for &'a AlgorithmResult +where + K: Clone + Hash + Ord, + V: Clone, +{ + type Item = (&'a K, &'a V); + type IntoIter = Iter<'a, K, V>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl FromIterator<(K, V)> for AlgorithmResult { + fn from_iter>(iter: T) -> Self { + let result = iter.into_iter().collect(); + Self { + result, + marker: PhantomData, + } + } +} + +impl AlgorithmResult +where + K: Clone + Hash + Eq + Ord, + V: Clone, + O: Ord, + V: AsOrd, +{ + /// Sorts the `AlgorithmResult` by its values in ascending or descending order. + /// + /// # Arguments + /// + /// * `reverse`: If `true`, sorts the result in descending order; otherwise, sorts in ascending order. + /// + /// # Returns + /// + /// A sorted vector of tuples containing keys of type `H` and values of type `Y`. + pub fn sort_by_value(&self, reverse: bool) -> Vec<(K, V)> { + self.sort_by(|a, b| O::cmp(a.as_ord(), b.as_ord()), reverse) + } + + /// Retrieves the top-k elements from the `AlgorithmResult` based on its values. + /// + /// # Arguments + /// + /// * `k`: The number of elements to retrieve. + /// * `percentage`: If `true`, the `k` parameter is treated as a percentage of total elements. + /// * `reverse`: If `true`, retrieves the elements in descending order; otherwise, in ascending order. + /// + /// # Returns + /// + /// An `a vector of tuples with keys of type `H` and values of type `Y`. + /// If `percentage` is `true`, the returned vector contains the top `k` percentage of elements. + /// If `percentage` is `false`, the returned vector contains the top `k` elements. + /// Returns empty vec if the result is empty or if `k` is 0. + pub fn top_k(&self, k: usize, percentage: bool, reverse: bool) -> Vec<(K, V)> { + self.top_k_by( + |a, b| O::cmp(a.as_ord(), b.as_ord()), + k, + percentage, + reverse, + ) + } + + pub fn min(&self) -> Option<(K, V)> { + self.min_by(|a, b| O::cmp(a.as_ord(), b.as_ord())) + } + + pub fn max(&self) -> Option<(K, V)> { + self.max_by(|a, b| O::cmp(a.as_ord(), b.as_ord())) + } + + pub fn median(&self) -> Option<(K, V)> { + self.median_by(|a, b| O::cmp(a.as_ord(), b.as_ord())) + } +} + +impl AlgorithmResult +where + K: Clone + Hash + Eq + Ord, + V: Clone + Hash + Eq, +{ + /// Groups the `AlgorithmResult` by its values. + /// + /// # Returns + /// + /// A `HashMap` where keys are unique values from the `AlgorithmResult` and values are vectors + /// containing keys of type `H` that share the same value. + pub fn group_by(&self) -> HashMap> { + let mut grouped: HashMap> = HashMap::new(); + for (key, value) in &self.result { + grouped.entry(value.clone()).or_default().push(key.clone()); + } + grouped + } +} + +impl Debug for AlgorithmResult { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let map_string = self + .result + .iter() + .map(|(key, value)| format!("{:?}: {:?}, ", key, value)) + .join(", "); + write!(f, "{{{}}}", map_string) + } +} + +/// Add tests for all functions +#[cfg(test)] +mod algorithm_result_test { + use crate::algorithms::algorithm_result::AlgorithmResult; + use ordered_float::OrderedFloat; + use std::collections::HashMap; + + fn create_algo_result_u64() -> AlgorithmResult { + let mut map: HashMap = HashMap::new(); + map.insert("A".to_string(), 10); + map.insert("B".to_string(), 20); + map.insert("C".to_string(), 30); + AlgorithmResult::new(map) + } + + fn group_by_test() -> AlgorithmResult { + let mut map: HashMap = HashMap::new(); + map.insert("A".to_string(), 10); + map.insert("B".to_string(), 20); + map.insert("C".to_string(), 30); + map.insert("D".to_string(), 10); + AlgorithmResult::new(map) + } + + fn create_algo_result_f64() -> AlgorithmResult> { + let mut map: HashMap = HashMap::new(); + map.insert("A".to_string(), 10.0); + map.insert("B".to_string(), 20.0); + map.insert("C".to_string(), 30.0); + AlgorithmResult::new(map) + } + + fn create_algo_result_tuple( + ) -> AlgorithmResult, OrderedFloat)> { + let mut map: HashMap = HashMap::new(); + map.insert("A".to_string(), (10.0, 20.0)); + map.insert("B".to_string(), (20.0, 30.0)); + map.insert("C".to_string(), (30.0, 40.0)); + AlgorithmResult::new(map) + } + + fn create_algo_result_hashmap_vec() -> AlgorithmResult> { + let mut map: HashMap> = HashMap::new(); + map.insert("A".to_string(), vec![(11, "H".to_string())]); + map.insert("B".to_string(), vec![]); + map.insert( + "C".to_string(), + vec![(22, "E".to_string()), (33, "F".to_string())], + ); + AlgorithmResult::new(map) + } + + #[test] + fn test_min_max_value() { + let algo_result = create_algo_result_u64(); + assert_eq!(algo_result.min(), Some(("A".to_string(), 10u64))); + assert_eq!(algo_result.max(), Some(("C".to_string(), 30u64))); + assert_eq!(algo_result.median(), Some(("B".to_string(), 20u64))); + let algo_result = create_algo_result_f64(); + assert_eq!(algo_result.min(), Some(("A".to_string(), 10.0))); + assert_eq!(algo_result.max(), Some(("C".to_string(), 30.0))); + assert_eq!(algo_result.median(), Some(("B".to_string(), 20.0))); + } + + #[test] + fn test_get() { + let algo_result = create_algo_result_u64(); + assert_eq!(algo_result.get(&"C".to_string()), Some(&30)); + assert_eq!(algo_result.get(&"D".to_string()), None); + let algo_result = create_algo_result_f64(); + assert_eq!(algo_result.get(&"C".to_string()), Some(&30.0)); + let algo_result = create_algo_result_tuple(); + assert_eq!(algo_result.get(&"C".to_string()).unwrap().0, 30.0); + let algo_result = create_algo_result_hashmap_vec(); + assert_eq!(algo_result.get(&"C".to_string()).unwrap()[0].0, 22); + } + + #[test] + fn test_sort() { + let algo_result = create_algo_result_u64(); + let sorted = algo_result.sort_by_value(true); + assert_eq!(sorted[0].0, "C"); + let sorted = algo_result.sort_by_value(false); + assert_eq!(sorted[0].0, "A"); + + let algo_result = create_algo_result_f64(); + let sorted = algo_result.sort_by_value(true); + assert_eq!(sorted[0].0, "C"); + let sorted = algo_result.sort_by_value(false); + assert_eq!(sorted[0].0, "A"); + + let algo_result = create_algo_result_tuple(); + assert_eq!(algo_result.sort_by_value(true)[0].0, "C"); + + let algo_result = create_algo_result_hashmap_vec(); + assert_eq!(algo_result.sort_by_value(true)[0].0, "C"); + } + + #[test] + fn test_top_k() { + let algo_result = create_algo_result_u64(); + let top_k = algo_result.top_k(2, false, false); + assert_eq!(top_k[0].0, "A"); + let top_k = algo_result.top_k(2, false, true); + assert_eq!(top_k[0].0, "C"); + + let algo_result = create_algo_result_f64(); + let top_k = algo_result.top_k(2, false, false); + assert_eq!(top_k[0].0, "A"); + let top_k = algo_result.top_k(2, false, true); + assert_eq!(top_k[0].0, "C"); + + let algo_result = create_algo_result_tuple(); + assert_eq!(algo_result.top_k(2, false, false)[0].0, "A"); + + let algo_result = create_algo_result_hashmap_vec(); + assert_eq!(algo_result.top_k(2, false, false)[0].0, "B"); + } + + #[test] + fn test_group_by() { + let algo_result = group_by_test(); + let grouped = algo_result.group_by(); + assert_eq!(grouped.get(&10).unwrap().len(), 2); + assert!(grouped.get(&10).unwrap().contains(&"A".to_string())); + assert!(!grouped.get(&10).unwrap().contains(&"B".to_string())); + + let algo_result = create_algo_result_hashmap_vec(); + assert_eq!( + algo_result + .group_by() + .get(&vec![(11, "H".to_string())]) + .unwrap() + .len(), + 1 + ); + } + + #[test] + fn test_get_all() { + let algo_result = create_algo_result_u64(); + let all = algo_result.get_all(); + assert_eq!(all.len(), 3); + assert!(all.contains_key("A")); + + let algo_result = create_algo_result_f64(); + let all = algo_result.get_all(); + assert_eq!(all.len(), 3); + assert!(all.contains_key("A")); + + let algo_result = create_algo_result_tuple(); + assert_eq!(algo_result.get_all().get("A").unwrap().0, 10.0); + assert_eq!(algo_result.get_all().len(), 3); + + let algo_result = create_algo_result_hashmap_vec(); + assert_eq!(algo_result.get_all().get("A").unwrap()[0].0, 11); + assert_eq!(algo_result.get_all().len(), 3); + } + + #[test] + fn test_sort_by_key() { + let algo_result = create_algo_result_u64(); + let sorted = algo_result.sort_by_key(true); + let my_array: Vec<(String, u64)> = vec![ + ("C".to_string(), 30u64), + ("B".to_string(), 20u64), + ("A".to_string(), 10u64), + ]; + assert_eq!(my_array, sorted); + // + let algo_result = create_algo_result_f64(); + let sorted = algo_result.sort_by_key(true); + let my_array: Vec<(String, f64)> = vec![ + ("C".to_string(), 30.0), + ("B".to_string(), 20.0), + ("A".to_string(), 10.0), + ]; + assert_eq!(my_array, sorted); + // + let algo_result = create_algo_result_tuple(); + let sorted = algo_result.sort_by_key(true); + let my_array: Vec<(String, (f32, f32))> = vec![ + ("C".to_string(), (30.0, 40.0)), + ("B".to_string(), (20.0, 30.0)), + ("A".to_string(), (10.0, 20.0)), + ]; + assert_eq!(my_array, sorted); + // + let algo_result = create_algo_result_hashmap_vec(); + let sorted = algo_result.sort_by_key(true); + let my_array: Vec<(String, Vec<(i64, String)>)> = vec![ + ( + "C".to_string(), + vec![(22, "E".to_string()), (33, "F".to_string())], + ), + ("B".to_string(), vec![]), + ("A".to_string(), vec![(11, "H".to_string())]), + ]; + assert_eq!(my_array, sorted); + } +} diff --git a/raphtory/src/algorithms/balance.rs b/raphtory/src/algorithms/balance.rs new file mode 100644 index 0000000000..f55ad66fe4 --- /dev/null +++ b/raphtory/src/algorithms/balance.rs @@ -0,0 +1,193 @@ +//! # Weight Accumulation +//! +//! This algorithm provides functionality to accumulate (or sum) weights on vertices +//! in a graph. +use crate::{ + algorithms::algorithm_result::AlgorithmResult, + core::{ + state::{ + accumulator_id::accumulators::sum, + compute_state::{ComputeState, ComputeStateVec}, + }, + Direction, + }, + db::{ + api::view::GraphViewOps, + task::{ + context::Context, + task::{ATask, Job, Step}, + task_runner::TaskRunner, + vertex::eval_vertex::EvalVertexView, + }, + }, + prelude::{EdgeListOps, PropUnwrap, VertexViewOps}, +}; +use ordered_float::OrderedFloat; + +/// Computes the net sum of weights for a given vertex based on edge direction. +/// +/// For every edge connected to the vertex, this function checks the source of the edge +/// against the vertex itself to determine the directionality. The weight can be treated +/// as negative or positive based on the edge's source and the specified direction: +/// +/// - If the edge's source is the vertex itself and the direction is either `OUT` or `BOTH`, +/// the weight is treated as negative. +/// - If the edge's source is not the vertex and the direction is either `IN` or `BOTH`, +/// the weight is treated as positive. +/// - In all other cases, the weight contribution is zero. +/// +/// # Parameters +/// - `v`: The vertex for which we want to compute the weight sum. +/// - `name`: The name of the property which holds the edge weight. +/// - `direction`: Specifies the direction of edges to consider (`IN`, `OUT`, or `BOTH`). +/// +/// # Returns +/// Returns a `f64` which is the net sum of weights for the vertex considering the specified direction. +fn balance_per_vertex( + v: &EvalVertexView, + name: &str, + direction: Direction, +) -> f64 { + // let in_result = v.in_edges().properties().get(name.clone()).sum(); + // in_result - out_result + match direction { + Direction::IN => v + .in_edges() + .properties() + .flat_map(|prop| { + prop.temporal().get(name).map(|val| { + val.values() + .into_iter() + .map(|valval| valval.into_f64().unwrap_or(0.0f64)) + .sum::() + }) + }) + .sum::(), + Direction::OUT => -v + .out_edges() + .properties() + .flat_map(|prop| { + prop.temporal().get(name).map(|val| { + val.values() + .into_iter() + .map(|valval| valval.into_f64().unwrap_or(0.0f64)) + .sum::() + }) + }) + .sum::(), + Direction::BOTH => { + let in_res = balance_per_vertex(v, name, Direction::IN); + let out_res = balance_per_vertex(v, name, Direction::OUT); + in_res + out_res + } + } +} + +/// Computes the sum of weights for all vertices in the graph. +/// +/// This function iterates over all vertices and calculates the net sum of weights. +/// Incoming edges have a positive sum and outgoing edges have a negative sum +/// It uses a compute context and tasks to achieve this. +/// +/// # Parameters +/// - `graph`: The graph on which the operation is to be performed. +/// - `name`: The name of the property which holds the edge weight. +/// - `threads`: An optional parameter to specify the number of threads to use. +/// If `None`, it defaults to a suitable number. +/// +/// # Returns +/// Returns an `AlgorithmResult` which maps each vertex to its corresponding net weight sum. +pub fn balance( + graph: &G, + name: String, + direction: Direction, + threads: Option, +) -> AlgorithmResult> { + let mut ctx: Context = graph.into(); + let min = sum(0); + ctx.agg(min); + let step1 = ATask::new(move |evv| { + let res = balance_per_vertex(evv, &name, direction); + evv.update(&min, res); + Step::Done + }); + let mut runner: TaskRunner = TaskRunner::new(ctx); + AlgorithmResult::new(runner.run( + vec![], + vec![Job::new(step1)], + None, + |_, ess, _, _| ess.finalize(&min, |min| min), + threads, + 1, + None, + None, + )) +} + +#[cfg(test)] +mod sum_weight_test { + use crate::{ + algorithms::balance::balance, + core::{Direction, Prop}, + db::{api::mutation::AdditionOps, graph::graph::Graph}, + }; + use pretty_assertions::assert_eq; + + #[test] + fn test_sum_float_weights() { + let graph = Graph::new(); + + let vs = vec![ + ("1", "2", 10.0, 1), + ("1", "4", 20.0, 2), + ("2", "3", 5.0, 3), + ("3", "2", 2.0, 4), + ("3", "1", 1.0, 5), + ("4", "3", 10.0, 6), + ("4", "1", 5.0, 7), + ("1", "5", 2.0, 8), + ]; + + for (src, dst, val, time) in &vs { + graph + .add_edge( + *time, + *src, + *dst, + [("value_dec".to_string(), Prop::F64(*val))], + None, + ) + .expect("Couldnt add edge"); + } + + let res = balance(&graph, "value_dec".to_string(), Direction::BOTH, None); + let expected = vec![ + ("1".to_string(), -26.0), + ("2".to_string(), 7.0), + ("3".to_string(), 12.0), + ("4".to_string(), 5.0), + ("5".to_string(), 2.0), + ]; + assert_eq!(res.sort_by_key(false), expected); + + let res = balance(&graph, "value_dec".to_string(), Direction::IN, None); + let expected = vec![ + ("1".to_string(), 6.0), + ("2".to_string(), 12.0), + ("3".to_string(), 15.0), + ("4".to_string(), 20.0), + ("5".to_string(), 2.0), + ]; + assert_eq!(res.sort_by_key(false), expected); + + let res = balance(&graph, "value_dec".to_string(), Direction::OUT, None); + let expected = vec![ + ("1".to_string(), -32.0), + ("2".to_string(), -5.0), + ("3".to_string(), -3.0), + ("4".to_string(), -15.0), + ("5".to_string(), 0.0), + ]; + assert_eq!(res.sort_by_key(false), expected); + } +} diff --git a/raphtory/src/algorithms/clustering_coefficient.rs b/raphtory/src/algorithms/clustering_coefficient.rs index d48a809ac1..af14872321 100644 --- a/raphtory/src/algorithms/clustering_coefficient.rs +++ b/raphtory/src/algorithms/clustering_coefficient.rs @@ -1,6 +1,7 @@ -use crate::algorithms::triangle_count::triangle_count; -use crate::algorithms::triplet_count::triplet_count; -use crate::db::view_api::GraphViewOps; +use crate::{ + algorithms::{triangle_count::triangle_count, triplet_count::triplet_count}, + db::api::view::GraphViewOps, +}; /// Computes the global clustering coefficient of a graph. The global clustering coefficient is /// defined as the number of triangles in the graph divided by the number of triplets in the graph. @@ -16,10 +17,9 @@ use crate::db::view_api::GraphViewOps; /// # Example /// /// ```rust -/// use raphtory::db::graph::Graph; +/// use raphtory::prelude::*; /// use raphtory::algorithms::clustering_coefficient::clustering_coefficient; -/// use raphtory::db::view_api::*; -/// let graph = Graph::new(2); +/// let graph = Graph::new(); /// let edges = vec![ /// (1, 2), /// (1, 3), @@ -29,7 +29,7 @@ use crate::db::view_api::GraphViewOps; /// (2, 7), /// ]; /// for (src, dst) in edges { -/// graph.add_edge(0, src, dst, &vec![], None).expect("Unable to add edge"); +/// graph.add_edge(0, src, dst, NO_PROPS, None).expect("Unable to add edge"); /// } /// let results = clustering_coefficient(&graph.at(1)); /// println!("global_clustering_coefficient: {}", results); @@ -49,14 +49,19 @@ pub fn clustering_coefficient(g: &G) -> f64 { #[cfg(test)] mod cc_test { use super::*; - use crate::db::graph::Graph; - use crate::db::view_api::*; + use crate::{ + db::{ + api::{mutation::AdditionOps, view::*}, + graph::graph::Graph, + }, + prelude::NO_PROPS, + }; use pretty_assertions::assert_eq; /// Test the global clustering coefficient #[test] fn test_global_cc() { - let graph = Graph::new(1); + let graph = Graph::new(); // Graph has 2 triangles and 20 triplets let edges = vec![ @@ -83,7 +88,7 @@ mod cc_test { ]; for (src, dst) in edges { - graph.add_edge(0, src, dst, &vec![], None).unwrap(); + graph.add_edge(0, src, dst, NO_PROPS, None).unwrap(); } let graph_at = graph.at(1); diff --git a/raphtory/src/algorithms/connected_components.rs b/raphtory/src/algorithms/connected_components.rs index 527b1841ec..a5bb674f96 100644 --- a/raphtory/src/algorithms/connected_components.rs +++ b/raphtory/src/algorithms/connected_components.rs @@ -1,32 +1,26 @@ -use std::cmp; -use crate::db::view_api::VertexViewOps; +use super::algorithm_result::AlgorithmResult; use crate::{ - core::state::{accumulator_id::accumulators, compute_state::ComputeStateVec}, + core::{ + entities::{vertices::vertex_ref::VertexRef, VID}, + state::compute_state::ComputeStateVec, + }, db::{ + api::view::{GraphViewOps, VertexViewOps}, task::{ context::Context, task::{ATask, Job, Step}, task_runner::TaskRunner, + vertex::eval_vertex::EvalVertexView, }, - view_api::GraphViewOps, }, }; -use std::collections::HashMap; -use crate::db::task::eval_vertex::EvalVertexView; +use std::{cmp, collections::HashMap}; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] struct WccState { component: u64, } -impl WccState { - fn new() -> Self { - Self { - component: 0, - } - } -} - /// Computes the connected components of a graph using the Simple Connected Components algorithm /// /// # Arguments @@ -37,17 +31,17 @@ impl WccState { /// /// # Returns /// -/// A hash map containing the mapping from component ID to the number of vertices in the component +/// An AlgorithmResult containing the mapping from component ID to the number of vertices in the component /// pub fn weakly_connected_components( graph: &G, iter_count: usize, threads: Option, -) -> HashMap +) -> AlgorithmResult where G: GraphViewOps, { - let mut ctx: Context = graph.into(); + let ctx: Context = graph.into(); let step1 = ATask::new(move |vv| { let min_neighbour_id = vv.neighbours().id().min(); @@ -57,31 +51,42 @@ where Step::Continue }); - let step2 = ATask::new(move |vv: &mut EvalVertexView<'_, G,ComputeStateVec, WccState>| { - let prev:u64 = vv.prev().component; - let current = vv.neighbours().into_iter().map(|n|n.prev().component).min().unwrap_or(prev); - let state: &mut WccState = vv.get_mut(); - if current| { + let prev: u64 = vv.prev().component; + let current = vv + .neighbours() + .into_iter() + .map(|n| n.prev().component) + .min() + .unwrap_or(prev); + let state: &mut WccState = vv.get_mut(); + if current < prev { + state.component = current; + Step::Continue + } else { + Step::Done + } + }, + ); let mut runner: TaskRunner = TaskRunner::new(ctx); - runner.run( + let res = runner.run( vec![Job::new(step1)], vec![Job::read_only(step2)], - WccState::new(), - |g, _, _, local| { + None, + |_, _, _, local| { + let layers = graph.layer_ids(); + let edge_filter = graph.edge_filter(); local .iter() - .filter_map(|line| { - line.as_ref() - .map(|(v_ref, state)| (v_ref.clone(), state.component)) + .enumerate() + .filter_map(|(v_ref, state)| { + let v_ref = VID(v_ref); + graph + .has_vertex_ref(VertexRef::Internal(v_ref), &layers, edge_filter) + .then_some((graph.vertex_name(v_ref), state.component)) }) .collect::>() }, @@ -89,22 +94,22 @@ where iter_count, None, None, - ).into_iter() - .map(|(k, v)| (graph.vertex_name(k), v)) - .collect() + ); + AlgorithmResult::new(res) } #[cfg(test)] mod cc_test { - use crate::db::graph::Graph; + use crate::prelude::*; use super::*; + use crate::db::api::mutation::AdditionOps; use itertools::*; use std::{cmp::Reverse, iter::once}; #[test] fn run_loop_simple_connected_components() { - let graph = Graph::new(2); + let graph = Graph::new(); let edges = vec![ (1, 2, 1), @@ -117,12 +122,12 @@ mod cc_test { ]; for (src, dst, ts) in edges { - graph.add_edge(ts, src, dst, &vec![], None).unwrap(); + graph.add_edge(ts, src, dst, NO_PROPS, None).unwrap(); } - let results: HashMap = weakly_connected_components(&graph, usize::MAX, None); - + let results: AlgorithmResult = + weakly_connected_components(&graph, usize::MAX, None); assert_eq!( - results, + *results.get_all(), vec![ ("1".to_string(), 1), ("2".to_string(), 1), @@ -140,7 +145,7 @@ mod cc_test { #[test] fn simple_connected_components_2() { - let graph = Graph::new(2); + let graph = Graph::new(); let edges = vec![ (1, 2, 1), @@ -169,13 +174,14 @@ mod cc_test { ]; for (src, dst, ts) in edges { - graph.add_edge(ts, src, dst, &vec![], None).unwrap(); + graph.add_edge(ts, src, dst, NO_PROPS, None).unwrap(); } - let results: HashMap = weakly_connected_components(&graph, usize::MAX, None); + let results: AlgorithmResult = + weakly_connected_components(&graph, usize::MAX, None); assert_eq!( - results, + *results.get_all(), vec![ ("1".to_string(), 1), ("2".to_string(), 1), @@ -197,24 +203,58 @@ mod cc_test { // connected components on a graph with 1 node and a self loop #[test] fn simple_connected_components_3() { - let graph = Graph::new(2); + let graph = Graph::new(); let edges = vec![(1, 1, 1)]; for (src, dst, ts) in edges { - graph.add_edge(ts, src, dst, &vec![], None).unwrap(); + graph.add_edge(ts, src, dst, NO_PROPS, None).unwrap(); } - let results: HashMap = weakly_connected_components(&graph, usize::MAX, None); + let results: AlgorithmResult = + weakly_connected_components(&graph, usize::MAX, None); assert_eq!( - results, - vec![("1".to_string(), 1),] + *results.get_all(), + vec![("1".to_string(), 1)] .into_iter() .collect::>() ); } + #[test] + fn windowed_connected_components() { + let graph = Graph::new(); + graph.add_edge(0, 1, 2, NO_PROPS, None).expect("add edge"); + graph.add_edge(0, 2, 1, NO_PROPS, None).expect("add edge"); + graph.add_edge(9, 3, 4, NO_PROPS, None).expect("add edge"); + graph.add_edge(9, 4, 3, NO_PROPS, None).expect("add edge"); + + let results: AlgorithmResult = + weakly_connected_components(&graph, usize::MAX, None); + let expected = vec![ + ("1".to_string(), 1), + ("2".to_string(), 1), + ("3".to_string(), 3), + ("4".to_string(), 3), + ] + .into_iter() + .collect::>(); + + assert_eq!(*results.get_all(), expected); + + let wg = graph.window(0, 2); + let results: AlgorithmResult = + weakly_connected_components(&wg, usize::MAX, None); + + let expected = vec![("1", 1), ("2", 1)] + .into_iter() + .map(|(k, v)| (k.to_string(), v)) + .collect::>(); + + assert_eq!(*results.get_all(), expected); + } + #[quickcheck] fn circle_graph_the_smallest_value_is_the_cc(vs: Vec) { if !vs.is_empty() { @@ -233,18 +273,19 @@ mod cc_test { assert_eq!(edges[0].0, first); assert_eq!(edges.last().unwrap().1, first); - let graph = Graph::new(2); + let graph = Graph::new(); for (src, dst) in edges.iter() { - graph.add_edge(0, *src, *dst, &vec![], None).unwrap(); + graph.add_edge(0, *src, *dst, NO_PROPS, None).unwrap(); } // now we do connected components over window 0..1 - let components: HashMap = + let res: AlgorithmResult = weakly_connected_components(&graph, usize::MAX, None); - let actual = components + let actual = res + .get_all() .iter() .group_by(|(_, cc)| *cc) .into_iter() diff --git a/raphtory/src/algorithms/degree.rs b/raphtory/src/algorithms/degree.rs index 5b90aed4a5..ce0192cda1 100644 --- a/raphtory/src/algorithms/degree.rs +++ b/raphtory/src/algorithms/degree.rs @@ -17,10 +17,9 @@ //! //! ```rust //! use raphtory::algorithms::degree::{max_out_degree, max_in_degree, min_out_degree, min_in_degree, average_degree}; -//! use raphtory::db::graph::Graph; -//! use raphtory::db::view_api::*; +//! use raphtory::prelude::*; //! -//! let g = Graph::new(1); +//! let g = Graph::new(); //! let windowed_graph = g.window(0, 7); //! let vs = vec![ //! (1, 1, 2), @@ -32,7 +31,7 @@ //! ]; //! //! for (t, src, dst) in &vs { -//! g.add_edge(*t, *src, *dst, &vec![], None); +//! g.add_edge(*t, *src, *dst, NO_PROPS, None); //! } //! //! print!("Max out degree: {:?}", max_out_degree(&windowed_graph)); @@ -42,7 +41,7 @@ //! print!("Average degree: {:?}", average_degree(&windowed_graph)); //! ``` //! -use crate::db::view_api::*; +use crate::db::api::view::*; /// The maximum out degree of any vertex in the graph. pub fn max_out_degree(graph: &G) -> usize { @@ -104,14 +103,15 @@ pub fn average_degree(graph: &G) -> f64 { mod degree_test { use crate::{ algorithms::degree::{average_degree, max_in_degree, min_in_degree, min_out_degree}, - db::graph::Graph, + db::{api::mutation::AdditionOps, graph::graph::Graph}, + prelude::NO_PROPS, }; use super::max_out_degree; #[test] fn degree_test() { - let g = Graph::new(1); + let g = Graph::new(); let vs = vec![ (1, 1, 2), (2, 1, 3), @@ -122,7 +122,7 @@ mod degree_test { ]; for (t, src, dst) in &vs { - g.add_edge(*t, *src, *dst, &vec![], None).unwrap(); + g.add_edge(*t, *src, *dst, NO_PROPS, None).unwrap(); } let expected_max_out_degree = 3; diff --git a/raphtory/src/algorithms/directed_graph_density.rs b/raphtory/src/algorithms/directed_graph_density.rs index b292c6af5f..888336a856 100644 --- a/raphtory/src/algorithms/directed_graph_density.rs +++ b/raphtory/src/algorithms/directed_graph_density.rs @@ -11,10 +11,9 @@ //! //! ```rust //! use raphtory::algorithms::directed_graph_density::directed_graph_density; -//! use raphtory::db::graph::Graph; -//! use raphtory::db::view_api::*; +//! use raphtory::prelude::*; //! -//! let g = Graph::new(1); +//! let g = Graph::new(); //! let windowed_graph = g.window(0, 7); //! let vs = vec![ //! (1, 1, 2), @@ -26,27 +25,31 @@ //! ]; //! //! for (t, src, dst) in &vs { -//! g.add_edge(*t, *src, *dst, &vec![], None); +//! g.add_edge(*t, *src, *dst, NO_PROPS, None); //! } //! //! println!("graph density: {:?}", directed_graph_density(&windowed_graph)); //! ``` //! -use crate::db::view_api::*; +use crate::db::api::view::*; /// Measures how dense or sparse a graph is pub fn directed_graph_density(graph: &G) -> f32 { - graph.num_edges() as f32 / (graph.num_vertices() as f32 * (graph.num_vertices() as f32 - 1.0)) + graph.count_edges() as f32 + / (graph.count_vertices() as f32 * (graph.count_vertices() as f32 - 1.0)) } #[cfg(test)] mod directed_graph_density_tests { use super::*; - use crate::db::graph::Graph; + use crate::{ + db::{api::mutation::AdditionOps, graph::graph::Graph}, + prelude::NO_PROPS, + }; #[test] fn low_graph_density() { - let g = Graph::new(1); + let g = Graph::new(); let windowed_graph = g.window(0, 7); let vs = vec![ (1, 1, 2), @@ -58,7 +61,7 @@ mod directed_graph_density_tests { ]; for (t, src, dst) in &vs { - g.add_edge(*t, *src, *dst, &vec![], None).unwrap(); + g.add_edge(*t, *src, *dst, NO_PROPS, None).unwrap(); } let actual = directed_graph_density(&windowed_graph); @@ -69,12 +72,12 @@ mod directed_graph_density_tests { #[test] fn complete_graph_has_graph_density_of_one() { - let g = Graph::new(1); + let g = Graph::new(); let windowed_graph = g.window(0, 3); let vs = vec![(1, 1, 2), (2, 2, 1)]; for (t, src, dst) in &vs { - g.add_edge(*t, *src, *dst, &vec![], None).unwrap(); + g.add_edge(*t, *src, *dst, NO_PROPS, None).unwrap(); } let actual = directed_graph_density(&windowed_graph); diff --git a/raphtory/src/algorithms/hits.rs b/raphtory/src/algorithms/hits.rs index a1d6cb6f98..81cd1c7952 100644 --- a/raphtory/src/algorithms/hits.rs +++ b/raphtory/src/algorithms/hits.rs @@ -1,20 +1,25 @@ -use crate::core::state::accumulator_id::accumulators::{max, sum}; -use crate::db::task::eval_vertex::EvalVertexView; use crate::{ - core::state::compute_state::ComputeStateVec, + algorithms::algorithm_result::AlgorithmResult, + core::{ + entities::vertices::vertex_ref::VertexRef, + state::{ + accumulator_id::accumulators::{max, sum}, + compute_state::ComputeStateVec, + }, + }, db::{ + api::view::{GraphViewOps, VertexViewOps}, task::{ context::Context, task::{ATask, Job, Step}, task_runner::TaskRunner, + vertex::eval_vertex::EvalVertexView, }, - view_api::{GraphViewOps, VertexViewOps}, }, }; use num_traits::abs; -use rustc_hash::FxHashMap; +use ordered_float::OrderedFloat; use std::collections::HashMap; -use std::ops::Range; #[derive(Debug, Clone)] struct Hits { @@ -22,20 +27,31 @@ struct Hits { auth_score: f32, } -// HITS (Hubs and Authority) Algorithm: -// AuthScore of a vertex (A) = Sum of HubScore of all vertices pointing at vertex (A) from previous iteration / -// Sum of HubScore of all vertices in the current iteration -// -// HubScore of a vertex (A) = Sum of AuthScore of all vertices pointing away from vertex (A) from previous iteration / -// Sum of AuthScore of all vertices in the current iteration +impl Default for Hits { + fn default() -> Self { + Self { + hub_score: 1f32, + auth_score: 1f32, + } + } +} +/// HITS (Hubs and Authority) Algorithm: +/// AuthScore of a vertex (A) = Sum of HubScore of all vertices pointing at vertex (A) from previous iteration / +/// Sum of HubScore of all vertices in the current iteration +/// +/// HubScore of a vertex (A) = Sum of AuthScore of all vertices pointing away from vertex (A) from previous iteration / +/// Sum of AuthScore of all vertices in the current iteration +/// +/// Returns +/// +/// * An AlgorithmResult object containing the mapping from vertex ID to the hub and authority score of the vertex #[allow(unused_variables)] pub fn hits( g: &G, - window: Range, iter_count: usize, threads: Option, -) -> FxHashMap { +) -> AlgorithmResult, OrderedFloat)> { let mut ctx: Context = g.into(); let recv_hub_score = sum::(2); @@ -96,8 +112,8 @@ pub fn hits( let md_hub_score = abs(prev_hub_score - curr_hub_score); evv.global_update(&max_diff_hub_score, md_hub_score); - let prev_auth_score = evv.prev().auth_score; - let curr_auth_score = evv.get().auth_score; + let prev_auth_score = evv.prev().auth_score; + let curr_auth_score = evv.get().auth_score; let md_auth_score = abs(prev_auth_score - curr_auth_score); evv.global_update(&max_diff_auth_score, md_auth_score); @@ -122,16 +138,15 @@ pub fn hits( let (hub_scores, auth_scores) = runner.run( vec![], vec![Job::new(step2), Job::new(step3), Job::new(step4), step5], - Hits { - hub_score: 1f32, - auth_score: 1f32, - }, + None, |_, _, els, local| { let mut hubs = HashMap::new(); let mut auths = HashMap::new(); - for line in local.iter() { - if let Some((v_ref, hit)) = line { - let v_gid = g.vertex_name(v_ref.clone()); + let layers = g.layer_ids(); + let edge_filter = g.edge_filter(); + for (v_ref, hit) in local.iter().enumerate() { + if g.has_vertex_ref(VertexRef::Internal(v_ref.into()), &layers, edge_filter) { + let v_gid = g.vertex_name(v_ref.into()); hubs.insert(v_gid.clone(), hit.hub_score); auths.insert(v_gid, hit.auth_score); } @@ -144,7 +159,7 @@ pub fn hits( None, ); - let mut results: FxHashMap = FxHashMap::default(); + let mut results: HashMap = HashMap::new(); hub_scores.into_iter().for_each(|(k, v)| { results.insert(k, (v, 0.0)); @@ -155,52 +170,47 @@ pub fn hits( results.insert(k, (*a, v)); }); - results + AlgorithmResult::new(results) } #[cfg(test)] mod hits_tests { use super::*; - use crate::db::graph::Graph; - use itertools::Itertools; + use crate::{ + db::{api::mutation::AdditionOps, graph::graph::Graph}, + prelude::NO_PROPS, + }; - fn load_graph(n_shards: usize, edges: Vec<(u64, u64)>) -> Graph { - let graph = Graph::new(n_shards); + fn load_graph(edges: Vec<(u64, u64)>) -> Graph { + let graph = Graph::new(); for (src, dst) in edges { - graph.add_edge(0, src, dst, &vec![], None).unwrap(); + graph.add_edge(0, src, dst, NO_PROPS, None).unwrap(); } graph } - fn test_hits(n_shards: usize) { - let graph = load_graph( - n_shards, - vec![ - (1, 4), - (2, 3), - (2, 5), - (3, 1), - (4, 2), - (4, 3), - (5, 2), - (5, 3), - (5, 4), - (5, 6), - (6, 3), - (6, 8), - (7, 1), - (7, 3), - (8, 1), - ], - ); - - let window = 0..10; - - let mut results: Vec<(String, (f32, f32))> = - hits(&graph, window, 20, None).into_iter().collect_vec(); - - results.sort_by_key(|k| (*k).0.clone()); + #[test] + fn test_hits() { + let graph = load_graph(vec![ + (1, 4), + (2, 3), + (2, 5), + (3, 1), + (4, 2), + (4, 3), + (5, 2), + (5, 3), + (5, 4), + (5, 6), + (6, 3), + (6, 8), + (7, 1), + (7, 3), + (8, 1), + ]); + + let results = hits(&graph, 20, None); // NetworkX results // >>> G = nx.DiGraph() @@ -232,7 +242,7 @@ mod hits_tests { // ) assert_eq!( - results, + results.sort_by_key(false), vec![ ("1".to_string(), (0.0431365, 0.096625775)), ("2".to_string(), (0.14359662, 0.18366566)), @@ -245,9 +255,4 @@ mod hits_tests { ] ); } - - #[test] - fn test_hits_11() { - test_hits(1); - } } diff --git a/raphtory/src/algorithms/k_core.rs b/raphtory/src/algorithms/k_core.rs new file mode 100644 index 0000000000..b8f3e0e2c4 --- /dev/null +++ b/raphtory/src/algorithms/k_core.rs @@ -0,0 +1,174 @@ +use crate::{ + core::{ + entities::{vertices::vertex_ref::VertexRef, VID}, + state::compute_state::ComputeStateVec, + }, + db::{ + api::view::{GraphViewOps, VertexViewOps}, + graph::views::vertex_subgraph::VertexSubgraph, + task::{ + context::Context, + task::{ATask, Job, Step}, + task_runner::TaskRunner, + vertex::eval_vertex::EvalVertexView, + }, + }, +}; +use std::collections::HashSet; + +#[derive(Clone, Debug)] +struct KCoreState { + alive: bool, +} + +impl Default for KCoreState { + fn default() -> Self { + Self { alive: true } + } +} + +/// Determines which nodes are in the k-core for a given value of k +/// +/// # Arguments +/// +/// * `g` - A reference to the graph +/// * `k` - Value of k such that the returned vertices have degree > k (recursively) +/// * `iter_count` - The number of iterations to run +/// * `threads` - number of threads to run on +/// +/// # Returns +/// +/// A hash set of vertices in the k core +/// +pub fn k_core_set(graph: &G, k: usize, iter_count: usize, threads: Option) -> HashSet +where + G: GraphViewOps, +{ + let ctx: Context = graph.into(); + + let step1 = ATask::new(move |vv| { + let deg = vv.degree(); + let state: &mut KCoreState = vv.get_mut(); + state.alive = deg >= k; + Step::Continue + }); + + let step2 = ATask::new( + move |vv: &mut EvalVertexView<'_, G, ComputeStateVec, KCoreState>| { + let prev: bool = vv.prev().alive; + if prev == true { + let current = vv + .neighbours() + .into_iter() + .filter(|n| n.prev().alive) + .count() + >= k; + let state: &mut KCoreState = vv.get_mut(); + if current != prev { + state.alive = current; + Step::Continue + } else { + Step::Done + } + } else { + Step::Done + } + }, + ); + + let mut runner: TaskRunner = TaskRunner::new(ctx); + + runner.run( + vec![Job::new(step1)], + vec![Job::read_only(step2)], + None, + |_, _, _, local| { + let layers = graph.layer_ids(); + let edge_filter = graph.edge_filter(); + local + .iter() + .enumerate() + .filter(|(v_ref, state)| { + state.alive + && graph.has_vertex_ref( + VertexRef::Internal((*v_ref).into()), + &layers, + edge_filter, + ) + }) + .map(|(v_ref, _)| v_ref.into()) + .collect::>() + }, + threads, + iter_count, + None, + None, + ) +} + +pub fn k_core( + graph: &G, + k: usize, + iter_count: usize, + threads: Option, +) -> VertexSubgraph +where + G: GraphViewOps, +{ + let v_set = k_core_set(graph, k, iter_count, threads); + graph.subgraph(v_set) +} + +#[cfg(test)] +mod k_core_test { + use std::collections::HashSet; + + use crate::{algorithms::k_core::k_core_set, prelude::*}; + + #[test] + fn k_core_2() { + let graph = Graph::new(); + + let edges = vec![ + (1, 2, 1), + (1, 3, 2), + (1, 4, 3), + (3, 1, 4), + (3, 4, 5), + (3, 5, 6), + (4, 5, 7), + (5, 6, 8), + (5, 8, 9), + (7, 5, 10), + (8, 5, 11), + (1, 9, 12), + (9, 1, 13), + (6, 3, 14), + (4, 8, 15), + (8, 3, 16), + (5, 10, 17), + (10, 5, 18), + (10, 8, 19), + (1, 11, 20), + (11, 1, 21), + (9, 11, 22), + (11, 9, 23), + ]; + + for (src, dst, ts) in edges { + graph.add_edge(ts, src, dst, NO_PROPS, None).unwrap(); + } + + let result = k_core_set(&graph, 2, usize::MAX, None); + let subgraph = graph.subgraph(result.clone()); + let actual = vec!["1", "3", "4", "5", "6", "8", "9", "10", "11"] + .into_iter() + .map(|k| k.to_string()) + .collect::>(); + + assert_eq!( + actual, + subgraph.vertices().name().collect::>() + ); + } +} diff --git a/raphtory/src/algorithms/local_clustering_coefficient.rs b/raphtory/src/algorithms/local_clustering_coefficient.rs index 79b268d123..becc916fac 100644 --- a/raphtory/src/algorithms/local_clustering_coefficient.rs +++ b/raphtory/src/algorithms/local_clustering_coefficient.rs @@ -24,10 +24,9 @@ //! //! ```rust //! use raphtory::algorithms::local_clustering_coefficient::{local_clustering_coefficient}; -//! use raphtory::db::graph::Graph; -//! use raphtory::db::view_api::*; +//! use raphtory::prelude::*; //! -//! let g = Graph::new(1); +//! let g = Graph::new(); //! let windowed_graph = g.window(0, 7); //! let vs = vec![ //! (1, 1, 2), @@ -39,7 +38,7 @@ //! ]; //! //! for (t, src, dst) in &vs { -//! g.add_edge(*t, *src, *dst, &vec![], None); +//! g.add_edge(*t, *src, *dst, NO_PROPS, None); //! } //! //! let actual = (1..=5) @@ -49,9 +48,10 @@ //! println!("local clustering coefficient of all nodes: {:?}", actual); //! ``` -use crate::algorithms::local_triangle_count::local_triangle_count; -use crate::core::vertex_ref::VertexRef; -use crate::db::view_api::*; +use crate::{ + algorithms::local_triangle_count::local_triangle_count, + core::entities::vertices::vertex_ref::VertexRef, db::api::view::*, +}; /// measures the degree to which nodes in a graph tend to cluster together pub fn local_clustering_coefficient>( @@ -79,12 +79,17 @@ pub fn local_clustering_coefficient>( #[cfg(test)] mod clustering_coefficient_tests { use super::local_clustering_coefficient; - use crate::db::graph::Graph; - use crate::db::view_api::*; + use crate::{ + db::{ + api::{mutation::AdditionOps, view::*}, + graph::graph::Graph, + }, + prelude::NO_PROPS, + }; #[test] fn clusters_of_triangles() { - let g = Graph::new(1); + let g = Graph::new(); let windowed_graph = g.window(0, 7); let vs = vec![ (1, 1, 2), @@ -96,7 +101,7 @@ mod clustering_coefficient_tests { ]; for (t, src, dst) in &vs { - g.add_edge(*t, *src, *dst, &vec![], None).unwrap(); + g.add_edge(*t, *src, *dst, NO_PROPS, None).unwrap(); } let expected = vec![0.33333334, 1.0, 1.0, 0.0, 0.0]; diff --git a/raphtory/src/algorithms/local_triangle_count.rs b/raphtory/src/algorithms/local_triangle_count.rs index 8286e1ab0a..1b40dff856 100644 --- a/raphtory/src/algorithms/local_triangle_count.rs +++ b/raphtory/src/algorithms/local_triangle_count.rs @@ -18,14 +18,13 @@ //! //! ```rust //! use raphtory::algorithms::local_triangle_count::{local_triangle_count}; -//! use raphtory::db::graph::Graph; -//! use raphtory::db::view_api::*; +//! use raphtory::prelude::*; //! -//! let g = Graph::new(1); +//! let g = Graph::new(); //! let vs = vec![(1, 1, 2), (2, 1, 3), (3, 2, 1), (4, 3, 2)]; //! //! for (t, src, dst) in &vs { -//! g.add_edge(*t, *src, *dst, &vec![], None); +//! g.add_edge(*t, *src, *dst, NO_PROPS, None); //! } //! //! let windowed_graph = g.window(0, 5); @@ -38,28 +37,27 @@ //! println!("local_triangle_count: {:?}", result); //! ``` //! -use crate::core::vertex_ref::VertexRef; -use crate::db::view_api::*; +use crate::{core::entities::vertices::vertex_ref::VertexRef, db::api::view::*}; use itertools::Itertools; /// calculates the number of triangles (a cycle of length 3) for a node. pub fn local_triangle_count>(graph: &G, v: V) -> Option { if let Some(vertex) = graph.vertex(v) { if vertex.degree() >= 2 { - let x: Vec = vertex + let len = vertex .neighbours() .id() .into_iter() .combinations(2) - .filter_map(|nb| match graph.has_edge(nb[0], nb[1], None) { + .filter_map(|nb| match graph.has_edge(nb[0], nb[1], Layer::All) { true => Some(1), - false => match graph.has_edge(nb[1], nb[0], None) { + false => match graph.has_edge(nb[1], nb[0], Layer::All) { true => Some(1), false => None, }, }) - .collect(); - Some(x.len()) + .count(); + Some(len) } else { Some(0) } @@ -72,16 +70,21 @@ pub fn local_triangle_count>(graph: &G, v: V mod triangle_count_tests { use super::local_triangle_count; - use crate::db::graph::Graph; - use crate::db::view_api::*; + use crate::{ + db::{ + api::{mutation::AdditionOps, view::*}, + graph::graph::Graph, + }, + prelude::NO_PROPS, + }; #[test] fn counts_triangles() { - let g = Graph::new(1); + let g = Graph::new(); let vs = vec![(1, 1, 2), (2, 1, 3), (3, 2, 1), (4, 3, 2)]; for (t, src, dst) in &vs { - g.add_edge(*t, *src, *dst, &vec![], None).unwrap(); + g.add_edge(*t, *src, *dst, NO_PROPS, None).unwrap(); } let windowed_graph = g.window(0, 5); diff --git a/raphtory/src/algorithms/mod.rs b/raphtory/src/algorithms/mod.rs index ff7ca93079..4b54f5dc4a 100644 --- a/raphtory/src/algorithms/mod.rs +++ b/raphtory/src/algorithms/mod.rs @@ -8,9 +8,9 @@ //! //! ```rust //! use raphtory::algorithms::degree::{average_degree}; -//! use raphtory::db::graph::Graph; +//! use raphtory::prelude::*; //! -//! let g = Graph::new(1); +//! let g = Graph::new(); //! let vs = vec![ //! (1, 1, 2), //! (2, 1, 3), @@ -21,21 +21,24 @@ //! ]; //! //! for (t, src, dst) in &vs { -//! g.add_edge(*t, *src, *dst, &vec![], None); +//! g.add_edge(*t, *src, *dst, NO_PROPS, None); //! }; //! println!("average_degree: {:?}", average_degree(&g)); //! ``` +pub mod algorithm_result; +pub mod balance; pub mod clustering_coefficient; pub mod connected_components; pub mod degree; pub mod directed_graph_density; -pub mod generic_taint; pub mod hits; +pub mod k_core; pub mod local_clustering_coefficient; pub mod local_triangle_count; pub mod motifs; pub mod pagerank; pub mod reciprocity; +pub mod temporal_reachability; pub mod triangle_count; pub mod triplet_count; diff --git a/raphtory/src/algorithms/motifs/mod.rs b/raphtory/src/algorithms/motifs/mod.rs index 7180cd10ef..73bd75ec28 100644 --- a/raphtory/src/algorithms/motifs/mod.rs +++ b/raphtory/src/algorithms/motifs/mod.rs @@ -1,2 +1,3 @@ -pub mod three_node_local; +pub mod three_node_local_single_thread; pub mod three_node_motifs; +pub mod three_node_temporal_motifs; diff --git a/raphtory/src/algorithms/motifs/three_node_local.rs b/raphtory/src/algorithms/motifs/three_node_local.rs deleted file mode 100644 index 3ef5aa4868..0000000000 --- a/raphtory/src/algorithms/motifs/three_node_local.rs +++ /dev/null @@ -1,523 +0,0 @@ -use std::collections::HashMap; -use std::slice::Iter; - -use crate::core::agg::ValDef; -use crate::core::state::accumulator_id::AccId; -use crate::db::view_api::*; - -use crate::algorithms::motifs::three_node_motifs::*; -use crate::core::state::accumulator_id::accumulators::val; -use crate::core::state::compute_state::ComputeStateVec; -use crate::db::task::context::Context; -use crate::db::task::eval_vertex::EvalVertexView; -use crate::db::task::task::{ATask, Job, Step}; -use crate::db::task::task_runner::TaskRunner; -use crate::db::view_api::{GraphViewOps, VertexViewOps}; -use num_traits::Zero; -use std::ops::Add; - -pub fn star_motif_count( - evv: &EvalVertexView, - delta: i64, -) -> [usize; 24] { - let neigh_map: HashMap = evv - .neighbours() - .into_iter() - .enumerate() - .map(|(num, nb)| (nb.id(), num)) - .into_iter() - .collect(); - let mut exploded_edges = evv - .edges() - .explode() - .map(|edge| { - if edge.src().id() == evv.id() { - star_event(neigh_map[&edge.dst().id()], 1, edge.time().unwrap()) - } else { - star_event(neigh_map[&edge.src().id()], 0, edge.time().unwrap()) - } - }) - .collect::>(); - exploded_edges.sort_by_key(|e| e.time); - let mut star_count = init_star_count(neigh_map.len()); - star_count.execute(&exploded_edges, delta); - star_count.return_counts() -} - -pub fn twonode_motif_count( - graph: &G, - evv: &EvalVertexView, - delta: i64, -) -> [usize; 8] { - let mut counts = [0; 8]; - for nb in evv.neighbours().into_iter() { - let nb_id = nb.id(); - let out = graph.edge(evv.id(), nb_id, None); - let inc = graph.edge(nb_id, evv.id(), None); - let mut all_exploded = match (out, inc) { - (Some(o), Some(i)) => o - .explode() - .chain(i.explode()) - .map(|e| { - two_node_event( - if e.src().id() == evv.id() { 1 } else { 0 }, - e.time().unwrap(), - ) - }) - .collect::>(), - (Some(o), None) => o - .explode() - .map(|e| two_node_event(1, e.time().unwrap())) - .collect::>(), - (None, Some(i)) => i - .explode() - .map(|e| two_node_event(0, e.time().unwrap())) - .collect::>(), - (None, None) => Vec::new(), - }; - all_exploded.sort_by_key(|e| e.time); - let mut two_node_counter = init_two_node_count(); - two_node_counter.execute(&all_exploded, delta); - let two_node_result = two_node_counter.return_counts(); - for i in 0..8 { - counts[i] += two_node_result[i]; - } - } - counts -} - -pub fn triangle_motif_count( - graph: &G, - evv: &EvalVertexView, - delta: i64, - motif_counter: AccId>, -) { - let u: u64 = evv.id(); - for v in evv.neighbours().into_iter().filter(|x| x.id() > u) { - let mut nb_ct = 0; - for nb in evv.neighbours().into_iter().filter(|x| x.id() > v.id()) { - let u_to_v = match graph.edge(u, v.id(), None) { - Some(edge) => { - let r = edge - .explode() - .map(|e| new_triangle_edge(true, 1, 0, 1, e.time().unwrap())) - .collect::>(); - r.into_iter() - } - None => vec![].into_iter(), - }; - let v_to_u = match graph.edge(v.id(), u, None) { - Some(edge) => { - let r = edge - .explode() - .map(|e| new_triangle_edge(true, 0, 0, 0, e.time().unwrap())) - .collect::>(); - r.into_iter() - } - None => vec![].into_iter(), - }; - let mut tri_edges: Vec = Vec::new(); - let out = graph.edge(v.id(), nb.id(), None); - let inc = graph.edge(nb.id(), v.id(), None); - // The following code checks for triangles - match (out, inc) { - (Some(o), Some(i)) => { - tri_edges.append( - &mut o - .explode() - .map(|e| new_triangle_edge(false, 1, nb_ct, 1, e.time().unwrap())) - .collect::>(), - ); - tri_edges.append( - &mut i - .explode() - .map(|e| new_triangle_edge(false, 1, nb_ct, 0, e.time().unwrap())) - .collect::>(), - ); - } - (Some(o), None) => { - tri_edges.append( - &mut o - .explode() - .map(|e| new_triangle_edge(false, 1, nb_ct, 1, e.time().unwrap())) - .collect::>(), - ); - } - (None, Some(i)) => { - tri_edges.append( - &mut i - .explode() - .map(|e| new_triangle_edge(false, 1, nb_ct, 0, e.time().unwrap())) - .collect::>(), - ); - } - (None, None) => { - continue; - } - } - if !tri_edges.is_empty() { - let uout = graph.edge(u, nb.id(), None); - let uin = graph.edge(nb.id(), u, None); - match (uout, uin) { - (Some(o), Some(i)) => { - tri_edges.append( - &mut o - .explode() - .map(|e| new_triangle_edge(false, 0, nb_ct, 1, e.time().unwrap())) - .collect::>(), - ); - tri_edges.append( - &mut i - .explode() - .map(|e| new_triangle_edge(false, 0, nb_ct, 0, e.time().unwrap())) - .collect::>(), - ); - } - (Some(o), None) => { - tri_edges.append( - &mut o - .explode() - .map(|e| new_triangle_edge(false, 0, nb_ct, 1, e.time().unwrap())) - .collect::>(), - ); - } - (None, Some(i)) => { - tri_edges.append( - &mut i - .explode() - .map(|e| new_triangle_edge(false, 0, nb_ct, 0, e.time().unwrap())) - .collect::>(), - ); - } - (None, None) => { - continue; - } - } - nb_ct += 1; - // found triangle at this point!! - tri_edges.append(&mut u_to_v.collect::>()); - tri_edges.append(&mut v_to_u.collect::>()); - tri_edges.sort_by_key(|e| e.time); - - let mut tri_count = init_tri_count(nb_ct); - tri_count.execute(&tri_edges, delta); - let tmp_counts: Iter = tri_count.return_counts().iter(); - - update_counter(vec![evv, &v, &nb], motif_counter, tmp_counts); - } - } - } -} - -// works fine for 1 shard but breaks on more shard -// v1 - shard1, v2,v3 - shard2 -// distributed acc -// v1 -> v2 (sending new count v2/) -// v2 -> v1 (sending new count v1) 5, 6 (motif 3 c) - -// per vertex (motif counts) -// every iteration (sum them up) -// A -> B -> C-> A (motif 3) - -// A: [1, 2, 3(1), 4, 5, 6, 7, 8], B: [1, 2, 3(1), 4, 5, 6, 7, 8], C: [1, 2, 3(1), 4, 5, 6, 7, 8] - shard1 -// A: [1, 2, 3(0), 4, 5, 6, 7, 8], B: [1, 2, 3(0), 4, 5, 6, 7, 8], C: [1, 2, 3(0), 4, 5, 6, 7, 8] - shard2 -// A: [1, 2, 3(0), 4, 5, 6, 7, 8], B: [1, 2, 3(0), 4, 5, 6, 7, 8], C: [1, 2, 3(0), 4, 5, 6, 7, 8] - shard3 - -// global acc -// 1, 2, 3, 4, 5, 6, 7, 8 - -fn update_counter( - vs: Vec<&EvalVertexView>, - motif_counter: AccId>, - tmp_counts: Iter, -) { - for v in vs { - let mc = v.read(&motif_counter); - let triangle: [usize; 8] = mc - .triangle - .iter() - .zip(tmp_counts.clone()) - .map(|(&i1, &i2)| i1 + i2) - .collect::>() - .try_into() - .unwrap(); - v.update( - &motif_counter, - MotifCounter::from_triangle_counter(triangle), - ); - } -} - -#[derive(Eq, PartialEq, Clone, Debug, Default)] -pub struct MotifCounter { - pub two_nodes: [usize; 8], - pub star_nodes: [usize; 24], - pub triangle: [usize; 8], -} - -impl MotifCounter { - fn new(two_nodes: [usize; 8], star_nodes: [usize; 24], triangle: [usize; 8]) -> Self { - Self { - two_nodes, - star_nodes, - triangle, - } - } - - pub(crate) fn from_triangle_counter(triangle: [usize; 8]) -> Self { - Self { - two_nodes: [0; 8], - star_nodes: [0; 24], - triangle, - } - } -} - -impl Add for MotifCounter { - type Output = MotifCounter; - - fn add(self, rhs: Self) -> Self::Output { - rhs - } -} - -impl Zero for MotifCounter { - fn zero() -> Self { - MotifCounter { - two_nodes: [0; 8], - star_nodes: [0; 24], - triangle: [0; 8], - } - } - - fn set_zero(&mut self) { - *self = Zero::zero(); - } - - fn is_zero(&self) -> bool { - self.two_nodes == [0; 8] && self.star_nodes == [0; 24] && self.triangle == [0; 8] - } -} - -pub fn global_temporal_three_node_motif( - graph: &G, - threads: Option, - delta: i64, -) -> Vec { - let counts = temporal_three_node_motif(graph, threads, delta); - let mut tmp_counts = counts.values().fold(vec![0; 40], |acc, x| { - acc.iter().zip(x.iter()).map(|(x1, x2)| x1 + x2).collect() - }); - for ind in 31..40 { - tmp_counts[ind] = tmp_counts[ind] / 3; - } - tmp_counts -} - -pub fn global_temporal_three_node_motif_from_local( - counts: HashMap>, -) -> Vec { - let mut tmp_counts = counts.values().fold(vec![0; 40], |acc, x| { - acc.iter().zip(x.iter()).map(|(x1, x2)| x1 + x2).collect() - }); - for ind in 31..40 { - tmp_counts[ind] = tmp_counts[ind] / 3; - } - tmp_counts -} - -pub fn temporal_three_node_motif( - g: &G, - threads: Option, - delta: i64, -) -> HashMap> { - let mut ctx: Context = g.into(); - let motifs_counter = val::(0); - - ctx.agg(motifs_counter); - - let step1 = ATask::new( - move |evv: &mut EvalVertexView| { - let g = evv.graph; - - triangle_motif_count(g, evv, delta, motifs_counter); - let two_nodes = twonode_motif_count(g, evv, delta); - let star_nodes = star_motif_count(evv, delta); - - *evv.get_mut() = MotifCounter::new( - two_nodes, - star_nodes, - evv.get().triangle, - ); - - Step::Continue - }, - ); - - let mut runner: TaskRunner = TaskRunner::new(ctx); - - runner.run( - vec![], - vec![Job::new(step1)], - MotifCounter::zero(), - |_, _, els, _| { - els.finalize(&motifs_counter, |motifs_counter| { - let triangles = motifs_counter.triangle.to_vec(); - let two_nodes = motifs_counter.two_nodes.to_vec(); - let tmp_stars = motifs_counter.star_nodes.to_vec(); - let stars: Vec = tmp_stars - .iter() - .zip(two_nodes.iter().cycle().take(24)) - .map(|(&x1, &x2)| x1 - x2) - .collect(); - let mut final_cts = Vec::new(); - final_cts.extend(stars.into_iter()); - final_cts.extend(two_nodes.into_iter()); - final_cts.extend(triangles.into_iter()); - - final_cts - }) - }, - threads, - 1, - None, - None, - ) -} - -#[cfg(test)] -mod motifs_test { - use super::*; - use crate::db::graph::Graph; - - fn load_graph(n_shards: usize, edges: Vec<(i64, u64, u64)>) -> Graph { - let graph = Graph::new(n_shards); - - for (t, src, dst) in edges { - graph.add_edge(t, src, dst, &vec![], None).unwrap(); - } - graph - } - - #[test] - #[ignore = "This is not correct, it needs a rethink of the algorithm to be parallel"] - fn test_two_node_motif() { - let g = load_graph( - 1, - vec![ - (1, 1, 2), - (2, 1, 3), - (3, 1, 4), - (4, 3, 1), - (5, 3, 4), - (6, 3, 5), - (7, 4, 5), - (8, 5, 6), - (9, 5, 8), - (10, 7, 5), - (11, 8, 5), - (12, 1, 9), - (13, 9, 1), - (14, 6, 3), - (15, 4, 8), - (16, 8, 3), - (17, 5, 10), - (18, 10, 5), - (19, 10, 8), - (20, 1, 11), - (21, 11, 1), - (22, 9, 11), - (23, 11, 9), - ], - ); - - let actual = temporal_three_node_motif(&g, None, 10); - - let expected: HashMap> = HashMap::from([ - ( - "1".to_string(), - vec![ - 0, 0, 0, 0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 2, 0, - ], - ), - ( - "10".to_string(), - vec![ - 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, - ], - ), - ( - "11".to_string(), - vec![ - 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, - ], - ), - ( - "2".to_string(), - vec![ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ], - ), - ( - "3".to_string(), - vec![ - 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 1, 0, 2, 0, 1, 2, 0, - ], - ), - ( - "4".to_string(), - vec![ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 2, 0, - ], - ), - ( - "5".to_string(), - vec![ - 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 4, 0, 0, 0, 3, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 1, 2, 1, 3, 0, 1, 1, 1, - ], - ), - ( - "6".to_string(), - vec![ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, - ], - ), - ( - "7".to_string(), - vec![ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - ], - ), - ( - "8".to_string(), - vec![ - 0, 0, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 1, 2, 1, 2, 0, 1, 0, 1, - ], - ), - ( - "9".to_string(), - vec![ - 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, - ], - ), - ]); - - for ind in 1..12 { - assert_eq!( - actual.get(&ind.to_string()).unwrap(), - expected.get(&ind.to_string()).unwrap() - ); - } - } -} diff --git a/raphtory/src/algorithms/motifs/three_node_local_single_thread.rs b/raphtory/src/algorithms/motifs/three_node_local_single_thread.rs new file mode 100644 index 0000000000..2cad41bb5a --- /dev/null +++ b/raphtory/src/algorithms/motifs/three_node_local_single_thread.rs @@ -0,0 +1,445 @@ +use crate::algorithms::algorithm_result::AlgorithmResult; +/// This class regards the counting of the number of three edge, up-to-three node delta-temporal motifs in the graph, using the algorithm of Paranjape et al, Motifs in Temporal Networks (2017). +/// We point the reader to this reference for more information on the algorithm and background, but provide a short summary below. +/// +/// ## Motifs included +/// +/// ### Stars +/// +/// There are three classes (in the order they are outputted) of star motif on three nodes based on the switching behaviour of the edges between the two leaf nodes. +/// +/// - PRE: Stars of the form i<->j, i<->j, i<->k (ie two interactions with leaf j followed by one with leaf k) +/// - MID: Stars of the form i<->j, i<->k, i<->j (ie switching interactions from leaf j to leaf k, back to j again) +/// - POST: Stars of the form i<->j, i<->k, i<->k (ie one interaction with leaf j followed by two with leaf k) +/// +/// Within each of these classes is 8 motifs depending on the direction of the first to the last edge -- incoming "I" or outgoing "O". +/// These are enumerated in the order III, IIO, IOI, IOO, OII, OIO, OOI, OOO (like binary with "I"-0 and "O"-1). +/// +/// ### Two node motifs +/// +/// Also included are two node motifs, of which there are 8 when counted from the perspective of each vertex. These are characterised by the direction of each edge, enumerated +/// in the above order. Note that for the global graph counts, each motif is counted in both directions (a single III motif for one vertex is an OOO motif for the other vertex). +/// +/// ### Triangles +/// +/// There are 8 triangle motifs: +/// +/// 1. i --> j, k --> j, i --> k +/// 2. i --> j, k --> i, j --> k +/// 3. i --> j, j --> k, i --> k +/// 4. i --> j, i --> k, j --> k +/// 5. i --> j, k --> j, k --> i +/// 6. i --> j, k --> i, k --> j +/// 7. i --> j, j --> k, k --> i +/// 8. i --> j, i --> k, k --> j +/// +use crate::{algorithms::motifs::three_node_motifs::*, db::api::view::*}; +use std::collections::HashMap; + +fn star_motif_count(graph: &G, v: u64, delta: i64) -> [usize; 24] { + if let Some(vertex) = graph.vertex(v) { + let neigh_map: HashMap = vertex + .neighbours() + .iter() + .enumerate() + .map(|(num, nb)| (nb.id(), num)) + .into_iter() + .collect(); + let mut exploded_edges = vertex + .edges() + .explode() + .map(|edge| { + if edge.src().id() == v { + star_event(neigh_map[&edge.dst().id()], 1, edge.time().unwrap()) + } else { + star_event(neigh_map[&edge.src().id()], 0, edge.time().unwrap()) + } + }) + .collect::>(); + exploded_edges.sort_by_key(|e| e.time); + let mut star_count = init_star_count(neigh_map.len()); + star_count.execute(&exploded_edges, delta); + star_count.return_counts() + } else { + [0; 24] + } +} + +fn twonode_motif_count(graph: &G, v: u64, delta: i64) -> [usize; 8] { + let mut counts = [0; 8]; + if let Some(vertex) = graph.vertex(v) { + for nb in vertex.neighbours().iter() { + let nb_id = nb.id(); + let out = graph.edge(vertex.id(), nb_id); + let inc = graph.edge(nb_id, vertex.id()); + let mut all_exploded = match (out, inc) { + (Some(o), Some(i)) => o + .explode() + .chain(i.explode()) + .map(|e| { + two_node_event(if e.src().id() == v { 1 } else { 0 }, e.time().unwrap()) + }) + .collect::>(), + (Some(o), None) => o + .explode() + .map(|e| two_node_event(1, e.time().unwrap())) + .collect::>(), + (None, Some(i)) => i + .explode() + .map(|e| two_node_event(0, e.time().unwrap())) + .collect::>(), + (None, None) => Vec::new(), + }; + all_exploded.sort_by_key(|e| e.time); + let mut two_node_counter = init_two_node_count(); + two_node_counter.execute(&all_exploded, delta); + let two_node_result = two_node_counter.return_counts(); + for i in 0..8 { + counts[i] += two_node_result[i]; + } + } + } + counts +} + +fn triangle_motif_count( + graph: &G, + delta: i64, +) -> AlgorithmResult> { + let mut counts: HashMap> = HashMap::new(); + for u in graph.vertices() { + counts.insert(u.id(), vec![0; 8]); + } + for u in graph.vertices() { + let uid = u.id(); + for v in u.neighbours().iter().filter(|x| x.id() > uid) { + for nb in u.neighbours().iter().filter(|x| x.id() > v.id()) { + let mut tri_edges: Vec = Vec::new(); + let out = graph.edge(v.id(), nb.id()); + let inc = graph.edge(nb.id(), v.id()); + // The following code checks for triangles + match (out, inc) { + (Some(o), Some(i)) => { + tri_edges.append( + &mut o + .explode() + .map(|e| new_triangle_edge(false, 1, 0, 1, e.time().unwrap())) + .collect::>(), + ); + tri_edges.append( + &mut i + .explode() + .map(|e| new_triangle_edge(false, 1, 0, 0, e.time().unwrap())) + .collect::>(), + ); + } + (Some(o), None) => { + tri_edges.append( + &mut o + .explode() + .map(|e| new_triangle_edge(false, 1, 0, 1, e.time().unwrap())) + .collect::>(), + ); + } + (None, Some(i)) => { + tri_edges.append( + &mut i + .explode() + .map(|e| new_triangle_edge(false, 1, 0, 0, e.time().unwrap())) + .collect::>(), + ); + } + (None, None) => { + continue; + } + } + if !tri_edges.is_empty() { + let uout = graph.edge(uid, nb.id()); + let uin = graph.edge(nb.id(), uid); + match (uout, uin) { + (Some(o), Some(i)) => { + tri_edges.append( + &mut o + .explode() + .map(|e| new_triangle_edge(false, 0, 0, 1, e.time().unwrap())) + .collect::>(), + ); + tri_edges.append( + &mut i + .explode() + .map(|e| new_triangle_edge(false, 0, 0, 0, e.time().unwrap())) + .collect::>(), + ); + } + (Some(o), None) => { + tri_edges.append( + &mut o + .explode() + .map(|e| new_triangle_edge(false, 0, 0, 1, e.time().unwrap())) + .collect::>(), + ); + } + (None, Some(i)) => { + tri_edges.append( + &mut i + .explode() + .map(|e| new_triangle_edge(false, 0, 0, 0, e.time().unwrap())) + .collect::>(), + ); + } + (None, None) => { + continue; + } + } + // found triangle at this point!! + let u_to_v = match graph.edge(uid, v.id()) { + Some(edge) => { + let r = edge + .explode() + .map(|e| new_triangle_edge(true, 1, 0, 1, e.time().unwrap())) + .collect::>(); + r.into_iter() + } + None => vec![].into_iter(), + }; + let v_to_u = match graph.edge(v.id(), uid) { + Some(edge) => { + let r = edge + .explode() + .map(|e| new_triangle_edge(true, 0, 0, 0, e.time().unwrap())) + .collect::>(); + r.into_iter() + } + None => vec![].into_iter(), + }; + tri_edges.append(&mut u_to_v.collect::>()); + tri_edges.append(&mut v_to_u.collect::>()); + tri_edges.sort_by_key(|e| e.time); + + let mut tri_count = init_tri_count(1); + tri_count.execute(&tri_edges, delta); + let tmp_counts = tri_count.return_counts().iter(); + for id in [uid, v.id(), nb.id()] { + counts.insert( + id, + counts + .get(&id) + .unwrap() + .iter() + .zip(tmp_counts.clone()) + .map(|(&i1, &i2)| i1 + i2) + .collect::>(), + ); + } + } + } + } + } + AlgorithmResult::new(counts) +} + +/// Computes the number of each type of motif that each node participates in. +/// +/// # Arguments +/// +/// * `g` - A reference to the graph +/// * `delta` - Maximum time difference between the first and last edge of the +/// motif. NB if time for edges was given as a UNIX epoch, this should be given in seconds, otherwise +/// milliseconds should be used (if edge times were given as string) +/// +/// # Returns +/// +/// A dictionary with vertex ids (u64) as keys and a 40 dimensional array of motif counts as a value. The first 24 elements are star counts, +/// the next 8 are two-node motif counts and the final 8 are triangle counts. +/// +/// # Notes +/// +/// For this local count, a node is counted as participating in a motif in the following way. For star motifs, only the centre node counts +/// the motif. For two node motifs, both constituent nodes count the motif. For triangles, all three constituent nodes count the motif. +/// +/// +pub fn local_temporal_three_node_motifs( + graph: &G, + delta: i64, +) -> AlgorithmResult> { + let mut counts = triangle_motif_count(graph, delta).get_all().to_owned(); + for v in graph.vertices() { + let vid = v.id(); + let two_nodes = twonode_motif_count(graph, vid, delta).to_vec(); + let tmp_stars = star_motif_count(graph, vid, delta); + let stars: Vec = tmp_stars + .iter() + .zip(two_nodes.iter().cycle().take(24)) + .map(|(&x1, &x2)| x1 - x2) + .collect(); + let mut final_cts = Vec::new(); + final_cts.extend(stars.into_iter()); + final_cts.extend(two_nodes.into_iter()); + final_cts.extend(counts.get(&vid).unwrap().into_iter()); + counts.insert(vid, final_cts); + } + AlgorithmResult::new(counts) +} + +/// Computes the number of each type of motif there is in the graph. +/// +/// # Arguments +/// +/// * `g` - A reference to the graph +/// * `delta` - Maximum time difference between the first and last edge of the +/// motif. NB if time for edges was given as a UNIX epoch, this should be given in seconds, otherwise +/// milliseconds should be used (if edge times were given as string) +/// +/// # Returns +/// +/// A 40 dimensional array with the counts of each motif, given in the same order as described in the class summary. Note that the two-node motif counts are symmetrical so it may be more useful just to consider the first four elements. +/// +/// # Notes +/// +/// This is achieved by calling the local motif counting algorithm, summing the resulting arrays and dealing with overcounted motifs: the triangles (by dividing each motif count by three) and two-node motifs (dividing by two). +/// +/// +pub fn global_temporal_three_node_motifs(graph: &G, delta: i64) -> Vec { + let counts = local_temporal_three_node_motifs(graph, delta) + .get_all() + .to_owned(); + let mut tmp_counts = counts.values().fold(vec![0; 40], |acc, x| { + acc.iter().zip(x.iter()).map(|(x1, x2)| x1 + x2).collect() + }); + for ind in 32..40 { + tmp_counts[ind] /= 3; + } + tmp_counts +} + +#[cfg(test)] +mod local_motif_test { + use crate::{ + algorithms::motifs::three_node_local_single_thread::*, + db::{api::mutation::AdditionOps, graph::graph::Graph}, + prelude::NO_PROPS, + }; + + #[test] + fn test_init() { + let graph = Graph::new(); + + let vs = vec![ + (1, 2, 1), + (1, 3, 2), + (1, 4, 3), + (3, 1, 4), + (3, 4, 5), + (3, 5, 6), + (4, 5, 7), + (5, 6, 8), + (5, 8, 9), + (7, 5, 10), + (8, 5, 11), + (1, 9, 12), + (9, 1, 13), + (6, 3, 14), + (4, 8, 15), + (8, 3, 16), + (5, 10, 17), + (10, 5, 18), + (10, 8, 19), + (1, 11, 20), + (11, 1, 21), + (9, 11, 22), + (11, 9, 23), + ]; + + for (src, dst, time) in &vs { + graph.add_edge(*time, *src, *dst, NO_PROPS, None).unwrap(); + } + + // let counts = star_motif_count(&graph, 1, 100); + let counts = local_temporal_three_node_motifs(&graph, 10); + // FIXME: Should test this + let _global_counts = global_temporal_three_node_motifs(&graph, 10); + let expected: HashMap> = HashMap::from([ + ( + 1, + vec![ + 0, 0, 0, 0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 2, 0, + ], + ), + ( + 10, + vec![ + 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, + ], + ), + ( + 11, + vec![ + 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, + ], + ), + ( + 2, + vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + ), + ( + 3, + vec![ + 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 0, 2, 0, 1, 2, 0, + ], + ), + ( + 4, + vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 2, 0, + ], + ), + ( + 5, + vec![ + 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 4, 0, 0, 0, 3, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 1, 2, 1, 3, 0, 1, 1, 1, + ], + ), + ( + 6, + vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, + ], + ), + ( + 7, + vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + ), + ( + 8, + vec![ + 0, 0, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 1, 2, 1, 2, 0, 1, 0, 1, + ], + ), + ( + 9, + vec![ + 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, + ], + ), + ]); + for ind in 1..12 { + assert_eq!(counts.get(&ind).unwrap(), expected.get(&ind).unwrap()); + } + // print!("{:?}", global_counts); + } +} diff --git a/raphtory/src/algorithms/motifs/three_node_motifs.rs b/raphtory/src/algorithms/motifs/three_node_motifs.rs index c55262a735..fa5e5fc445 100644 --- a/raphtory/src/algorithms/motifs/three_node_motifs.rs +++ b/raphtory/src/algorithms/motifs/three_node_motifs.rs @@ -72,7 +72,7 @@ pub fn init_two_node_count() -> TwoNodeCounter { // Star Motifs pub struct StarEvent { nb: usize, - dir: usize, + pub dir: usize, pub time: i64, } @@ -81,7 +81,7 @@ pub fn star_event(nb: usize, dir: usize, time: i64) -> StarEvent { } pub struct StarCounter { - N: usize, + n: usize, pre_nodes: Vec, post_nodes: Vec, pre_sum: [usize; 8], @@ -94,41 +94,41 @@ pub struct StarCounter { impl StarCounter { fn push_pre(&mut self, cur_edge: &StarEvent) { self.pre_sum[map2d(INCOMING, cur_edge.dir)] += - self.pre_nodes[INCOMING * self.N + cur_edge.nb]; + self.pre_nodes[INCOMING * self.n + cur_edge.nb]; self.pre_sum[map2d(OUTGOING, cur_edge.dir)] += - self.pre_nodes[OUTGOING * self.N + cur_edge.nb]; - self.pre_nodes[cur_edge.dir * self.N + cur_edge.nb] += 1; + self.pre_nodes[OUTGOING * self.n + cur_edge.nb]; + self.pre_nodes[cur_edge.dir * self.n + cur_edge.nb] += 1; } fn push_post(&mut self, cur_edge: &StarEvent) { self.post_sum[map2d(INCOMING, cur_edge.dir)] += - self.post_nodes[INCOMING * self.N + cur_edge.nb]; + self.post_nodes[INCOMING * self.n + cur_edge.nb]; self.post_sum[map2d(OUTGOING, cur_edge.dir)] += - self.post_nodes[OUTGOING * self.N + cur_edge.nb]; - self.post_nodes[cur_edge.dir * self.N + cur_edge.nb] += 1; + self.post_nodes[OUTGOING * self.n + cur_edge.nb]; + self.post_nodes[cur_edge.dir * self.n + cur_edge.nb] += 1; } fn pop_pre(&mut self, cur_edge: &StarEvent) { - self.pre_nodes[cur_edge.dir * self.N + cur_edge.nb] -= 1; + self.pre_nodes[cur_edge.dir * self.n + cur_edge.nb] -= 1; self.pre_sum[map2d(cur_edge.dir, INCOMING)] -= - self.pre_nodes[INCOMING * self.N + cur_edge.nb]; + self.pre_nodes[INCOMING * self.n + cur_edge.nb]; self.pre_sum[map2d(cur_edge.dir, OUTGOING)] -= - self.pre_nodes[OUTGOING * self.N + cur_edge.nb]; + self.pre_nodes[OUTGOING * self.n + cur_edge.nb]; } fn pop_post(&mut self, cur_edge: &StarEvent) { - self.post_nodes[cur_edge.dir * self.N + cur_edge.nb] -= 1; + self.post_nodes[cur_edge.dir * self.n + cur_edge.nb] -= 1; self.post_sum[map2d(cur_edge.dir, INCOMING)] -= - self.post_nodes[INCOMING * self.N + cur_edge.nb]; + self.post_nodes[INCOMING * self.n + cur_edge.nb]; self.post_sum[map2d(cur_edge.dir, OUTGOING)] -= - self.post_nodes[OUTGOING * self.N + cur_edge.nb]; + self.post_nodes[OUTGOING * self.n + cur_edge.nb]; } fn process_current(&mut self, cur_edge: &StarEvent) { self.mid_sum[map2d(INCOMING, cur_edge.dir)] -= - self.pre_nodes[INCOMING * self.N + cur_edge.nb]; + self.pre_nodes[INCOMING * self.n + cur_edge.nb]; self.mid_sum[map2d(OUTGOING, cur_edge.dir)] -= - self.pre_nodes[OUTGOING * self.N + cur_edge.nb]; + self.pre_nodes[OUTGOING * self.n + cur_edge.nb]; for (d1, d2) in DIRS2D { self.count_pre[map3d(d1, d2, cur_edge.dir)] += self.pre_sum[map2d(d1, d2)]; @@ -137,24 +137,24 @@ impl StarCounter { } self.mid_sum[map2d(cur_edge.dir, INCOMING)] += - self.post_nodes[INCOMING * self.N + cur_edge.nb]; + self.post_nodes[INCOMING * self.n + cur_edge.nb]; self.mid_sum[map2d(cur_edge.dir, OUTGOING)] += - self.post_nodes[OUTGOING * self.N + cur_edge.nb]; + self.post_nodes[OUTGOING * self.n + cur_edge.nb]; } pub fn execute(&mut self, edges: &Vec, delta: i64) { - let L = edges.len(); - if L < 3 { + let l = edges.len(); + if l < 3 { return; } let mut start = 0; let mut end = 0; - for j in 0..L { - while start < L && edges[start].time + delta < edges[j].time { + for j in 0..l { + while start < l && edges[start].time + delta < edges[j].time { self.pop_pre(&edges[start]); start += 1; } - while (end < L) && edges[end].time <= edges[j].time + delta { + while (end < l) && edges[end].time <= edges[j].time + delta { self.push_post(&edges[end]); end += 1; } @@ -174,11 +174,11 @@ impl StarCounter { counts } } -pub fn init_star_count(N: usize) -> StarCounter { +pub fn init_star_count(n: usize) -> StarCounter { StarCounter { - N: N, - pre_nodes: vec![0; 2 * N], - post_nodes: vec![0; 2 * N], + n, + pre_nodes: vec![0; 2 * n], + post_nodes: vec![0; 2 * n], pre_sum: [0; 8], mid_sum: [0; 8], post_sum: [0; 8], @@ -214,7 +214,7 @@ pub fn new_triangle_edge( } pub struct TriangleCounter { - N: usize, + n: usize, pre_nodes: Vec, post_nodes: Vec, pre_sum: [usize; 8], @@ -224,18 +224,18 @@ pub struct TriangleCounter { } impl TriangleCounter { pub fn execute(&mut self, edges: &Vec, delta: i64) { - let L = edges.len(); - if L < 3 { + let l = edges.len(); + if l < 3 { return; } let mut start = 0; let mut end = 0; - for j in 0..L { - while start < L && edges[start].time + delta < edges[j].time { + for j in 0..l { + while start < l && edges[start].time + delta < edges[j].time { self.pop_pre(&edges[start]); start += 1; } - while (end < L) && edges[end].time <= edges[j].time + delta { + while (end < l) && edges[end].time <= edges[j].time + delta { self.push_post(&edges[end]); end += 1; } @@ -249,10 +249,10 @@ impl TriangleCounter { let (is_uor_v, nb, dir) = (cur_edge.uorv, cur_edge.nb, cur_edge.dir); if !cur_edge.uv_edge { self.pre_sum[map3d(1 - is_uor_v, INCOMING, dir)] += - self.pre_nodes[self.N * map2d(INCOMING, 1 - is_uor_v) + nb]; + self.pre_nodes[self.n * map2d(INCOMING, 1 - is_uor_v) + nb]; self.pre_sum[map3d(1 - is_uor_v, OUTGOING, dir)] += - self.pre_nodes[self.N * map2d(OUTGOING, 1 - is_uor_v) + nb]; - self.pre_nodes[self.N * map2d(dir, is_uor_v) + nb] += 1; + self.pre_nodes[self.n * map2d(OUTGOING, 1 - is_uor_v) + nb]; + self.pre_nodes[self.n * map2d(dir, is_uor_v) + nb] += 1; } } @@ -260,32 +260,32 @@ impl TriangleCounter { let (is_uor_v, nb, dir) = (cur_edge.uorv, cur_edge.nb, cur_edge.dir); if !cur_edge.uv_edge { self.post_sum[map3d(1 - is_uor_v, INCOMING, dir)] += - self.post_nodes[self.N * map2d(INCOMING, 1 - is_uor_v) + nb]; + self.post_nodes[self.n * map2d(INCOMING, 1 - is_uor_v) + nb]; self.post_sum[map3d(1 - is_uor_v, OUTGOING, dir)] += - self.post_nodes[self.N * map2d(OUTGOING, 1 - is_uor_v) + nb]; - self.post_nodes[self.N * map2d(dir, is_uor_v) + nb] += 1; + self.post_nodes[self.n * map2d(OUTGOING, 1 - is_uor_v) + nb]; + self.post_nodes[self.n * map2d(dir, is_uor_v) + nb] += 1; } } fn pop_pre(&mut self, cur_edge: &TriangleEdge) { let (is_uor_v, nb, dir) = (cur_edge.uorv, cur_edge.nb, cur_edge.dir); if !cur_edge.uv_edge { - self.pre_nodes[self.N * map2d(dir, is_uor_v) + nb] -= 1; + self.pre_nodes[self.n * map2d(dir, is_uor_v) + nb] -= 1; self.pre_sum[map3d(is_uor_v, dir, INCOMING)] -= - self.pre_nodes[self.N * map2d(INCOMING, 1 - is_uor_v)]; + self.pre_nodes[self.n * map2d(INCOMING, 1 - is_uor_v)]; self.pre_sum[map3d(is_uor_v, dir, OUTGOING)] -= - self.pre_nodes[self.N * map2d(OUTGOING, 1 - is_uor_v)]; + self.pre_nodes[self.n * map2d(OUTGOING, 1 - is_uor_v)]; } } fn pop_post(&mut self, cur_edge: &TriangleEdge) { let (is_uor_v, nb, dir) = (cur_edge.uorv, cur_edge.nb, cur_edge.dir); if !cur_edge.uv_edge { - self.post_nodes[self.N * map2d(dir, is_uor_v) + nb] -= 1; + self.post_nodes[self.n * map2d(dir, is_uor_v) + nb] -= 1; self.post_sum[map3d(is_uor_v, dir, INCOMING)] -= - self.post_nodes[self.N * map2d(INCOMING, 1 - is_uor_v)]; + self.post_nodes[self.n * map2d(INCOMING, 1 - is_uor_v)]; self.post_sum[map3d(is_uor_v, dir, OUTGOING)] -= - self.post_nodes[self.N * map2d(OUTGOING, 1 - is_uor_v)]; + self.post_nodes[self.n * map2d(OUTGOING, 1 - is_uor_v)]; } } @@ -293,13 +293,13 @@ impl TriangleCounter { let (is_uor_v, nb, dir) = (cur_edge.uorv, cur_edge.nb, cur_edge.dir); if !cur_edge.uv_edge { self.mid_sum[map3d(1 - is_uor_v, INCOMING, dir)] -= - self.pre_nodes[self.N * map2d(INCOMING, 1 - is_uor_v) + nb]; + self.pre_nodes[self.n * map2d(INCOMING, 1 - is_uor_v) + nb]; self.mid_sum[map3d(1 - is_uor_v, OUTGOING, dir)] -= - self.pre_nodes[self.N * map2d(OUTGOING, 1 - is_uor_v) + nb]; + self.pre_nodes[self.n * map2d(OUTGOING, 1 - is_uor_v) + nb]; self.mid_sum[map3d(is_uor_v, dir, INCOMING)] += - self.post_nodes[self.N * map2d(INCOMING, 1 - is_uor_v) + nb]; + self.post_nodes[self.n * map2d(INCOMING, 1 - is_uor_v) + nb]; self.mid_sum[map3d(is_uor_v, dir, OUTGOING)] += - self.post_nodes[self.N * map2d(OUTGOING, 1 - is_uor_v) + nb]; + self.post_nodes[self.n * map2d(OUTGOING, 1 - is_uor_v) + nb]; } else { self.final_counts[0] += self.mid_sum[map3d(dir, 0, 0)] + self.post_sum[map3d(dir, 0, 1)] @@ -334,7 +334,7 @@ impl TriangleCounter { } pub fn init_tri_count(n: usize) -> TriangleCounter { TriangleCounter { - N: n, + n: n, pre_nodes: vec![0; 4 * n], post_nodes: vec![0; 4 * n], pre_sum: [0; 8], diff --git a/raphtory/src/algorithms/motifs/three_node_temporal_motifs.rs b/raphtory/src/algorithms/motifs/three_node_temporal_motifs.rs new file mode 100644 index 0000000000..d1cbd35711 --- /dev/null +++ b/raphtory/src/algorithms/motifs/three_node_temporal_motifs.rs @@ -0,0 +1,632 @@ +// Imports /////////////////////////////////////////// +use crate::{ + algorithms::{k_core::k_core_set, motifs::three_node_motifs::*}, + core::state::{ + accumulator_id::{ + accumulators::{self, val}, + AccId, + }, + agg::ValDef, + compute_state::ComputeStateVec, + }, + db::{ + api::view::{GraphViewOps, VertexViewOps, *}, + graph::{edge::EdgeView, views::vertex_subgraph::VertexSubgraph}, + task::{ + context::Context, + edge::eval_edge::EvalEdgeView, + task::{ATask, Job, Step}, + task_runner::TaskRunner, + vertex::eval_vertex::EvalVertexView, + }, + }, +}; + +use crate::core::entities::vertices::vertex_ref::VertexRef; +use itertools::{enumerate, Itertools}; +use num_traits::Zero; +use rand::{rngs::StdRng, Rng, SeedableRng}; +use rustc_hash::FxHashSet; +use std::{cmp::Ordering, collections::HashMap, ops::Add, slice::Iter}; +/////////////////////////////////////////////////////// + +// State objects for three node motifs +#[derive(Eq, PartialEq, Clone, Debug)] +pub struct MotifCounter { + pub two_nodes: Vec<[usize; 8]>, + pub star_nodes: Vec<[usize; 24]>, + pub triangle: Vec<[usize; 8]>, +} + +impl MotifCounter { + fn new( + size: usize, + two_nodes: Vec<[usize; 8]>, + star_nodes: Vec<[usize; 24]>, + triangle: Vec<[usize; 8]>, + ) -> Self { + let _ = size; + Self { + two_nodes: two_nodes, + star_nodes: star_nodes, + triangle: triangle, + } + } +} + +impl Default for MotifCounter { + fn default() -> Self { + Self::zero() + } +} + +impl Add for MotifCounter { + type Output = MotifCounter; + + fn add(self, rhs: Self) -> Self::Output { + rhs + } +} + +impl Zero for MotifCounter { + fn zero() -> Self { + MotifCounter { + two_nodes: vec![], + star_nodes: vec![], + triangle: vec![], + } + } + + fn set_zero(&mut self) { + *self = Zero::zero(); + } + + fn is_zero(&self) -> bool { + self.two_nodes.is_empty() && self.star_nodes.is_empty() && self.triangle.is_empty() + } +} + +/////////////////////////////////////////////////////// + +pub fn star_motif_count( + evv: &EvalVertexView, + deltas: Vec, +) -> Vec<[usize; 24]> +where + G: GraphViewOps, +{ + let neigh_map: HashMap = evv + .neighbours() + .into_iter() + .enumerate() + .map(|(num, nb)| (nb.id(), num)) + .collect(); + let mut events = evv + .edges() + .explode() + .sorted_by_key(|e| e.time_and_index()) + .map(|edge| { + if edge.src().id() == evv.id() { + star_event(neigh_map[&edge.dst().id()], 1, edge.time().unwrap()) + } else { + star_event(neigh_map[&edge.src().id()], 0, edge.time().unwrap()) + } + }) + .collect::>(); + + deltas + .into_iter() + .map(|delta| { + let mut star_count = init_star_count(evv.degree()); + star_count.execute(&events, delta); + star_count.return_counts() + }) + .collect::>() +} + +/////////////////////////////////////////////////////// + +pub fn twonode_motif_count( + graph: &G, + evv: &EvalVertexView, + deltas: Vec, +) -> Vec<[usize; 8]> +where + G: GraphViewOps, +{ + let mut results = deltas.iter().map(|_| [0; 8]).collect::>(); + + // Define a closure for sorting by time_and_index() + let sort_by_time_and_index = |e1: &EdgeView, e2: &EdgeView| -> Ordering { + Ord::cmp(&e1.time_and_index(), &e2.time_and_index()) + }; + + for nb in evv.neighbours().into_iter() { + let nb_id = nb.id(); + let out = graph.edge(evv.id(), nb_id); + let inc = graph.edge(nb_id, evv.id()); + let mut events: Vec = out + .iter() + .flat_map(|e| e.explode()) + .chain(inc.iter().flat_map(|e| e.explode())) + .sorted_by_key(|e| e.time_and_index()) + .map(|e| { + two_node_event( + if e.src().id() == evv.id() { 1 } else { 0 }, + e.time().unwrap(), + ) + }) + .collect(); + for j in 0..deltas.len() { + let mut two_node_counter = init_two_node_count(); + two_node_counter.execute(&events, deltas[j]); + let two_node_result = two_node_counter.return_counts(); + for i in 0..8 { + results[j][i] += two_node_result[i]; + } + } + } + results +} + +/////////////////////////////////////////////////////// + +pub fn triangle_motifs( + graph: &G, + deltas: Vec, + _motifs_count_id: AccId>, + threads: Option, +) -> HashMap> +where + G: GraphViewOps, +{ + let delta_len = deltas.len(); + + // Define a closure for sorting by time_and_index() + let sort_by_time_and_index = + |e1: &EdgeView>, e2: &EdgeView>| -> Ordering { + Ord::cmp(&e1.time_and_index(), &e2.time_and_index()) + }; + + // Define a closure for sorting by time() + let vertex_set = k_core_set(graph, 2, usize::MAX, None); + let g: VertexSubgraph = graph.subgraph(vertex_set); + let mut ctx: Context, ComputeStateVec> = Context::from(&g); + + let neighbours_set = accumulators::hash_set::(1); + + ctx.agg(neighbours_set); + + let step1 = ATask::new( + move |u: &mut EvalVertexView, ComputeStateVec, MotifCounter>| { + for v in u.neighbours() { + if u.id() > v.id() { + v.update(&neighbours_set, u.id()); + } + } + Step::Continue + }, + ); + + let step2 = ATask::new( + move |u: &mut EvalVertexView, ComputeStateVec, MotifCounter>| { + let uu = u.get_mut(); + if uu.triangle.len() == 0 { + uu.triangle = vec![[0 as usize; 8]; delta_len]; + } + for v in u.neighbours() { + // Find triangles on the UV edge + if u.id() > v.id() { + let intersection_nbs = { + match ( + u.entry(&neighbours_set) + .read_ref() + .unwrap_or(&FxHashSet::default()), + v.entry(&neighbours_set) + .read_ref() + .unwrap_or(&FxHashSet::default()), + ) { + (u_set, v_set) => { + let intersection = + u_set.intersection(v_set).cloned().collect::>(); + intersection + } + } + }; + + if intersection_nbs.is_empty() { + continue; + } + // let mut nb_ct = 0; + intersection_nbs.iter().for_each(|w| { + // For each triangle, run the triangle count. + + let mut all_exploded = vec![u.id(), v.id(), *w] + .into_iter() + .sorted() + .permutations(2) + .flat_map(|e| { + g.edge(e.get(0).unwrap().clone(), e.get(1).unwrap().clone()) + .iter() + .flat_map(|edge| edge.explode()) + .collect::>() + }) + .sorted_by_key(|e| e.time_and_index()) + .map(|e| { + let (src_id, dst_id) = (e.src().id(), e.dst().id()); + let (uid, vid) = (u.id(), v.id()); + if src_id == w.clone() { + new_triangle_edge( + false, + if dst_id == uid { 0 } else { 1 }, + 0, + 0, + e.time().unwrap(), + ) + } else if dst_id == w.clone() { + new_triangle_edge( + false, + if src_id == uid { 0 } else { 1 }, + 0, + 1, + e.time().unwrap(), + ) + } else if src_id == uid { + new_triangle_edge(true, 1, 0, 1, e.time().unwrap()) + } else { + new_triangle_edge(true, 0, 0, 0, e.time().unwrap()) + } + }) + .collect::>(); + + for i in 0..deltas.len() { + let delta = deltas[i]; + let mut tri_count = init_tri_count(2); + tri_count.execute(&all_exploded, delta); + let tmp_counts: Iter = tri_count.return_counts().iter(); + + // Triangle counts are going to be WRONG without w + // update_counter(&mut vec![u, &v], motifs_count_id, tmp_counts); + + let mc_u = u.get_mut(); + let triangle_u = mc_u.triangle[i] + .iter() + .zip(tmp_counts.clone()) + .map(|(&i1, &i2)| i1 + i2) + .collect::>() + .try_into() + .unwrap(); + mc_u.triangle[i] = triangle_u; + } + }) + } + } + Step::Continue + }, + ); + + let mut runner: TaskRunner, _> = TaskRunner::new(ctx); + + runner.run( + vec![Job::new(step1)], + vec![Job::read_only(step2)], + None, + |_, _, _els, local| { + let mut tri_motifs = HashMap::new(); + let layers = graph.layer_ids(); + let edge_filter = graph.edge_filter(); + for (vref, mc) in enumerate(local) { + if graph.has_vertex_ref(VertexRef::Internal(vref.into()), &layers, edge_filter) { + let v_gid = graph.vertex_name(vref.into()); + if mc.triangle.is_empty() { + tri_motifs.insert(v_gid.clone(), vec![[0; 8]; delta_len]); + } else { + tri_motifs.insert(v_gid.clone(), mc.triangle); + } + } + } + tri_motifs + }, + threads, + 1, + None, + None, + ) +} + +/////////////////////////////////////////////////////// + +pub fn temporal_three_node_motif( + g: &G, + deltas: Vec, + threads: Option, +) -> HashMap>> +where + G: GraphViewOps, +{ + let mut ctx: Context = g.into(); + let motifs_counter = val::(0); + let delta_len = deltas.len(); + + ctx.agg(motifs_counter); + + let out1 = triangle_motifs(g, deltas.clone(), motifs_counter, threads); + + let step1 = ATask::new( + move |evv: &mut EvalVertexView| { + let g = evv.graph; + + let two_nodes = twonode_motif_count(g, evv, deltas.clone()); + let star_nodes = star_motif_count(evv, deltas.clone()); + + *evv.get_mut() = MotifCounter::new( + deltas.len(), + two_nodes, + star_nodes, + evv.get().triangle.clone(), + ); + + Step::Continue + }, + ); + + let mut runner: TaskRunner = TaskRunner::new(ctx); + + let out2 = runner.run( + vec![Job::new(step1)], + vec![], + None, + |_, _, _els, local| { + let mut motifs = HashMap::new(); + for (vref, mc) in enumerate(local) { + let v_gid = g.vertex_name(vref.into()); + let triangles = out1 + .get(&v_gid) + .map(|v| v.clone()) + .unwrap_or_else(|| vec![[0 as usize; 8]; delta_len]); + let run_counts = (0..delta_len) + .map(|i| { + let two_nodes = mc.two_nodes[i].to_vec(); + let tmp_stars = mc.star_nodes[i].to_vec(); + let stars: Vec = tmp_stars + .iter() + .zip(two_nodes.iter().cycle().take(24)) + .map(|(&x1, &x2)| x1 - x2) + .collect(); + let mut final_cts = Vec::new(); + final_cts.extend(stars.into_iter()); + final_cts.extend(two_nodes.into_iter()); + final_cts.extend(triangles[i].into_iter()); + final_cts + }) + .collect::>>(); + motifs.insert(v_gid.clone(), run_counts); + } + motifs + }, + threads, + 1, + None, + None, + ); + out2 +} + +pub fn global_temporal_three_node_motif_from_local( + counts: HashMap>, +) -> Vec { + let tmp_counts = counts.values().fold(vec![0; 40], |acc, x| { + acc.iter().zip(x.iter()).map(|(x1, x2)| x1 + x2).collect() + }); + tmp_counts +} + +pub fn global_temporal_three_node_motif( + graph: &G, + delta: i64, + threads: Option, +) -> Vec { + let counts = global_temporal_three_node_motif_general(graph, vec![delta], threads); + counts[0].clone() +} + +pub fn global_temporal_three_node_motif_general( + graph: &G, + deltas: Vec, + threads: Option, +) -> Vec> { + let counts = temporal_three_node_motif(graph, deltas.clone(), threads); + + let mut result: Vec> = vec![vec![0; 40]; deltas.len()]; + for (_, values) in counts.iter() { + for i in 0..deltas.len() { + for j in 0..40 { + result[i][j] += values[i][j] + } + } + } + result +} + +#[cfg(test)] +mod motifs_test { + use super::*; + use crate::{ + db::{api::mutation::AdditionOps, graph::graph::Graph}, + prelude::NO_PROPS, + }; + + fn load_graph(edges: Vec<(i64, u64, u64)>) -> Graph { + let graph = Graph::new(); + + for (t, src, dst) in edges { + graph.add_edge(t, src, dst, NO_PROPS, None).unwrap(); + } + graph + } + + #[test] + #[ignore = "This is not correct, local version does not work"] + fn test_two_node_motif() { + let g = load_graph(vec![ + (1, 1, 2), + (2, 1, 3), + (3, 1, 4), + (4, 3, 1), + (5, 3, 4), + (6, 3, 5), + (7, 4, 5), + (8, 5, 6), + (9, 5, 8), + (10, 7, 5), + (11, 8, 5), + (12, 1, 9), + (13, 9, 1), + (14, 6, 3), + (15, 4, 8), + (16, 8, 3), + (17, 5, 10), + (18, 10, 5), + (19, 10, 8), + (20, 1, 11), + (21, 11, 1), + (22, 9, 11), + (23, 11, 9), + ]); + + let binding = temporal_three_node_motif(&g, Vec::from([10]), None); + let actual = binding + .iter() + .map(|(k, v)| (k, v[0].clone())) + .into_iter() + .collect::>>(); + + let expected: HashMap> = HashMap::from([ + ( + "1".to_string(), + vec![ + 0, 0, 0, 0, 1, 2, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 3, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 2, 0, + ], + ), + ( + "10".to_string(), + vec![ + 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, + ], + ), + ( + "11".to_string(), + vec![ + 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, + ], + ), + ( + "2".to_string(), + vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + ), + ( + "3".to_string(), + vec![ + 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 0, 2, 0, 1, 2, 0, + ], + ), + ( + "4".to_string(), + vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 2, 0, + ], + ), + ( + "5".to_string(), + vec![ + 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 4, 0, 0, 0, 3, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 1, 2, 1, 3, 0, 1, 1, 1, + ], + ), + ( + "6".to_string(), + vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, + ], + ), + ( + "7".to_string(), + vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + ), + ( + "8".to_string(), + vec![ + 0, 0, 2, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 1, 2, 1, 2, 0, 1, 0, 1, + ], + ), + ( + "9".to_string(), + vec![ + 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 0, + ], + ), + ]); + + for ind in 3..12 { + assert_eq!( + actual.get(&ind.to_string()).unwrap(), + expected.get(&ind.to_string()).unwrap() + ); + } + } + + #[test] + fn test_global() { + let g = load_graph(vec![ + (1, 1, 2), + (2, 1, 3), + (3, 1, 4), + (4, 3, 1), + (5, 3, 4), + (6, 3, 5), + (7, 4, 5), + (8, 5, 6), + (9, 5, 8), + (10, 7, 5), + (11, 8, 5), + (12, 1, 9), + (13, 9, 1), + (14, 6, 3), + (15, 4, 8), + (16, 8, 3), + (17, 5, 10), + (18, 10, 5), + (19, 10, 8), + (20, 1, 11), + (21, 11, 1), + (22, 9, 11), + (23, 11, 9), + ]); + + let global_motifs = &global_temporal_three_node_motif(&g, 10, None); + assert_eq!( + *global_motifs, + vec![ + 0, 0, 3, 6, 2, 3, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 1, 6, 0, 0, 1, 7, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 2, 3, 2, 4, 1, 2, 3, 1 + ] + .into_iter() + .map(|x| x as usize) + .collect::>() + ); + } +} diff --git a/raphtory/src/algorithms/pagerank.rs b/raphtory/src/algorithms/pagerank.rs index a04ac8a43d..828620afee 100644 --- a/raphtory/src/algorithms/pagerank.rs +++ b/raphtory/src/algorithms/pagerank.rs @@ -1,21 +1,23 @@ -use num_traits::abs; - -use crate::core::vertex_ref::LocalVertexRef; -use crate::db::view_api::VertexViewOps; use crate::{ - core::state::{accumulator_id::accumulators, compute_state::ComputeStateVec} , + algorithms::algorithm_result::AlgorithmResult, + core::{ + entities::{vertices::vertex_ref::VertexRef, VID}, + state::{accumulator_id::accumulators, compute_state::ComputeStateVec}, + }, db::{ + api::view::{GraphViewOps, VertexViewOps}, task::{ context::Context, task::{ATask, Job, Step}, task_runner::TaskRunner, }, - view_api::GraphViewOps, }, }; +use num_traits::abs; +use ordered_float::OrderedFloat; use std::collections::HashMap; -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] struct PageRankState { score: f64, out_degree: usize, @@ -34,6 +36,21 @@ impl PageRankState { } } +/// PageRank Algorithm: +/// PageRank shows how important a vertex is in a graph. +/// +/// Arguments: +/// +/// * `g`: A GraphView object +/// * `iter_count`: Number of iterations to run the algorithm for +/// * `threads`: Number of threads to use for parallel execution +/// * `tol`: The tolerance value for convergence +/// * `use_l2_norm`: Whether to use L2 norm for convergence +/// +/// Result: +/// +/// * An AlgorithmResult object containing the mapping from vertex ID to the PageRank score of the vertex +/// #[allow(unused_variables)] pub fn unweighted_page_rank( g: &G, @@ -41,9 +58,9 @@ pub fn unweighted_page_rank( threads: Option, tol: Option, use_l2_norm: bool, -) -> HashMap { - let n = g.num_vertices(); - let total_edges = g.num_edges(); +) -> AlgorithmResult> { + let n = g.count_vertices(); + let total_edges = g.count_edges(); let mut ctx: Context = g.into(); @@ -139,20 +156,23 @@ pub fn unweighted_page_rank( let mut runner: TaskRunner = TaskRunner::new(ctx); - let num_vertices = g.num_vertices(); + let num_vertices = g.count_vertices(); - let out: HashMap = runner.run( + let out: HashMap = runner.run( vec![Job::new(step1)], vec![Job::new(step2), Job::new(step3), Job::new(step4), step5], - PageRankState::new(num_vertices), - |g, _, _, local| { + Some(vec![PageRankState::new(num_vertices); num_vertices]), + |_, _, _, local| { + let layers = g.layer_ids(); + let edge_filter = g.edge_filter(); local .iter() - .filter_map(|line| { - line.as_ref() - .map(|(v_ref, state)| (v_ref.clone(), state.score)) + .enumerate() + .filter_map(|(v_ref, score)| { + g.has_vertex_ref(VertexRef::Internal(v_ref.into()), &layers, edge_filter) + .then_some((v_ref.into(), score.score)) }) - .collect::>() + .collect::>() }, threads, iter_count, @@ -160,9 +180,12 @@ pub fn unweighted_page_rank( None, ); - out.into_iter() + let res = out + .into_iter() .map(|(k, v)| (g.vertex_name(k), v)) - .collect() + .collect(); + + AlgorithmResult::new(res) } #[cfg(test)] @@ -172,27 +195,29 @@ mod page_rank_tests { use itertools::Itertools; use pretty_assertions::assert_eq; - use crate::db::graph::Graph; + use crate::{ + db::{api::mutation::AdditionOps, graph::graph::Graph}, + prelude::NO_PROPS, + }; use super::*; - fn load_graph(n_shards: usize) -> Graph { - let graph = Graph::new(n_shards); + fn load_graph() -> Graph { + let graph = Graph::new(); let edges = vec![(1, 2), (1, 4), (2, 3), (3, 1), (4, 1)]; for (src, dst) in edges { - graph.add_edge(0, src, dst, &vec![], None).unwrap(); + graph.add_edge(0, src, dst, NO_PROPS, None).unwrap(); } graph } - fn test_page_rank(n_shards: usize) { - let graph = load_graph(n_shards); + #[test] + fn test_page_rank() { + let graph = load_graph(); - let results: HashMap = unweighted_page_rank(&graph, 1000, Some(1), None, true) - .into_iter() - .collect(); + let results = unweighted_page_rank(&graph, 1000, Some(1), None, true); assert_eq_f64(results.get("1"), Some(&0.38694), 5); assert_eq_f64(results.get("2"), Some(&0.20195), 5); @@ -200,26 +225,6 @@ mod page_rank_tests { assert_eq_f64(results.get("3"), Some(&0.20916), 5); } - #[test] - fn test_page_rank_1() { - test_page_rank(1); - } - - #[test] - fn test_page_rank_2() { - test_page_rank(2); - } - - #[test] - fn test_page_rank_3() { - test_page_rank(3); - } - - #[test] - fn test_page_rank_4() { - test_page_rank(4); - } - #[test] fn motif_page_rank() { let edges = vec![ @@ -248,15 +253,13 @@ mod page_rank_tests { (11, 9, 23), ]; - let graph = Graph::new(4); + let graph = Graph::new(); for (src, dst, t) in edges { - graph.add_edge(t, src, dst, &vec![], None).unwrap(); + graph.add_edge(t, src, dst, NO_PROPS, None).unwrap(); } - let results: HashMap = unweighted_page_rank(&graph, 1000, Some(4), None, true) - .into_iter() - .collect(); + let results = unweighted_page_rank(&graph, 1000, Some(4), None, true); assert_eq_f64(results.get("10"), Some(&0.072082), 5); assert_eq_f64(results.get("8"), Some(&0.136473), 5); @@ -275,15 +278,13 @@ mod page_rank_tests { fn two_nodes_page_rank() { let edges = vec![(1, 2), (2, 1)]; - let graph = Graph::new(4); + let graph = Graph::new(); for (t, (src, dst)) in edges.into_iter().enumerate() { - graph.add_edge(t as i64, src, dst, &vec![], None).unwrap(); + graph.add_edge(t as i64, src, dst, NO_PROPS, None).unwrap(); } - let results: HashMap = unweighted_page_rank(&graph, 1000, Some(4), None, false) - .into_iter() - .collect(); + let results = unweighted_page_rank(&graph, 1000, Some(4), None, false); assert_eq_f64(results.get("1"), Some(&0.5), 3); assert_eq_f64(results.get("2"), Some(&0.5), 3); @@ -293,15 +294,13 @@ mod page_rank_tests { fn three_nodes_page_rank_one_dangling() { let edges = vec![(1, 2), (2, 1), (2, 3)]; - let graph = Graph::new(4); + let graph = Graph::new(); for (t, (src, dst)) in edges.into_iter().enumerate() { - graph.add_edge(t as i64, src, dst, &vec![], None).unwrap(); + graph.add_edge(t as i64, src, dst, NO_PROPS, None).unwrap(); } - let results: HashMap = unweighted_page_rank(&graph, 10, Some(4), None, false) - .into_iter() - .collect(); + let results = unweighted_page_rank(&graph, 10, Some(4), None, false); assert_eq_f64(results.get("1"), Some(&0.303), 3); assert_eq_f64(results.get("2"), Some(&0.393), 3); @@ -331,15 +330,13 @@ mod page_rank_tests { .map(|(t, (src, dst))| (src, dst, t as i64)) .collect_vec(); - let graph = Graph::new(4); + let graph = Graph::new(); for (src, dst, t) in edges { - graph.add_edge(t, src, dst, &vec![], None).unwrap(); + graph.add_edge(t, src, dst, NO_PROPS, None).unwrap(); } - let results: HashMap = unweighted_page_rank(&graph, 1000, Some(4), None, true) - .into_iter() - .collect(); + let results = unweighted_page_rank(&graph, 1000, Some(4), None, true); assert_eq_f64(results.get("1"), Some(&0.055), 3); assert_eq_f64(results.get("2"), Some(&0.079), 3); @@ -367,10 +364,7 @@ mod page_rank_tests { (Some(a), Some(b)) => { let left = (a.borrow() * factor).round(); let right = (b.borrow() * factor).round(); - assert_eq!( - left, - right, - ); + assert_eq!(left, right,); } _ => unreachable!(), } diff --git a/raphtory/src/algorithms/reciprocity.rs b/raphtory/src/algorithms/reciprocity.rs index 46bc7b8fa2..e03ca5b647 100644 --- a/raphtory/src/algorithms/reciprocity.rs +++ b/raphtory/src/algorithms/reciprocity.rs @@ -23,9 +23,8 @@ //! //! ```rust //! use raphtory::algorithms::reciprocity::{all_local_reciprocity, global_reciprocity}; -//! use raphtory::db::graph::Graph; -//! use raphtory::db::view_api::*; -//! let g = Graph::new(1); +//! use raphtory::prelude::*; +//! let g = Graph::new(); //! let vs = vec![ //! (1, 1, 2), //! (1, 1, 4), @@ -38,20 +37,30 @@ //! ]; //! //! for (t, src, dst) in &vs { -//! g.add_edge(*t, *src, *dst, &vec![], None); +//! g.add_edge(*t, *src, *dst, NO_PROPS, None).unwrap(); //! } //! //! println!("all_local_reciprocity: {:?}", all_local_reciprocity(&g, None)); //! println!("global_reciprocity: {:?}", global_reciprocity(&g, None)); //! ``` -use crate::core::state::accumulator_id::accumulators::sum; -use crate::core::state::compute_state::{ComputeState, ComputeStateVec}; -use crate::db::task::context::Context; -use crate::db::task::eval_vertex::EvalVertexView; -use crate::db::task::task::{ATask, Job, Step}; -use crate::db::task::task_runner::TaskRunner; -use crate::db::view_api::{GraphViewOps, VertexViewOps}; -use std::collections::{HashMap, HashSet}; +use crate::{ + algorithms::algorithm_result::AlgorithmResult, + core::state::{ + accumulator_id::accumulators::sum, + compute_state::{ComputeState, ComputeStateVec}, + }, + db::{ + api::view::{GraphViewOps, VertexViewOps}, + task::{ + context::Context, + task::{ATask, Job, Step}, + task_runner::TaskRunner, + vertex::eval_vertex::EvalVertexView, + }, + }, +}; +use ordered_float::OrderedFloat; +use std::collections::HashSet; /// Gets the unique edge counts excluding cycles for a vertex. Returns a tuple of usize /// (out neighbours, in neighbours, the intersection of the out and in neighbours) @@ -90,7 +99,7 @@ pub fn global_reciprocity(g: &G, threads: Option) -> f64 runner.run( vec![], vec![Job::new(step1)], - (), + None, |egs, _, _, _| { (egs.finalize(&total_out_inter_in) as f64) / (egs.finalize(&total_out_neighbours) as f64) @@ -103,17 +112,18 @@ pub fn global_reciprocity(g: &G, threads: Option) -> f64 } /// returns the reciprocity of every vertex in the graph as a tuple of +/// vector id and the reciprocity pub fn all_local_reciprocity( g: &G, threads: Option, -) -> HashMap { +) -> AlgorithmResult> { let mut ctx: Context = g.into(); let min = sum(0); ctx.agg(min); let step1 = ATask::new(move |evv| { - let edge_counts = get_reciprocal_edge_count(&evv); + let edge_counts = get_reciprocal_edge_count(evv); let res = (2.0 * edge_counts.2 as f64) / (edge_counts.1 as f64 + edge_counts.0 as f64); if res.is_nan() { evv.global_update(&min, 0.0); @@ -125,28 +135,31 @@ pub fn all_local_reciprocity( let mut runner: TaskRunner = TaskRunner::new(ctx); - runner.run( + AlgorithmResult::new(runner.run( vec![], vec![Job::new(step1)], - (), + None, |_, ess, _, _| ess.finalize(&min, |min| min), threads, 1, None, None, - ) + )) } #[cfg(test)] mod reciprocity_test { - use crate::algorithms::reciprocity::{all_local_reciprocity, global_reciprocity}; - use crate::db::graph::Graph; + use crate::{ + algorithms::reciprocity::{all_local_reciprocity, global_reciprocity}, + db::{api::mutation::AdditionOps, graph::graph::Graph}, + prelude::NO_PROPS, + }; use pretty_assertions::assert_eq; use std::collections::HashMap; #[test] fn test_global_recip() { - let graph = Graph::new(2); + let graph = Graph::new(); let vs = vec![ (1, 2), @@ -160,26 +173,20 @@ mod reciprocity_test { ]; for (src, dst) in &vs { - graph.add_edge(0, *src, *dst, &vec![], None).unwrap(); + graph.add_edge(0, *src, *dst, NO_PROPS, None).unwrap(); } let actual = global_reciprocity(&graph, None); assert_eq!(actual, 0.5); - let expected_vec: Vec<(String, f64)> = vec![ - ("1".to_string(), 0.4), - ("2".to_string(), 2.0 / 3.0), - ("3".to_string(), 0.5), - ("4".to_string(), 2.0 / 3.0), - ("5".to_string(), 0.0), - ]; - - let map_names_by_id: HashMap = expected_vec - .iter() - .map(|x| (x.0.to_string(), x.1)) - .collect(); + let mut hash_map_result: HashMap = HashMap::new(); + hash_map_result.insert("1".to_string(), 0.4); + hash_map_result.insert("2".to_string(), 2.0 / 3.0); + hash_map_result.insert("3".to_string(), 0.5); + hash_map_result.insert("4".to_string(), 2.0 / 3.0); + hash_map_result.insert("5".to_string(), 0.0); - let actual = all_local_reciprocity(&graph, None); - assert_eq!(actual, map_names_by_id); + let res = all_local_reciprocity(&graph, None); + assert_eq!(res.get("1"), hash_map_result.get("1")); } } diff --git a/raphtory/src/algorithms/generic_taint.rs b/raphtory/src/algorithms/temporal_reachability.rs similarity index 60% rename from raphtory/src/algorithms/generic_taint.rs rename to raphtory/src/algorithms/temporal_reachability.rs index 5ad7cbb043..971ddd91ab 100644 --- a/raphtory/src/algorithms/generic_taint.rs +++ b/raphtory/src/algorithms/temporal_reachability.rs @@ -1,14 +1,22 @@ -use crate::core::state::accumulator_id::accumulators::{hash_set, min, or}; -use crate::core::state::compute_state::ComputeStateVec; -use crate::core::vertex::InputVertex; -use crate::db::task::context::Context; -use crate::db::task::task::{ATask, Job, Step}; -use crate::db::task::task_runner::TaskRunner; -use crate::db::view_api::edge::EdgeViewOps; -use crate::db::view_api::{GraphViewOps, TimeOps, VertexViewOps}; +use crate::{ + algorithms::algorithm_result::AlgorithmResult, + core::{ + entities::vertices::input_vertex::InputVertex, + state::{ + accumulator_id::accumulators::{hash_set, min, or}, + compute_state::ComputeStateVec, + }, + }, + db::task::{ + context::Context, + task::{ATask, Job, Step}, + task_runner::TaskRunner, + vertex::eval_vertex::EvalVertexView, + }, + prelude::*, +}; use itertools::Itertools; use num_traits::Zero; -use std::collections::HashMap; use std::ops::Add; #[derive(Eq, Hash, PartialEq, Clone, Debug, Default)] @@ -46,18 +54,31 @@ impl Zero for TaintMessage { } } -pub fn generic_taint( +/// Temporal Reachability starts from a set of seed nodes and propagates the taint to all nodes that are reachable +/// from the seed nodes within a given time window. The algorithm stops when all nodes that are reachable from the +/// seed nodes have been tainted or when the taint has propagated to all nodes in the graph. +/// +/// Returns +/// +/// * An AlgorithmResult object containing the mapping from vertex ID to a vector of tuples containing the time at which +/// the vertex was tainted and the ID of the vertex that tainted it +/// +pub fn temporally_reachable_nodes( g: &G, threads: Option, - iter_count: usize, + max_hops: usize, start_time: i64, - infected_nodes: Vec, - stop_nodes: Vec, -) -> HashMap> { + seed_nodes: Vec, + stop_nodes: Option>, +) -> AlgorithmResult> { let mut ctx: Context = g.into(); - let infected_nodes = infected_nodes.into_iter().map(|n| n.id()).collect_vec(); - let stop_nodes = stop_nodes.into_iter().map(|n| n.id()).collect_vec(); + let infected_nodes = seed_nodes.into_iter().map(|n| n.id()).collect_vec(); + let stop_nodes = stop_nodes + .unwrap_or(vec![]) + .into_iter() + .map(|n| n.id()) + .collect_vec(); let taint_status = or(0); ctx.global_agg(taint_status); @@ -74,36 +95,38 @@ pub fn generic_taint( let tainted_vertices = hash_set::(4); ctx.global_agg(tainted_vertices); - let step1 = ATask::new(move |evv| { - if infected_nodes.contains(&evv.id()) { - evv.global_update(&tainted_vertices, evv.id()); - evv.update(&taint_status, true); - evv.update(&earliest_taint_time, start_time); - evv.update( - &taint_history, - TaintMessage { - event_time: start_time, - src_vertex: "start".to_string(), - }, - ); - evv.window(start_time, i64::MAX) - .out_edges() - .for_each(|eev| { - let dst = eev.dst(); - eev.history().into_iter().for_each(|t| { - dst.update(&earliest_taint_time, t); - dst.update( - &recv_tainted_msgs, - TaintMessage { - event_time: t, - src_vertex: evv.name(), - }, - ) + let step1 = ATask::new( + move |evv: &mut EvalVertexView<'_, G, ComputeStateVec, ()>| { + if infected_nodes.contains(&evv.id()) { + evv.global_update(&tainted_vertices, evv.id()); + evv.update(&taint_status, true); + evv.update(&earliest_taint_time, start_time); + evv.update( + &taint_history, + TaintMessage { + event_time: start_time, + src_vertex: "start".to_string(), + }, + ); + evv.window(start_time, i64::MAX) + .out_edges() + .for_each(|eev| { + let dst = eev.dst(); + eev.history().into_iter().for_each(|t| { + dst.update(&earliest_taint_time, t); + dst.update( + &recv_tainted_msgs, + TaintMessage { + event_time: t, + src_vertex: evv.name(), + }, + ) + }); }); - }); - } - Step::Continue - }); + } + Step::Continue + }, + ); let step2 = ATask::new(move |evv| { let msgs = evv.read(&recv_tainted_msgs); @@ -158,10 +181,10 @@ pub fn generic_taint( let mut runner: TaskRunner = TaskRunner::new(ctx); - runner.run( + AlgorithmResult::new(runner.run( vec![Job::new(step1)], vec![Job::new(step2), step3], - (), + None, |_, ess, _, _| { ess.finalize(&taint_history, |taint_history| { taint_history @@ -171,22 +194,22 @@ pub fn generic_taint( }) }, threads, - iter_count, + max_hops, None, None, - ) + )) } #[cfg(test)] mod generic_taint_tests { use super::*; - use crate::db::graph::Graph; + use crate::db::{api::mutation::AdditionOps, graph::graph::Graph}; - fn load_graph(n_shards: usize, edges: Vec<(i64, u64, u64)>) -> Graph { - let graph = Graph::new(n_shards); + fn load_graph(edges: Vec<(i64, u64, u64)>) -> Graph { + let graph = Graph::new(); for (t, src, dst) in edges { - graph.add_edge(t, src, dst, &vec![], None).unwrap(); + graph.add_edge(t, src, dst, NO_PROPS, None).unwrap(); } graph } @@ -196,9 +219,9 @@ mod generic_taint_tests { iter_count: usize, start_time: i64, infected_nodes: Vec, - stop_nodes: Vec, + stop_nodes: Option>, ) -> Vec<(String, Vec<(i64, String)>)> { - let mut results: Vec<(String, Vec<(i64, String)>)> = generic_taint( + let results: Vec<(String, Vec<(i64, String)>)> = temporally_reachable_nodes( &graph, None, iter_count, @@ -206,6 +229,7 @@ mod generic_taint_tests { infected_nodes, stop_nodes, ) + .sort_by_key(false) .into_iter() .map(|(k, mut v)| { v.sort(); @@ -213,29 +237,25 @@ mod generic_taint_tests { }) .collect_vec(); - results.sort(); results } #[test] fn test_generic_taint_1() { - let graph = load_graph( - 1, - vec![ - (10, 1, 3), - (11, 1, 2), - (12, 2, 4), - (13, 2, 5), - (14, 5, 5), - (14, 5, 4), - (5, 4, 6), - (15, 4, 7), - (10, 4, 7), - (10, 5, 8), - ], - ); - - let results = test_generic_taint(graph, 20, 11, vec![2], vec![]); + let graph = load_graph(vec![ + (10, 1, 3), + (11, 1, 2), + (12, 2, 4), + (13, 2, 5), + (14, 5, 5), + (14, 5, 4), + (5, 4, 6), + (15, 4, 7), + (10, 4, 7), + (10, 5, 8), + ]); + + let results = test_generic_taint(graph, 20, 11, vec![2], None); assert_eq!( results, @@ -253,29 +273,27 @@ mod generic_taint_tests { ), ("6".to_string(), vec![]), ("7".to_string(), vec![(15, "4".to_string())]), + ("8".to_string(), vec![]), ]) ); } #[test] fn test_generic_taint_1_multiple_start() { - let graph = load_graph( - 1, - vec![ - (10, 1, 3), - (11, 1, 2), - (12, 2, 4), - (13, 2, 5), - (14, 5, 5), - (14, 5, 4), - (5, 4, 6), - (15, 4, 7), - (10, 4, 7), - (10, 5, 8), - ], - ); - - let results = test_generic_taint(graph, 20, 11, vec![1, 2], vec![]); + let graph = load_graph(vec![ + (10, 1, 3), + (11, 1, 2), + (12, 2, 4), + (13, 2, 5), + (14, 5, 5), + (14, 5, 4), + (5, 4, 6), + (15, 4, 7), + (10, 4, 7), + (10, 5, 8), + ]); + + let results = test_generic_taint(graph, 20, 11, vec![1, 2], None); assert_eq!( results, @@ -296,29 +314,27 @@ mod generic_taint_tests { ), ("6".to_string(), vec![]), ("7".to_string(), vec![(15, "4".to_string())]), + ("8".to_string(), vec![]), ]) ); } #[test] fn test_generic_taint_1_stop_nodes() { - let graph = load_graph( - 1, - vec![ - (10, 1, 3), - (11, 1, 2), - (12, 2, 4), - (13, 2, 5), - (14, 5, 5), - (14, 5, 4), - (5, 4, 6), - (15, 4, 7), - (10, 4, 7), - (10, 5, 8), - ], - ); - - let results = test_generic_taint(graph, 20, 11, vec![1, 2], vec![4, 5]); + let graph = load_graph(vec![ + (10, 1, 3), + (11, 1, 2), + (12, 2, 4), + (13, 2, 5), + (14, 5, 5), + (14, 5, 4), + (5, 4, 6), + (15, 4, 7), + (10, 4, 7), + (10, 5, 8), + ]); + + let results = test_generic_taint(graph, 20, 11, vec![1, 2], Some(vec![4, 5])); assert_eq!( results, @@ -331,31 +347,31 @@ mod generic_taint_tests { ("3".to_string(), vec![]), ("4".to_string(), vec![(12, "2".to_string())]), ("5".to_string(), vec![(13, "2".to_string())]), + ("6".to_string(), vec![]), + ("7".to_string(), vec![]), + ("8".to_string(), vec![]), ]) ); } #[test] fn test_generic_taint_1_multiple_history_points() { - let graph = load_graph( - 1, - vec![ - (10, 1, 3), - (11, 1, 2), - (12, 1, 2), - (9, 1, 2), - (12, 2, 4), - (13, 2, 5), - (14, 5, 5), - (14, 5, 4), - (5, 4, 6), - (15, 4, 7), - (10, 4, 7), - (10, 5, 8), - ], - ); - - let results = test_generic_taint(graph, 20, 11, vec![1, 2], vec![4, 5]); + let graph = load_graph(vec![ + (10, 1, 3), + (11, 1, 2), + (12, 1, 2), + (9, 1, 2), + (12, 2, 4), + (13, 2, 5), + (14, 5, 5), + (14, 5, 4), + (5, 4, 6), + (15, 4, 7), + (10, 4, 7), + (10, 5, 8), + ]); + + let results = test_generic_taint(graph, 20, 11, vec![1, 2], Some(vec![4, 5])); assert_eq!( results, @@ -372,6 +388,9 @@ mod generic_taint_tests { ("3".to_string(), vec![]), ("4".to_string(), vec![(12, "2".to_string())]), ("5".to_string(), vec![(13, "2".to_string())]), + ("6".to_string(), vec![]), + ("7".to_string(), vec![]), + ("8".to_string(), vec![]), ]) ); } diff --git a/raphtory/src/algorithms/triangle_count.rs b/raphtory/src/algorithms/triangle_count.rs index e9748edf58..f7454c1680 100644 --- a/raphtory/src/algorithms/triangle_count.rs +++ b/raphtory/src/algorithms/triangle_count.rs @@ -1,9 +1,17 @@ -use crate::core::state::accumulator_id::accumulators; -use crate::core::state::compute_state::ComputeStateVec; -use crate::db::task::context::Context; -use crate::db::task::task::{ATask, Job, Step}; -use crate::db::task::task_runner::TaskRunner; -use crate::db::view_api::*; +use crate::{ + algorithms::k_core::k_core_set, + core::state::{accumulator_id::accumulators, compute_state::ComputeStateVec}, + db::{ + api::view::*, + graph::views::vertex_subgraph::VertexSubgraph, + task::{ + context::Context, + task::{ATask, Job, Step}, + task_runner::TaskRunner, + vertex::eval_vertex::EvalVertexView, + }, + }, +}; use rustc_hash::FxHashSet; /// Computes the number of triangles in a graph using a fast algorithm @@ -21,10 +29,10 @@ use rustc_hash::FxHashSet; /// # Example /// ```rust /// use std::{cmp::Reverse, iter::once}; -/// use raphtory::db::graph::Graph; /// use raphtory::algorithms::triangle_count::triangle_count; +/// use raphtory::prelude::*; /// -/// let graph = Graph::new(2); +/// let graph = Graph::new(); /// /// let edges = vec![ /// // triangle 1 @@ -44,29 +52,34 @@ use rustc_hash::FxHashSet; /// ]; /// /// for (src, dst, ts) in edges { -/// graph.add_edge(ts, src, dst, &vec![], None); +/// graph.add_edge(ts, src, dst, NO_PROPS, None); /// } /// /// let actual_tri_count = triangle_count(&graph, None); /// ``` /// -pub fn triangle_count(g: &G, threads: Option) -> usize { - let mut ctx: Context = g.into(); +pub fn triangle_count(graph: &G, threads: Option) -> usize { + let vertex_set = k_core_set(graph, 2, usize::MAX, None); + let g = graph.subgraph(vertex_set); + let mut ctx: Context, ComputeStateVec> = Context::from(&g); + // let mut ctx: Context = graph.into(); let neighbours_set = accumulators::hash_set::(0); let count = accumulators::sum::(1); ctx.agg(neighbours_set); ctx.global_agg(count); - let step1 = ATask::new(move |s| { - for t in s.neighbours() { - if s.id() > t.id() { - t.update(&neighbours_set, s.id()); + let step1 = ATask::new( + move |s: &mut EvalVertexView<'_, VertexSubgraph, ComputeStateVec, ()>| { + for t in s.neighbours() { + if s.id() > t.id() { + t.update(&neighbours_set, s.id()); + } } - } - Step::Continue - }); + Step::Continue + }, + ); let step2 = ATask::new(move |s| { for t in s.neighbours() { @@ -98,12 +111,12 @@ pub fn triangle_count(g: &G, threads: Option) -> usize { let init_tasks = vec![Job::new(step1)]; let tasks = vec![Job::new(step2)]; - let mut runner: TaskRunner = TaskRunner::new(ctx); + let mut runner: TaskRunner, _> = TaskRunner::new(ctx); runner.run( init_tasks, tasks, - (), + None, |egs, _, _, _| egs.finalize(&count), threads, 1, @@ -115,11 +128,14 @@ pub fn triangle_count(g: &G, threads: Option) -> usize { #[cfg(test)] mod triangle_count_tests { use super::*; - use crate::db::graph::Graph; + use crate::{ + db::{api::mutation::AdditionOps, graph::graph::Graph}, + prelude::NO_PROPS, + }; #[test] fn triangle_count_1() { - let graph = Graph::new(2); + let graph = Graph::new(); let edges = vec![ // triangle 1 @@ -139,7 +155,7 @@ mod triangle_count_tests { ]; for (src, dst, ts) in edges { - graph.add_edge(ts, src, dst, &vec![], None).unwrap(); + graph.add_edge(ts, src, dst, NO_PROPS, None).unwrap(); } let actual_tri_count = triangle_count(&graph, Some(2)); @@ -149,7 +165,7 @@ mod triangle_count_tests { #[test] fn triangle_count_3() { - let graph = Graph::new(2); + let graph = Graph::new(); let edges = vec![ (1, 2, 1), @@ -178,7 +194,7 @@ mod triangle_count_tests { ]; for (src, dst, ts) in edges { - graph.add_edge(ts, src, dst, &vec![], None).unwrap(); + graph.add_edge(ts, src, dst, NO_PROPS, None).unwrap(); } let actual_tri_count = triangle_count(&graph, None); diff --git a/raphtory/src/algorithms/triplet_count.rs b/raphtory/src/algorithms/triplet_count.rs index 695af8df14..5302dd59ec 100644 --- a/raphtory/src/algorithms/triplet_count.rs +++ b/raphtory/src/algorithms/triplet_count.rs @@ -14,10 +14,9 @@ //! # Example //! //! ```rust -//! use raphtory::db::graph::Graph; +//! use raphtory::prelude::*; //! use raphtory::algorithms::triplet_count::triplet_count; -//! use raphtory::db::view_api::*; -//! let graph = Graph::new(2); +//! let graph = Graph::new(); //! let edges = vec![ //! (1, 2), //! (1, 3), @@ -27,18 +26,24 @@ //! (2, 7), //! ]; //! for (src, dst) in edges { -//! graph.add_edge(0, src, dst, &vec![], None); +//! graph.add_edge(0, src, dst, NO_PROPS, None); //! } //! let results = triplet_count(&graph.at(1), None); //! println!("triplet count: {}", results); //! ``` //! -use crate::core::state::accumulator_id::accumulators::sum; -use crate::core::state::compute_state::ComputeStateVec; -use crate::db::task::context::Context; -use crate::db::task::task::{ATask, Job, Step}; -use crate::db::task::task_runner::TaskRunner; -use crate::db::view_api::{GraphViewOps, VertexViewOps}; +use crate::{ + core::state::{accumulator_id::accumulators::sum, compute_state::ComputeStateVec}, + db::{ + api::view::{GraphViewOps, VertexViewOps}, + task::{ + context::Context, + task::{ATask, Job, Step}, + task_runner::TaskRunner, + vertex::eval_vertex::EvalVertexView, + }, + }, +}; /// Computes the number of both open and closed triplets within a graph /// @@ -56,10 +61,9 @@ use crate::db::view_api::{GraphViewOps, VertexViewOps}; /// # Example /// /// ```rust -/// use raphtory::db::graph::Graph; /// use raphtory::algorithms::triplet_count::triplet_count; -/// use raphtory::db::view_api::*; -/// let graph = Graph::new(2); +/// use raphtory::prelude::*; +/// let graph = Graph::new(); /// let edges = vec![ /// (1, 2), /// (1, 3), @@ -69,7 +73,7 @@ use crate::db::view_api::{GraphViewOps, VertexViewOps}; /// (2, 7), /// ]; /// for (src, dst) in edges { -/// graph.add_edge(0, src, dst, &vec![], None); +/// graph.add_edge(0, src, dst, NO_PROPS, None); /// } /// /// let results = triplet_count(&graph.at(1), None); @@ -87,19 +91,21 @@ pub fn triplet_count(g: &G, threads: Option) -> usize { let count = sum::(0); ctx.global_agg(count); - let step1 = ATask::new(move |evv| { - let c1 = evv.neighbours().id().filter(|n| *n != evv.id()).count(); - let c2 = count_two_combinations(c1); - evv.global_update(&count, c2); - Step::Continue - }); + let step1 = ATask::new( + move |evv: &mut EvalVertexView<'_, G, ComputeStateVec, ()>| { + let c1 = evv.neighbours().id().filter(|n| *n != evv.id()).count(); + let c2 = count_two_combinations(c1); + evv.global_update(&count, c2); + Step::Continue + }, + ); let mut runner: TaskRunner = TaskRunner::new(ctx); runner.run( vec![], vec![Job::new(step1)], - (), + None, |egs, _, _, _| egs.finalize(&count), threads, 1, @@ -111,14 +117,19 @@ pub fn triplet_count(g: &G, threads: Option) -> usize { #[cfg(test)] mod triplet_test { use super::*; - use crate::db::graph::Graph; - use crate::db::view_api::*; + use crate::{ + db::{ + api::{mutation::AdditionOps, view::*}, + graph::graph::Graph, + }, + prelude::NO_PROPS, + }; use pretty_assertions::assert_eq; /// Test the global clustering coefficient #[test] fn test_triplet_count() { - let graph = Graph::new(1); + let graph = Graph::new(); // Graph has 2 triangles and 20 triplets let edges = vec![ @@ -145,7 +156,7 @@ mod triplet_test { ]; for (src, dst) in edges { - graph.add_edge(0, src, dst, &vec![], None).unwrap(); + graph.add_edge(0, src, dst, NO_PROPS, None).unwrap(); } let exp_triplet_count = 20; let results = triplet_count(&graph.at(1), None); diff --git a/raphtory/src/core/adj.rs b/raphtory/src/core/adj.rs deleted file mode 100644 index 844e901132..0000000000 --- a/raphtory/src/core/adj.rs +++ /dev/null @@ -1,77 +0,0 @@ -use crate::core::edge_layer::VID; -use crate::core::{tadjset::TAdjSet, Direction}; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Serialize, Deserialize, PartialEq, Default)] -pub(crate) enum Adj { - #[default] - Solo, - List { - // local: - out: TAdjSet, - into: TAdjSet, - // remote: - remote_out: TAdjSet, - remote_into: TAdjSet, - }, -} - -impl Adj { - pub(crate) fn get_edge(&self, v: VID, dir: Direction) -> Option { - match self { - Adj::Solo => None, - Adj::List { - out, - into, - remote_out, - remote_into, - } => match dir { - Direction::OUT => match v { - VID::Remote(v) => remote_out.find(v), - VID::Local(v) => out.find(v), - }, - Direction::IN => match v { - VID::Remote(v) => remote_into.find(v), - VID::Local(v) => into.find(v), - }, - Direction::BOTH => self - .get_edge(v, Direction::OUT) - .or_else(|| self.get_edge(v, Direction::IN)), - }, - } - } - - pub(crate) fn new_out(v: VID, e: usize) -> Self { - match v { - VID::Local(v) => Adj::List { - out: TAdjSet::new(v, e), - into: TAdjSet::default(), - remote_out: TAdjSet::default(), - remote_into: TAdjSet::default(), - }, - VID::Remote(v) => Adj::List { - out: TAdjSet::default(), - into: TAdjSet::default(), - remote_out: TAdjSet::new(v, e), - remote_into: TAdjSet::default(), - }, - } - } - - pub(crate) fn new_into(v: VID, e: usize) -> Self { - match v { - VID::Local(v) => Adj::List { - into: TAdjSet::new(v, e), - out: TAdjSet::default(), - remote_out: TAdjSet::default(), - remote_into: TAdjSet::default(), - }, - VID::Remote(v) => Adj::List { - out: TAdjSet::default(), - into: TAdjSet::default(), - remote_into: TAdjSet::new(v, e), - remote_out: TAdjSet::default(), - }, - } - } -} diff --git a/raphtory/src/core/edge_layer.rs b/raphtory/src/core/edge_layer.rs deleted file mode 100644 index 5e3b2293f5..0000000000 --- a/raphtory/src/core/edge_layer.rs +++ /dev/null @@ -1,753 +0,0 @@ -use itertools::chain; -use itertools::Itertools; -use rayon::prelude::*; -use serde::{Deserialize, Serialize}; -use std::iter; -use std::ops::Range; - -use crate::core::adj::Adj; -use crate::core::edge_ref::EdgeRef; -use crate::core::props::Props; -use crate::core::timeindex::TimeIndex; -use crate::core::{Direction, Prop}; - -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub(crate) enum VID { - Local(usize), - Remote(u64), -} - -impl From for VID { - fn from(value: u64) -> Self { - VID::Remote(value) - } -} - -impl From for VID { - fn from(value: usize) -> Self { - VID::Local(value) - } -} - -#[derive(Debug, Serialize, Deserialize, PartialEq)] -pub(crate) struct EdgeLayer { - layer_id: usize, - shard_id: usize, - local_timestamps: Vec, - remote_out_timestamps: Vec, - remote_into_timestamps: Vec, - - // Vector of adjacency lists. It is populated lazyly, so avoid using [] accessor for reading - adj_lists: Vec, - local_props: Props, - remote_out_props: Props, - remote_into_props: Props, -} - -impl EdgeLayer { - pub(crate) fn new(layer_id: usize, shard_id: usize) -> Self { - Self { - layer_id, - shard_id, - adj_lists: Default::default(), - local_props: Default::default(), - remote_out_props: Default::default(), - local_timestamps: Default::default(), - remote_out_timestamps: Default::default(), - remote_into_timestamps: Default::default(), - remote_into_props: Default::default(), - } - } - - fn new_local_out_edge_ref( - &self, - src_pid: usize, - dst_pid: usize, - e_pid: usize, - time: Option, - ) -> EdgeRef { - EdgeRef::LocalOut { - e_pid, - shard_id: self.shard_id, - layer_id: self.layer_id, - src_pid, - dst_pid, - time, - } - } - - fn new_local_into_edge_ref( - &self, - src_pid: usize, - dst_pid: usize, - e_pid: usize, - time: Option, - ) -> EdgeRef { - EdgeRef::LocalInto { - e_pid, - shard_id: self.shard_id, - layer_id: self.layer_id, - src_pid, - dst_pid, - time, - } - } - - fn new_remote_out_edge_ref( - &self, - src_pid: usize, - dst: u64, - e_pid: usize, - time: Option, - ) -> EdgeRef { - EdgeRef::RemoteOut { - e_pid, - shard_id: self.shard_id, - layer_id: self.layer_id, - src_pid, - dst, - time, - } - } - - fn new_remote_into_edge_ref( - &self, - src: u64, - dst_pid: usize, - e_pid: usize, - time: Option, - ) -> EdgeRef { - EdgeRef::RemoteInto { - e_pid, - shard_id: self.shard_id, - layer_id: self.layer_id, - src, - dst_pid, - time, - } - } -} - -// INGESTION: -impl EdgeLayer { - pub(crate) fn add_edge_with_props( - &mut self, - t: i64, - src_pid: usize, - dst_pid: usize, - props: &Vec<(String, Prop)>, - ) { - let required_len = std::cmp::max(src_pid, dst_pid) + 1; - let dst = VID::Local(dst_pid); - let src = VID::Local(src_pid); - self.ensure_adj_lists_len(required_len); - let edge_meta = self.get_edge_and_update_time(src_pid, dst, t, Direction::OUT); - self.link_outbound_edge(edge_meta, src_pid, dst); - self.link_inbound_edge(edge_meta, src, dst_pid); - self.local_props.upsert_temporal_props(t, edge_meta, props); - } - - #[allow(unused_variables)] - pub(crate) fn add_edge_remote_out( - &mut self, - t: i64, - src_pid: usize, - dst: u64, - props: &Vec<(String, Prop)>, - ) { - self.ensure_adj_lists_len(src_pid + 1); - let dst = VID::Remote(dst); - let edge_meta = self.get_edge_and_update_time(src_pid, dst, t, Direction::OUT); - self.link_outbound_edge(edge_meta, src_pid, dst); - self.remote_out_props - .upsert_temporal_props(t, edge_meta, props); - } - - #[allow(unused_variables)] - pub(crate) fn add_edge_remote_into( - &mut self, - t: i64, - src: u64, - dst_pid: usize, - props: &Vec<(String, Prop)>, - ) { - let src = VID::Remote(src); - self.ensure_adj_lists_len(dst_pid + 1); - let edge_meta = self.get_edge_and_update_time(dst_pid, src, t, Direction::IN); - self.link_inbound_edge(edge_meta, src, dst_pid); - self.remote_into_props - .upsert_temporal_props(t, edge_meta, props); - } - - pub(crate) fn edge_props_mut(&mut self, edge: EdgeRef) -> &mut Props { - match edge { - EdgeRef::RemoteInto { .. } => &mut self.remote_into_props, - EdgeRef::RemoteOut { .. } => &mut self.remote_out_props, - _ => &mut self.local_props, - } - } - - pub(crate) fn edge_props(&self, edge: EdgeRef) -> &Props { - match edge { - EdgeRef::RemoteInto { .. } => &self.remote_into_props, - EdgeRef::RemoteOut { .. } => &self.remote_out_props, - _ => &self.local_props, - } - } -} - -// INGESTION HELPERS: -impl EdgeLayer { - #[inline] - fn ensure_adj_lists_len(&mut self, len: usize) { - if self.adj_lists.len() < len { - self.adj_lists.resize_with(len, Default::default); - } - } - - #[inline] - fn get_adj(&self, v_pid: usize) -> &Adj { - self.adj_lists.get(v_pid).unwrap_or(&Adj::Solo) - } - - fn get_edge_and_update_time( - &mut self, - local_v: usize, - other: VID, - t: i64, - dir: Direction, - ) -> usize { - let timestamps = match other { - VID::Remote(_) => match dir { - Direction::IN => &mut self.remote_into_timestamps, - Direction::OUT => &mut self.remote_out_timestamps, - Direction::BOTH => { - panic!("Internal get_edge function should not be called with `Direction::BOTH`") - } - }, - VID::Local(_) => &mut self.local_timestamps, - }; - match self.adj_lists[local_v].get_edge(other, dir) { - Some(edge) => { - timestamps[edge].insert(t); - edge - } - None => { - let edge = timestamps.len(); - timestamps.push(TimeIndex::one(t)); - edge - } - } - } - - pub(crate) fn link_inbound_edge( - &mut self, - edge: usize, - src: VID, // may or may not be physical id depending on remote_edge flag - dst_pid: usize, - ) { - match &mut self.adj_lists[dst_pid] { - entry @ Adj::Solo => { - *entry = Adj::new_into(src, edge); - } - Adj::List { - into, remote_into, .. - } => match src { - VID::Remote(v) => remote_into.push(v, edge), - VID::Local(v) => into.push(v, edge), - }, - } - } - - pub(crate) fn link_outbound_edge( - &mut self, - edge: usize, - src_pid: usize, - dst: VID, // may or may not pe physical id depending on remote_edge flag - ) { - match &mut self.adj_lists[src_pid] { - entry @ Adj::Solo => { - *entry = Adj::new_out(dst, edge); - } - Adj::List { - out, remote_out, .. - } => match dst { - VID::Remote(v) => remote_out.push(v, edge), - VID::Local(v) => out.push(v, edge), - }, - } - } -} - -// SINGLE EDGE ACCESS: -impl EdgeLayer { - pub(crate) fn edge(&self, src: VID, dst: VID, w: Option>) -> Option { - match src { - VID::Local(src_pid) => { - let adj = self.get_adj(src_pid); - match adj { - Adj::Solo => None, - Adj::List { - out, remote_out, .. - } => match dst { - VID::Local(dst_pid) => { - let e = out.find(dst_pid).and_then(|e| match w { - Some(w) => self.local_timestamps[e].active(w).then_some(e), - None => Some(e), - })?; - Some(EdgeRef::LocalOut { - e_pid: e, - shard_id: self.shard_id, - layer_id: self.layer_id, - src_pid, - dst_pid, - time: None, - }) - } - VID::Remote(dst) => { - let e = remote_out.find(dst).and_then(|e| match w { - Some(w) => self.remote_out_timestamps[e].active(w).then_some(e), - None => Some(e), - })?; - Some(EdgeRef::RemoteOut { - e_pid: e, - shard_id: self.shard_id, - layer_id: self.layer_id, - src_pid, - dst, - time: None, - }) - } - }, - } - } - VID::Remote(src) => match dst { - VID::Local(dst_pid) => { - let adj = self.get_adj(dst_pid); - match adj { - Adj::Solo => None, - Adj::List { remote_into, .. } => { - let e = remote_into.find(src).filter(|e| match w { - Some(w) => self.remote_into_timestamps[*e].active(w), - None => true, - })?; - Some(EdgeRef::RemoteInto { - e_pid: e, - shard_id: self.shard_id, - layer_id: self.layer_id, - src, - dst_pid, - time: None, - }) - } - } - } - VID::Remote(_) => None, - }, - } - } - - pub(crate) fn has_edge(&self, src: VID, dst: VID, w: Option>) -> bool { - self.edge(src, dst, w).is_some() - } - - #[inline] - pub(crate) fn get_edge_history(&self, edge: EdgeRef) -> impl Iterator + '_ { - let timestamps = match edge { - EdgeRef::RemoteInto { e_pid, .. } => &self.remote_into_timestamps[e_pid], - EdgeRef::RemoteOut { e_pid, .. } => &self.remote_out_timestamps[e_pid], - local_edge => &self.local_timestamps[local_edge.pid()], - }; - timestamps.iter().copied() - } - - #[inline] - pub(crate) fn get_edge_history_window( - &self, - edge: EdgeRef, - w: Range, - ) -> impl Iterator + '_ { - let timestamps = match edge { - EdgeRef::RemoteInto { e_pid, .. } => &self.remote_into_timestamps[e_pid], - EdgeRef::RemoteOut { e_pid, .. } => &self.remote_out_timestamps[e_pid], - local_edge => &self.local_timestamps[local_edge.pid()], - }; - timestamps.range(w).copied() - } - - pub(crate) fn explode_edge(&self, edge: EdgeRef) -> impl Iterator + '_ { - self.get_edge_history(edge).map(move |t| edge.at(t)) - } - - pub(crate) fn explode_edge_window( - &self, - edge: EdgeRef, - w: Range, - ) -> impl Iterator + '_ { - self.get_edge_history_window(edge, w) - .map(move |t| edge.at(t)) - } -} - -// AGGREGATED ACCESS: -impl EdgeLayer { - pub(crate) fn out_edges_len(&self) -> usize { - self.local_timestamps.len() + self.remote_out_timestamps.len() - } - - pub(crate) fn out_edges_len_window(&self, w: &Range) -> usize { - self.local_timestamps - .par_iter() - .filter(|ts| ts.active(w.clone())) - .count() - + self - .remote_out_timestamps - .par_iter() - .filter(|ts| ts.active(w.clone())) - .count() - } -} - -// MULTIPLE EDGE ACCES: -impl EdgeLayer { - pub fn vertex_neighbours( - &self, - v_pid: usize, - d: Direction, - ) -> Box + Send + '_> { - let adj = self.get_adj(v_pid); - match adj { - Adj::Solo => { - let iter: Box + Send + '_> = Box::new(iter::empty()); - iter - } - Adj::List { - out, - into, - remote_out, - remote_into, - } => match d { - Direction::OUT => { - let iter: Box + Send + '_> = Box::new( - out.vertices() - .map_into() - .chain(remote_out.vertices().map_into()), - ); - iter - } - Direction::IN => { - let iter: Box + Send + '_> = Box::new( - into.vertices() - .map_into() - .chain(remote_into.vertices().map_into()), - ); - iter - } - Direction::BOTH => { - let iter: Box + Send + '_> = Box::new( - out.vertices() - .merge(into.vertices()) - .dedup() - .map_into() - .chain( - remote_out - .vertices() - .merge(remote_into.vertices()) - .dedup() - .map_into(), - ), - ); - iter - } - }, - } - } - - pub fn vertex_neighbours_window( - &self, - v_pid: usize, - d: Direction, - window: &Range, - ) -> Box + Send + '_> { - let adj = self.get_adj(v_pid); - match adj { - Adj::Solo => { - let iter: Box + Send + '_> = Box::new(iter::empty()); - iter - } - Adj::List { - out, - into, - remote_out, - remote_into, - } => match d { - Direction::OUT => { - let iter: Box + Send + '_> = Box::new( - out.vertices_window(&self.local_timestamps, window) - .map_into() - .chain( - remote_out - .vertices_window(&self.remote_out_timestamps, window) - .map_into(), - ), - ); - iter - } - Direction::IN => { - let iter: Box + Send + '_> = Box::new( - into.vertices_window(&self.local_timestamps, window) - .map_into() - .chain( - remote_into - .vertices_window(&self.remote_into_timestamps, window) - .map_into(), - ), - ); - iter - } - Direction::BOTH => { - let iter: Box + Send + '_> = Box::new( - out.vertices_window(&self.local_timestamps, window) - .merge(into.vertices_window(&self.local_timestamps, window)) - .dedup() - .map_into() - .chain( - remote_out - .vertices_window(&self.remote_out_timestamps, window) - .merge( - remote_into - .vertices_window(&self.remote_into_timestamps, window), - ) - .dedup() - .map_into(), - ), - ); - iter - } - }, - } - } - - pub fn degree(&self, v_pid: usize, d: Direction) -> usize { - let adj = self.get_adj(v_pid); - match adj { - Adj::Solo => 0, - Adj::List { - out, - into, - remote_out, - remote_into, - } => match d { - Direction::OUT => out.len() + remote_out.len(), - Direction::IN => into.len() + remote_into.len(), - Direction::BOTH => { - out.vertices().merge(into.vertices()).dedup().count() - + remote_out - .vertices() - .merge(remote_into.vertices()) - .dedup() - .count() - } - }, - } - } - - pub fn degree_window(&self, v_pid: usize, d: Direction, window: &Range) -> usize { - let adj = self.get_adj(v_pid); - match adj { - Adj::Solo => 0, - Adj::List { - out, - remote_out, - into, - remote_into, - } => match d { - Direction::OUT => { - out.len_window(&self.local_timestamps, window) - + remote_out.len_window(&self.remote_out_timestamps, window) - } - Direction::IN => { - into.len_window(&self.local_timestamps, window) - + remote_into.len_window(&self.remote_into_timestamps, window) - } - Direction::BOTH => { - out.vertices_window(&self.local_timestamps, window) - .merge(into.vertices_window(&self.local_timestamps, window)) - .dedup() - .count() - + remote_out - .vertices_window(&self.remote_out_timestamps, window) - .merge( - remote_into.vertices_window(&self.remote_into_timestamps, window), - ) - .dedup() - .count() - } - }, - } - } - - pub(crate) fn vertex_edges_iter( - &self, - v_pid: usize, - d: Direction, - ) -> Box + Send + '_> { - match self.get_adj(v_pid) { - Adj::List { - out, - into, - remote_out, - remote_into, - } => match d { - Direction::OUT => Box::new( - out.iter() - .map(move |(dst_pid, e)| { - self.new_local_out_edge_ref(v_pid, dst_pid, e, None) - }) - .chain(remote_out.iter().map(move |(dst, e)| { - self.new_remote_out_edge_ref(v_pid, dst, e, None) - })), - ), - Direction::IN => Box::new( - into.iter() - .map(move |(src_pid, e)| { - self.new_local_into_edge_ref(src_pid, v_pid, e, None) - }) - .chain(remote_into.iter().map(move |(src, e)| { - self.new_remote_into_edge_ref(src, v_pid, e, None) - })), - ), - - Direction::BOTH => { - let remote = remote_out - .iter() - .map(move |(dst, e)| { - (dst, self.new_remote_out_edge_ref(v_pid, dst, e, None)) - }) - .merge_by( - remote_into.iter().map(move |(src, e)| { - (src, self.new_remote_into_edge_ref(src, v_pid, e, None)) - }), - |(left, _), (right, _)| left < right, - ) - .map(|item| item.1); - - let local = out - .iter() - .map(move |(dst_pid, e)| { - ( - dst_pid, - self.new_local_out_edge_ref(v_pid, dst_pid, e, None), - ) - }) - .merge_by( - into.iter().map(move |(src_pid, e)| { - ( - src_pid, - self.new_local_into_edge_ref(src_pid, v_pid, e, None), - ) - }), - |(left, _), (right, _)| left < right, - ) - .map(|item| item.1); - Box::new(chain!(local, remote)) - } - }, - _ => Box::new(std::iter::empty()), - } - } - - pub(crate) fn vertex_edges_iter_window( - &self, - v_pid: usize, - r: &Range, - d: Direction, - ) -> Box + Send + '_> { - match self.get_adj(v_pid) { - Adj::List { - out, - into, - remote_out, - remote_into, - } => match d { - Direction::OUT => Box::new(chain!( - out.iter_window(&self.local_timestamps, r) - .map(move |(dst_pid, e)| self - .new_local_out_edge_ref(v_pid, dst_pid, e, None)), - remote_out - .iter_window(&self.remote_out_timestamps, r) - .map(move |(dst, e)| self.new_remote_out_edge_ref(v_pid, dst, e, None)) - )), - Direction::IN => { - let iter = chain!( - into.iter_window(&self.local_timestamps, r) - .map(move |(src_pid, e)| self - .new_local_into_edge_ref(src_pid, v_pid, e, None)), - remote_into - .iter_window(&self.remote_into_timestamps, r) - .map(move |(src, e)| self.new_remote_into_edge_ref(src, v_pid, e, None)) - ); - Box::new(iter) - } - Direction::BOTH => Box::new(chain!( - out.iter_window(&self.local_timestamps, r) - .map(move |(dst_pid, e)| ( - dst_pid, - self.new_local_out_edge_ref(v_pid, dst_pid, e, None) - )) - .merge_by( - into.iter_window(&self.local_timestamps, r) - .map(move |(src_pid, e)| ( - src_pid, - self.new_local_into_edge_ref(src_pid, v_pid, e, None) - )), - |left, right| left.0 < right.0 - ) - .map(|item| item.1), - remote_out - .iter_window(&self.remote_out_timestamps, r) - .map(move |(dst, e)| ( - dst, - self.new_remote_out_edge_ref(v_pid, dst, e, None) - )) - .merge_by( - remote_into - .iter_window(&self.remote_into_timestamps, r) - .map(move |(src, e)| ( - src, - self.new_remote_into_edge_ref(src, v_pid, e, None) - )), - |left, right| left.0 < right.0 - ) - .map(|item| item.1) - )), - }, - _ => Box::new(std::iter::empty()), - } - } - - pub(crate) fn vertex_edges_iter_t( - // TODO: change back to private if appropriate - &self, - v_pid: usize, - d: Direction, - ) -> Box + Send + '_> { - Box::new( - self.vertex_edges_iter(v_pid, d) - .flat_map(|e| self.explode_edge(e)), - ) - } - - pub(crate) fn vertex_edges_iter_window_t<'a>( - // TODO: change back to private if appropriate - &'a self, - v_pid: usize, - w: &'a Range, - d: Direction, - ) -> Box + Send + '_> { - Box::new( - self.vertex_edges_iter_window(v_pid, w, d) - .flat_map(|e| self.explode_edge_window(e, w.clone())), - ) - } -} diff --git a/raphtory/src/core/edge_ref.rs b/raphtory/src/core/edge_ref.rs deleted file mode 100644 index 0a91a3f6ed..0000000000 --- a/raphtory/src/core/edge_ref.rs +++ /dev/null @@ -1,210 +0,0 @@ -use crate::core::vertex_ref::VertexRef; - -#[derive(Debug, Copy, Clone, PartialEq)] -pub enum EdgeRef { - RemoteInto { - e_pid: usize, - shard_id: usize, - layer_id: usize, - src: u64, - dst_pid: usize, - time: Option, - }, - RemoteOut { - e_pid: usize, - shard_id: usize, - layer_id: usize, - src_pid: usize, - dst: u64, - time: Option, - }, - LocalInto { - e_pid: usize, - shard_id: usize, - layer_id: usize, - src_pid: usize, - dst_pid: usize, - time: Option, - }, - LocalOut { - e_pid: usize, - shard_id: usize, - layer_id: usize, - src_pid: usize, - dst_pid: usize, - time: Option, - }, -} - -impl EdgeRef { - #[inline(always)] - pub fn shard(&self) -> usize { - match &self { - EdgeRef::RemoteInto { shard_id, .. } => *shard_id, - EdgeRef::RemoteOut { shard_id, .. } => *shard_id, - EdgeRef::LocalInto { shard_id, .. } => *shard_id, - EdgeRef::LocalOut { shard_id, .. } => *shard_id, - } - } - - #[inline(always)] - pub fn layer(&self) -> usize { - match &self { - EdgeRef::RemoteInto { layer_id, .. } => *layer_id, - EdgeRef::RemoteOut { layer_id, .. } => *layer_id, - EdgeRef::LocalInto { layer_id, .. } => *layer_id, - EdgeRef::LocalOut { layer_id, .. } => *layer_id, - } - } - - #[inline(always)] - pub fn time(&self) -> Option { - match self { - EdgeRef::RemoteInto { time, .. } => *time, - EdgeRef::RemoteOut { time, .. } => *time, - EdgeRef::LocalInto { time, .. } => *time, - EdgeRef::LocalOut { time, .. } => *time, - } - } - - pub fn src(&self) -> VertexRef { - match self { - EdgeRef::RemoteInto { src, .. } => VertexRef::Remote(*src), - EdgeRef::RemoteOut { - src_pid, shard_id, .. - } => VertexRef::new_local(*src_pid, *shard_id), - EdgeRef::LocalInto { - src_pid, shard_id, .. - } => VertexRef::new_local(*src_pid, *shard_id), - EdgeRef::LocalOut { - src_pid, shard_id, .. - } => VertexRef::new_local(*src_pid, *shard_id), - } - } - - pub fn dst(&self) -> VertexRef { - match self { - EdgeRef::RemoteInto { - dst_pid, shard_id, .. - } => VertexRef::new_local(*dst_pid, *shard_id), - EdgeRef::RemoteOut { dst, .. } => VertexRef::Remote(*dst), - EdgeRef::LocalInto { - dst_pid, shard_id, .. - } => VertexRef::new_local(*dst_pid, *shard_id), - EdgeRef::LocalOut { - dst_pid, shard_id, .. - } => VertexRef::new_local(*dst_pid, *shard_id), - } - } - - pub fn remote(&self) -> VertexRef { - match self { - EdgeRef::RemoteInto { .. } => self.src(), - EdgeRef::RemoteOut { .. } => self.dst(), - EdgeRef::LocalInto { .. } => self.src(), - EdgeRef::LocalOut { .. } => self.dst(), - } - } - - pub fn local(&self) -> VertexRef { - match self { - EdgeRef::RemoteInto { .. } => self.dst(), - EdgeRef::RemoteOut { .. } => self.src(), - EdgeRef::LocalInto { .. } => self.dst(), - EdgeRef::LocalOut { .. } => self.src(), - } - } - - pub fn is_remote(&self) -> bool { - match self { - EdgeRef::RemoteInto { .. } => true, - EdgeRef::RemoteOut { .. } => true, - EdgeRef::LocalInto { .. } => false, - EdgeRef::LocalOut { .. } => false, - } - } - - pub fn is_local(&self) -> bool { - !self.is_remote() - } - - #[inline(always)] - pub(in crate::core) fn pid(&self) -> usize { - match self { - EdgeRef::RemoteInto { e_pid, .. } => *e_pid, - EdgeRef::RemoteOut { e_pid, .. } => *e_pid, - EdgeRef::LocalInto { e_pid, .. } => *e_pid, - EdgeRef::LocalOut { e_pid, .. } => *e_pid, - } - } - - pub(in crate::core) fn merge_cmp(&self, other: &EdgeRef) -> bool { - (self.local(), self.remote(), self.time(), self.layer()) - < (other.local(), other.remote(), other.time(), other.layer()) - } - - pub fn at(&self, time: i64) -> Self { - match *self { - EdgeRef::RemoteInto { - e_pid, - shard_id, - layer_id, - src, - dst_pid, - .. - } => EdgeRef::RemoteInto { - time: Some(time), - e_pid, - shard_id, - layer_id, - src, - dst_pid, - }, - EdgeRef::RemoteOut { - e_pid, - shard_id, - layer_id, - src_pid, - dst, - .. - } => EdgeRef::RemoteOut { - time: Some(time), - e_pid, - shard_id, - layer_id, - src_pid, - dst, - }, - EdgeRef::LocalInto { - e_pid, - shard_id, - layer_id, - src_pid, - dst_pid, - .. - } => EdgeRef::LocalInto { - time: Some(time), - e_pid, - shard_id, - layer_id, - src_pid, - dst_pid, - }, - EdgeRef::LocalOut { - e_pid, - shard_id, - layer_id, - src_pid, - dst_pid, - .. - } => EdgeRef::LocalOut { - time: Some(time), - e_pid, - shard_id, - layer_id, - src_pid, - dst_pid, - }, - } - } -} diff --git a/raphtory/src/core/entities/edges/edge.rs b/raphtory/src/core/entities/edges/edge.rs new file mode 100644 index 0000000000..620e4c031c --- /dev/null +++ b/raphtory/src/core/entities/edges/edge.rs @@ -0,0 +1,289 @@ +use crate::core::{ + entities::{ + edges::edge_store::EdgeStore, + graph::{ + tgraph::TGraph, + tgraph_storage::{GraphEntry, LockedGraphStorage}, + }, + properties::tprop::{LockedLayeredTProp, TProp}, + vertices::vertex::Vertex, + GraphItem, LayerIds, VRef, EID, VID, + }, + storage::{ + locked_view::LockedView, + timeindex::{LayeredIndex, TimeIndex, TimeIndexOps}, + Entry, + }, + Direction, Prop, +}; +// use crate::prelude::Layer::Default; +use crate::core::storage::timeindex::{LockedLayeredIndex, TimeIndexEntry}; +use std::{ + default::Default, + ops::{Deref, Range}, + sync::Arc, +}; + +#[derive(Debug)] +pub(crate) enum ERef<'a, const N: usize> { + ERef(Entry<'a, EdgeStore, N>), + ELock { + lock: Arc>, + eid: EID, + }, +} + +// impl fn edge_id for ERef +impl<'a, const N: usize> ERef<'a, N> { + pub(crate) fn edge_id(&self) -> EID { + match self { + ERef::ELock { lock: _, eid } => *eid, + ERef::ERef(es) => es.index().into(), + } + } + + fn vertex_ref(&self, src: VID) -> Option> { + match self { + ERef::ELock { lock, .. } => { + Some(VRef::LockedEntry(GraphEntry::new(lock.clone(), src.into()))) + } + _ => None, + } + } +} + +impl<'a, const N: usize> Deref for ERef<'a, N> { + type Target = EdgeStore; + + fn deref(&self) -> &Self::Target { + match self { + ERef::ERef(e) => e, + ERef::ELock { lock, eid } => lock.get_edge((*eid).into()), + } + } +} + +impl<'a, const N: usize> GraphItem<'a, N> for EdgeView<'a, N> { + fn from_edge_ids( + src: VID, + dst: VID, + e_id: ERef<'a, N>, + dir: Direction, + graph: &'a TGraph, + ) -> Self { + EdgeView::from_edge_ids(src, dst, e_id, dir, graph) + } +} +#[derive(Debug)] +pub struct EdgeView<'a, const N: usize> { + src: VID, + dst: VID, + edge_id: ERef<'a, N>, + dir: Direction, + graph: &'a TGraph, +} + +impl<'a, const N: usize> PartialEq for EdgeView<'a, N> { + fn eq(&self, other: &Self) -> bool { + self.edge_id.edge_id() == other.edge_id.edge_id() + && self.src == other.src + && self.dst == other.dst + } +} + +impl<'a, const N: usize> PartialOrd for EdgeView<'a, N> { + fn partial_cmp(&self, other: &Self) -> Option { + self.origin() + .eq(&other.origin()) + .then(|| self.neighbour().cmp(&other.neighbour())) + } +} + +impl<'a, const N: usize> EdgeView<'a, N> { + pub(crate) fn additions( + self, + layer_ids: LayerIds, + ) -> Option> { + match self.edge_id { + ERef::ERef(entry) => { + let t_index = entry.map(|entry| entry.additions()); + Some(LayeredIndex::new(layer_ids, t_index)) + } + _ => None, + } + } + + pub(crate) fn deletions( + self, + layer_ids: LayerIds, + ) -> Option> { + match self.edge_id { + ERef::ERef(entry) => { + let t_index = entry.map(|entry| entry.deletions()); + Some(LayeredIndex::new(layer_ids, t_index)) + } + _ => None, + } + } + + pub(crate) fn temporal_property( + self, + layer_ids: LayerIds, + prop_id: usize, + ) -> Option> { + match self.edge_id { + ERef::ERef(entry) => { + if entry.has_temporal_prop(&layer_ids, prop_id) { + match layer_ids { + LayerIds::None => None, + LayerIds::All => { + let props: Vec<_> = entry + .layer_ids_iter() + .flat_map(|id| { + entry.temporal_prop_layer(id, prop_id).is_some().then(|| { + entry + .clone() + .map(|e| e.temporal_prop_layer(id, prop_id).unwrap()) + }) + }) + .collect(); + Some(LockedLayeredTProp::new(props)) + } + LayerIds::One(id) => Some(LockedLayeredTProp::new(vec![entry.map(|e| { + e.temporal_prop_layer(id, prop_id) + .expect("already checked in the beginning") + })])), + LayerIds::Multiple(ids) => { + let props: Vec<_> = ids + .iter() + .flat_map(|&id| { + entry.temporal_prop_layer(id, prop_id).is_some().then(|| { + entry + .clone() + .map(|e| e.temporal_prop_layer(id, prop_id).unwrap()) + }) + }) + .collect(); + Some(LockedLayeredTProp::new(props)) + } + } + } else { + None + } + } + _ => None, + } + } + + fn neighbour(&self) -> VID { + match self.dir { + Direction::OUT => self.dst, + Direction::IN => self.src, + _ => panic!("Invalid direction"), // FIXME: perhaps we should have 2 enums for direction one strict and one not + } + } + + fn origin(&self) -> VID { + match self.dir { + Direction::OUT => self.src, + Direction::IN => self.dst, + _ => panic!("Invalid direction"), // FIXME: perhaps we should have 2 enums for direction one strict and one not + } + } + + pub fn src_id(&self) -> VID { + self.src + } + + pub fn dst_id(&self) -> VID { + self.dst + } + + pub fn edge_id(&self) -> EID { + self.edge_id.edge_id() + } + + pub fn src(&self) -> Vertex<'a, N> { + if let Some(v_ref) = self.edge_id.vertex_ref(self.src) { + Vertex::new(v_ref, self.graph) + } else { + self.graph.vertex(self.src) + } + } + + pub fn dst(&self) -> Vertex<'a, N> { + if let Some(v_ref) = self.edge_id.vertex_ref(self.dst) { + Vertex::new(v_ref, self.graph) + } else { + self.graph.vertex(self.dst) + } + } + + pub(crate) fn from_edge_ids( + v1: VID, // the initiator of the edges call + v2: VID, // the edge on the other side + edge_id: ERef<'a, N>, + dir: Direction, + graph: &'a TGraph, + ) -> Self { + let (src, dst) = match dir { + Direction::OUT => (v1, v2), + Direction::IN => (v2, v1), + _ => panic!("Invalid direction"), + }; + EdgeView { + src, + dst, + edge_id, + graph, + dir, + } + } + + pub(crate) fn from_entry(entry: Entry<'a, EdgeStore, N>, graph: &'a TGraph) -> Self { + Self { + src: entry.src().into(), + dst: entry.dst().into(), + edge_id: ERef::ERef(entry), + dir: Direction::OUT, + graph, + } + } + + pub(crate) fn active(&'a self, layer_ids: LayerIds, w: Range) -> bool { + match &self.edge_id { + ERef::ELock { lock, .. } => { + let e = lock.get_edge(self.edge_id().into()); + self.check_layers(layer_ids, e, |t| t.active(w.clone())) + } + ERef::ERef(entry) => { + let e = entry.deref(); + self.check_layers(layer_ids, e, |t| t.active(w.clone())) + } + } + } + + fn check_layers, F: Fn(&TimeIndex) -> bool>( + &self, + layer_ids: LayerIds, + e: E, + f: F, + ) -> bool { + match layer_ids { + LayerIds::All => e.additions().iter().any(f), + LayerIds::One(id) => f(&e.additions()[id]), + LayerIds::Multiple(ids) => ids.iter().any(|id| f(&e.additions()[*id])), + LayerIds::None => false, + } + } + + pub(crate) fn layer_ids(&self) -> LayerIds { + match &self.edge_id { + ERef::ELock { lock, .. } => { + let e = lock.get_edge(self.edge_id().into()); + e.layer_ids() + } + ERef::ERef(entry) => (*entry).layer_ids(), + } + } +} diff --git a/raphtory/src/core/entities/edges/edge_ref.rs b/raphtory/src/core/entities/edges/edge_ref.rs new file mode 100644 index 0000000000..dcb42274c4 --- /dev/null +++ b/raphtory/src/core/entities/edges/edge_ref.rs @@ -0,0 +1,142 @@ +use crate::core::{ + entities::{vertices::vertex_ref::VertexRef, EID, VID}, + storage::timeindex::{AsTime, TimeIndexEntry}, +}; +use std::cmp::Ordering; + +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct EdgeRef { + e_pid: EID, + src_pid: VID, + dst_pid: VID, + e_type: Dir, + time: Option, + layer_id: Option, +} + +// This is used for merging iterators of EdgeRefs and only makes sense if the local vertex for both +// sides is the same +impl PartialOrd for EdgeRef { + fn partial_cmp(&self, other: &Self) -> Option { + self.remote().partial_cmp(&other.remote()) + } +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum Dir { + Into, + Out, +} + +impl EdgeRef { + #[inline] + pub fn new_outgoing(e_pid: EID, src_pid: VID, dst_pid: VID) -> Self { + EdgeRef { + e_pid, + src_pid, + dst_pid, + e_type: Dir::Out, + time: None, + layer_id: None, + } + } + + #[inline] + pub fn new_incoming(e_pid: EID, src_pid: VID, dst_pid: VID) -> Self { + EdgeRef { + e_pid, + src_pid, + dst_pid, + e_type: Dir::Into, + time: None, + layer_id: None, + } + } + + #[inline] + pub fn new(e_pid: EID, local_pid: VID, remote_pid: VID, dir: Dir) -> Self { + match dir { + Dir::Out => EdgeRef { + e_pid, + src_pid: local_pid, + dst_pid: remote_pid, + e_type: dir, + time: None, + layer_id: None, + }, + Dir::Into => EdgeRef { + e_pid, + src_pid: remote_pid, + dst_pid: local_pid, + e_type: dir, + time: None, + layer_id: None, + }, + } + } + + #[inline(always)] + pub fn layer(&self) -> Option<&usize> { + self.layer_id.as_ref() + } + + #[inline(always)] + pub fn time(&self) -> Option { + self.time + } + + #[inline(always)] + pub fn time_t(&self) -> Option { + self.time.map(|t| *t.t()) + } + + #[inline] + pub fn dir(&self) -> Dir { + self.e_type + } + + #[inline] + pub fn src(&self) -> VID { + self.src_pid + } + + #[inline] + pub fn dst(&self) -> VID { + self.dst_pid + } + + #[inline] + pub fn remote(&self) -> VID { + match self.e_type { + Dir::Into => self.src(), + Dir::Out => self.dst(), + } + } + + #[inline] + pub fn local(&self) -> VID { + match self.e_type { + Dir::Into => self.dst(), + Dir::Out => self.src(), + } + } + + #[inline(always)] + pub(crate) fn pid(&self) -> EID { + self.e_pid + } + + #[inline] + pub fn at(&self, time: TimeIndexEntry) -> Self { + let mut e_ref = *self; + e_ref.time = Some(time); + e_ref + } + + #[inline] + pub fn at_layer(&self, layer: usize) -> Self { + let mut e_ref = *self; + e_ref.layer_id = Some(layer); + e_ref + } +} diff --git a/raphtory/src/core/entities/edges/edge_store.rs b/raphtory/src/core/entities/edges/edge_store.rs new file mode 100644 index 0000000000..3453ad31c0 --- /dev/null +++ b/raphtory/src/core/entities/edges/edge_store.rs @@ -0,0 +1,425 @@ +use crate::{ + core::{ + entities::{ + edges::edge_ref::EdgeRef, + properties::{props::Props, tprop::TProp}, + LayerIds, EID, VID, + }, + storage::{ + lazy_vec::IllegalSet, + locked_view::LockedView, + timeindex::{TimeIndex, TimeIndexEntry, TimeIndexOps}, + }, + utils::errors::{GraphError, MutateGraphError}, + Prop, + }, + prelude::TimeOps, +}; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use std::ops::{Deref, DerefMut, Range}; +use tantivy::HasLen; + +#[derive(Serialize, Deserialize, Debug, Default, PartialEq)] +pub struct EdgeStore { + pub(crate) eid: EID, + src: VID, + dst: VID, + layers: Vec, // each layer has its own set of properties + additions: Vec>, + deletions: Vec>, +} + +#[derive(Serialize, Deserialize, Debug, Default, PartialEq)] +pub struct EdgeLayer { + props: Option, // memory optimisation: only allocate props if needed +} + +impl EdgeLayer { + pub fn props(&self) -> Option<&Props> { + self.props.as_ref() + } + + pub fn add_prop( + &mut self, + t: TimeIndexEntry, + prop_id: usize, + prop: Prop, + ) -> Result<(), GraphError> { + let props = self.props.get_or_insert_with(|| Props::new()); + props.add_prop(t, prop_id, prop) + } + + pub fn add_constant_prop( + &mut self, + prop_id: usize, + prop: Prop, + ) -> Result<(), IllegalSet>> { + let props = self.props.get_or_insert_with(|| Props::new()); + props.add_constant_prop(prop_id, prop) + } + + pub(crate) fn const_prop_ids(&self) -> impl Iterator + '_ { + self.props + .as_ref() + .into_iter() + .flat_map(|props| props.const_prop_ids()) + } + + pub(crate) fn const_prop(&self, prop_id: usize) -> Option<&Prop> { + self.props.as_ref().and_then(|ps| ps.const_prop(prop_id)) + } + + pub(crate) fn temporal_property(&self, prop_id: usize) -> Option<&TProp> { + self.props.as_ref().and_then(|ps| ps.temporal_prop(prop_id)) + } + + pub(crate) fn temporal_properties<'a>( + &'a self, + prop_id: usize, + window: Option>, + ) -> Box + 'a> { + if let Some(window) = window { + self.props + .as_ref() + .map(|props| props.temporal_props_window(prop_id, window.start, window.end)) + .unwrap_or_else(|| Box::new(std::iter::empty())) + } else { + self.props + .as_ref() + .map(|props| props.temporal_props(prop_id)) + .unwrap_or_else(|| Box::new(std::iter::empty())) + } + } +} + +impl> From for EdgeRef { + fn from(val: E) -> Self { + EdgeRef::new_outgoing(val.e_id(), val.src(), val.dst()) + } +} + +impl EdgeStore { + fn get_or_allocate_layer(&mut self, layer_id: usize) -> &mut EdgeLayer { + if self.layers.len() <= layer_id { + self.layers.resize_with(layer_id + 1, Default::default); + } + &mut self.layers[layer_id] + } + + pub fn has_layer(&self, layers: &LayerIds) -> bool { + match layers { + LayerIds::All => true, + LayerIds::One(layer_ids) => { + self.additions + .get(*layer_ids) + .filter(|t_index| !t_index.is_empty()) + .is_some() + || self + .deletions + .get(*layer_ids) + .filter(|t_index| !t_index.is_empty()) + .is_some() + } + LayerIds::Multiple(layer_ids) => layer_ids + .iter() + .any(|layer_id| self.has_layer(&LayerIds::One(*layer_id))), + LayerIds::None => false, + } + } + + // an edge is in a layer if it has either deletions or additions in that layer + pub fn layer_ids(&self) -> LayerIds { + let layer_ids = self.layer_ids_iter().collect::>(); + if layer_ids.len() == 1 { + LayerIds::One(layer_ids[0]) + } else { + LayerIds::Multiple(layer_ids.into()) + } + } + + pub fn layer_iter(&self) -> impl Iterator + '_ { + self.layers.iter() + } + pub fn layer_ids_iter(&self) -> impl Iterator + '_ { + let layer_ids = self + .additions + .iter() + .enumerate() + .zip_longest(self.deletions.iter().enumerate()) + .flat_map(|e| match e { + itertools::EitherOrBoth::Both((i, t1), (_, t2)) => { + if !t1.is_empty() || !t2.is_empty() { + Some(i) + } else { + None + } + } + itertools::EitherOrBoth::Left((i, t)) => { + if !t.is_empty() { + Some(i) + } else { + None + } + } + itertools::EitherOrBoth::Right((i, t)) => { + if !t.is_empty() { + Some(i) + } else { + None + } + } + }); + layer_ids + } + + pub fn layer_ids_window_iter(&self, w: Range) -> impl Iterator + '_ { + let layer_ids = self + .additions + .iter() + .enumerate() + .zip_longest(self.deletions.iter().enumerate()) + .flat_map(move |e| match e { + itertools::EitherOrBoth::Both((i, t1), (_, t2)) => { + if t1.contains(w.clone()) || t2.contains(w.clone()) { + Some(i) + } else { + None + } + } + itertools::EitherOrBoth::Left((i, t)) => { + if t.contains(w.clone()) { + Some(i) + } else { + None + } + } + itertools::EitherOrBoth::Right((i, t)) => { + if t.contains(w.clone()) { + Some(i) + } else { + None + } + } + }); + + layer_ids + } + + pub fn new(src: VID, dst: VID) -> Self { + Self { + eid: 0.into(), + src, + dst, + layers: Vec::with_capacity(1), + additions: Vec::with_capacity(1), + deletions: Vec::with_capacity(1), + } + } + + pub fn layer(&self, layer_id: usize) -> Option<&EdgeLayer> { + self.layers.get(layer_id) + } + + pub fn additions(&self) -> &Vec> { + &self.additions + } + + pub fn deletions(&self) -> &Vec> { + &self.deletions + } + + /// an edge is active in a window if it has an addition event in any of the layers + pub fn active(&self, layer_ids: &LayerIds, w: Range) -> bool { + match layer_ids { + LayerIds::None => false, + LayerIds::All => self + .additions() + .iter() + .any(|t_index| t_index.contains(w.clone())), + LayerIds::One(l_id) => self + .additions() + .get(*l_id) + .map(|t_index| t_index.contains(w)) + .unwrap_or(false), + LayerIds::Multiple(layers) => layers + .iter() + .any(|l_id| self.active(&LayerIds::One(*l_id), w.clone())), + } + } + + pub fn last_deletion(&self, layer_ids: &LayerIds) -> Option<&TimeIndexEntry> { + match layer_ids { + LayerIds::None => None, + LayerIds::All => self.deletions().iter().flat_map(|d| d.last()).max(), + LayerIds::One(id) => self.deletions.get(*id).and_then(|t| t.last()), + LayerIds::Multiple(ids) => ids + .iter() + .flat_map(|id| self.deletions.get(*id).and_then(|t| t.last())) + .max(), + } + } + + pub fn last_addition(&self, layer_ids: &LayerIds) -> Option<&TimeIndexEntry> { + match layer_ids { + LayerIds::None => None, + LayerIds::All => self.additions().iter().flat_map(|d| d.last()).max(), + LayerIds::One(id) => self.additions.get(*id).and_then(|t| t.last()), + LayerIds::Multiple(ids) => ids + .iter() + .flat_map(|id| self.additions.get(*id).and_then(|t| t.last())) + .max(), + } + } + + pub fn last_deletion_before(&self, layer_ids: &LayerIds, t: i64) -> Option { + match layer_ids { + LayerIds::None => None, + LayerIds::All => self + .deletions() + .iter() + .flat_map(|dels| dels.range(i64::MIN..t).last_t()) + .max(), + LayerIds::One(id) => { + let layer = self.deletions.get(*id)?; + layer.range(i64::MIN..t).last_t() + } + LayerIds::Multiple(ids) => ids + .iter() + .flat_map(|id| { + self.deletions + .get(*id) + .and_then(|t_index| t_index.range(i64::MIN..t).last_t()) + }) + .max(), + } + } + + pub fn has_temporal_prop(&self, layer_ids: &LayerIds, prop_id: usize) -> bool { + match layer_ids { + LayerIds::None => false, + LayerIds::All => self.layer_ids_iter().any(|id| { + self.layer(id) + .and_then(|layer| layer.temporal_property(prop_id)) + .is_some() + }), + LayerIds::One(id) => self + .layer(*id) + .and_then(|layer| layer.temporal_property(prop_id)) + .is_some(), + LayerIds::Multiple(ids) => ids.iter().any(|id| { + self.layer(*id) + .and_then(|layer| layer.temporal_property(prop_id)) + .is_some() + }), + } + } + + pub fn has_temporal_prop_window( + &self, + layer_ids: LayerIds, + prop_id: usize, + w: Range, + ) -> bool { + match layer_ids { + LayerIds::None => false, + LayerIds::All => self.layer_ids_iter().any(|id| { + self.layer(id) + .and_then(|layer| { + layer + .temporal_property(prop_id) + .filter(|p| p.iter_window(w.clone()).next().is_some()) + }) + .is_some() + }), + LayerIds::One(id) => self + .layer(id) + .and_then(|layer| { + layer + .temporal_property(prop_id) + .filter(|p| p.iter_window(w.clone()).next().is_some()) + }) + .is_some(), + LayerIds::Multiple(ids) => ids.iter().any(|id| { + self.layer(*id) + .and_then(|layer| { + layer + .temporal_property(prop_id) + .filter(|p| p.iter_window(w.clone()).next().is_some()) + }) + .is_some() + }), + } + } + + pub fn temporal_prop_layer(&self, layer_id: usize, prop_id: usize) -> Option<&TProp> { + self.layers + .get(layer_id) + .and_then(|layer| layer.temporal_property(prop_id)) + } + + pub fn layer_mut(&mut self, layer_id: usize) -> impl DerefMut + '_ { + self.get_or_allocate_layer(layer_id) + } + + pub fn deletions_mut(&mut self, layer_id: usize) -> &mut TimeIndex { + if self.deletions.len() <= layer_id { + self.deletions.resize_with(layer_id + 1, Default::default); + } + &mut self.deletions[layer_id] + } + + pub fn additions_mut(&mut self, layer_id: usize) -> &mut TimeIndex { + if self.additions.len() <= layer_id { + self.additions.resize_with(layer_id + 1, Default::default); + } + &mut self.additions[layer_id] + } + + pub fn src(&self) -> VID { + self.src + } + + pub fn dst(&self) -> VID { + self.dst + } + + pub fn e_id(&self) -> EID { + self.eid + } + + pub(crate) fn props(&self, layer_id: Option) -> Box + '_> { + if let Some(layer_id) = layer_id { + let iter = self + .layers + .get(layer_id) + .into_iter() + .flat_map(|layer| layer.props()); + Box::new(iter) + } else { + Box::new(self.layers.iter().flat_map(|layer| layer.props())) + } + } + + pub(crate) fn temp_prop_ids( + &self, + layer_id: Option, + ) -> Box + '_> { + if let Some(layer_id) = layer_id { + Box::new(self.layers.get(layer_id).into_iter().flat_map(|layer| { + layer + .props() + .into_iter() + .flat_map(|props| props.temporal_prop_ids()) + })) + } else { + Box::new( + self.layers + .iter() + .flat_map(|layer| layer.props().map(|prop| prop.temporal_prop_ids())) + .kmerge() + .dedup(), + ) + } + } +} diff --git a/raphtory/src/core/entities/edges/mod.rs b/raphtory/src/core/entities/edges/mod.rs new file mode 100644 index 0000000000..28c344e2dd --- /dev/null +++ b/raphtory/src/core/entities/edges/mod.rs @@ -0,0 +1,3 @@ +pub(crate) mod edge; +pub mod edge_ref; +pub mod edge_store; diff --git a/raphtory/src/core/entities/graph/mod.rs b/raphtory/src/core/entities/graph/mod.rs new file mode 100644 index 0000000000..42c9d80327 --- /dev/null +++ b/raphtory/src/core/entities/graph/mod.rs @@ -0,0 +1,77 @@ +pub mod tgraph; +pub mod tgraph_storage; +pub(crate) mod timer; + +#[cfg(test)] +mod test { + use crate::{ + core::{Direction, PropType}, + db::api::mutation::internal::InternalAdditionOps, + prelude::{IntoProp, Prop, NO_PROPS}, + }; + + use super::{tgraph::InnerTemporalGraph, *}; + + #[test] + fn test_neighbours_multiple_layers() { + let g: InnerTemporalGraph<2> = InnerTemporalGraph::default(); + let l_btc = g.resolve_layer(Some("btc")); + let l_eth = g.resolve_layer(Some("eth")); + let l_tether = g.resolve_layer(Some("tether")); + let v1 = g.resolve_vertex(1, None); + let v2 = g.resolve_vertex(2, None); + let tx_sent_id = g + .resolve_edge_property("tx_sent", PropType::I32, false) + .unwrap(); + g.inner() + .add_edge_internal(1.into(), v1, v2, vec![(tx_sent_id, Prop::I32(10))], l_btc); + g.inner() + .add_edge_internal(1.into(), v1, v2, vec![(tx_sent_id, Prop::I32(20))], l_eth); + g.inner().add_edge_internal( + 1.into(), + v1, + v2, + vec![(tx_sent_id, Prop::I32(70))], + l_tether, + ); + + let first = g.inner().vertex(0.into()); + + let ns = first + .neighbours(vec!["btc", "eth"], Direction::OUT) + .map(|v| v.id().0) + .collect::>(); + + assert_eq!(ns, vec![1]); + + let first = g.inner().vertex_arc(0.into()); + let edges = first + .edge_tuples([0, 1, 2, 3, 4].into(), Direction::OUT) + .collect::>(); + + assert_eq!(edges.len(), 1, "should only have one edge {:?}", edges); + } + + #[test] + fn simple_triangle() { + let g: InnerTemporalGraph<2> = InnerTemporalGraph::default(); + let v1 = g.resolve_vertex(1, None); + let v2 = g.resolve_vertex(2, None); + let v3 = g.resolve_vertex(3, None); + let vs = vec![(1, v1, v2), (2, v1, v3), (3, v2, v1), (4, v3, v2)]; + + let empty: Vec<(usize, Prop)> = vec![]; + for (t, src, dst) in vs { + g.inner() + .add_edge_internal(t.into(), src, dst, empty.clone(), 0); + } + + let v = g.inner().vertex(0.into()); + + let ns = v + .neighbours(vec![], Direction::BOTH) + .map(|v| v.id().0) + .collect::>(); + assert_eq!(ns, vec![1, 2]); + } +} diff --git a/raphtory/src/core/entities/graph/tgraph.rs b/raphtory/src/core/entities/graph/tgraph.rs new file mode 100644 index 0000000000..60109f3666 --- /dev/null +++ b/raphtory/src/core/entities/graph/tgraph.rs @@ -0,0 +1,555 @@ +use crate::{ + core::{ + entities::{ + edges::{ + edge::EdgeView, + edge_ref::EdgeRef, + edge_store::{EdgeLayer, EdgeStore}, + }, + graph::{ + tgraph_storage::{GraphStorage, LockedIter}, + timer::{MaxCounter, MinCounter, TimeCounterTrait}, + }, + properties::{ + graph_props::GraphProps, + props::{ArcReadLockedVec, Meta}, + tprop::TProp, + }, + vertices::{ + input_vertex::InputVertex, + vertex::{ArcEdge, ArcVertex, Vertex}, + vertex_ref::VertexRef, + vertex_store::VertexStore, + }, + LayerIds, EID, VID, + }, + storage::{ + lazy_vec::IllegalSet, + locked_view::LockedView, + timeindex::{AsTime, LayeredIndex, TimeIndexEntry, TimeIndexOps}, + ArcEntry, Entry, EntryMut, + }, + utils::{ + errors::{GraphError, IllegalMutate, MutateGraphError}, + time::TryIntoTime, + }, + ArcStr, Direction, Prop, PropUnwrap, + }, + db::api::view::{internal::EdgeFilter, BoxedIter, Layer}, +}; +use dashmap::{DashMap, DashSet}; +use itertools::Itertools; +use parking_lot::RwLockReadGuard; +use rayon::prelude::*; +use rustc_hash::FxHasher; +use serde::{Deserialize, Serialize}; +use std::{ + fmt::Debug, + hash::BuildHasherDefault, + iter, + ops::{Deref, Range}, + path::Path, + sync::{atomic::AtomicUsize, Arc}, +}; + +pub(crate) type FxDashMap = DashMap>; +pub(crate) type FxDashSet = DashSet>; + +pub(crate) type TGraph = TemporalGraph; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct InnerTemporalGraph(Arc>); + +impl InnerTemporalGraph { + #[inline] + pub(crate) fn inner(&self) -> &TemporalGraph { + &self.0 + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct TemporalGraph { + // mapping between logical and physical ids + logical_to_physical: FxDashMap, + string_pool: FxDashSet, + + pub(crate) storage: GraphStorage, + + pub(crate) event_counter: AtomicUsize, + + //earliest time seen in this graph + pub(in crate::core) earliest_time: MinCounter, + + //latest time seen in this graph + pub(in crate::core) latest_time: MaxCounter, + + // props meta data for vertices (mapping between strings and ids) + pub(crate) vertex_meta: Arc, + + // props meta data for edges (mapping between strings and ids) + pub(crate) edge_meta: Arc, + + // graph properties + pub(crate) graph_props: GraphProps, +} + +impl std::fmt::Display for InnerTemporalGraph { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Graph(num_vertices={}, num_edges={})", + self.inner().storage.nodes.len(), + self.inner().storage.edges.len() + ) + } +} + +impl Default for InnerTemporalGraph { + fn default() -> Self { + let tg = TemporalGraph { + logical_to_physical: FxDashMap::default(), // TODO: could use DictMapper here + string_pool: Default::default(), + storage: GraphStorage::new(), + event_counter: AtomicUsize::new(0), + earliest_time: MinCounter::new(), + latest_time: MaxCounter::new(), + vertex_meta: Arc::new(Meta::new()), + edge_meta: Arc::new(Meta::new()), + graph_props: GraphProps::new(), + }; + + Self(Arc::new(tg)) + } +} + +impl TemporalGraph { + pub(crate) fn num_layers(&self) -> usize { + self.edge_meta.layer_meta().len() + } + + pub(crate) fn layer_names(&self, layer_ids: LayerIds) -> BoxedIter { + match layer_ids { + LayerIds::None => Box::new(iter::empty()), + LayerIds::All => Box::new(self.edge_meta.layer_meta().get_keys().into_iter()), + LayerIds::One(id) => { + let name = self.edge_meta.layer_meta().get_name(id).clone(); + Box::new(iter::once(name)) + } + LayerIds::Multiple(ids) => { + let keys = self.edge_meta.layer_meta().get_keys(); + Box::new((0..ids.len()).map(move |index| { + let id = ids[index]; + keys[id].clone() + })) + } + } + } + + fn as_local_vertex(&self, v: VertexRef) -> Result { + match v { + VertexRef::Internal(vid) => Ok(vid), + VertexRef::External(gid) => self + .logical_to_physical + .get(&gid) + .map(|entry| *entry) + .ok_or(GraphError::FailedToMutateGraph { + source: MutateGraphError::VertexNotFoundError { vertex_id: gid }, + }), + } + } + + pub(crate) fn get_all_vertex_property_names( + &self, + is_static: bool, + ) -> ArcReadLockedVec { + self.vertex_meta.get_all_property_names(is_static) + } + + pub(crate) fn get_all_edge_property_names(&self, is_static: bool) -> ArcReadLockedVec { + self.edge_meta.get_all_property_names(is_static) + } + + pub(crate) fn get_all_layers(&self) -> Vec { + self.edge_meta.get_all_layers() + } + + pub(crate) fn layer_id(&self, key: Layer) -> LayerIds { + match key { + Layer::All => LayerIds::All, + Layer::Default => LayerIds::One(0), + Layer::One(id) => match self.edge_meta.get_layer_id(&id) { + Some(id) => LayerIds::One(id), + None => LayerIds::None, + }, + Layer::Multiple(ids) => { + let mut new_layers = ids + .iter() + .filter_map(|id| self.edge_meta.get_layer_id(id)) + .collect::>(); + let num_layers = self.num_layers(); + let num_new_layers = new_layers.len(); + if num_new_layers == 0 { + LayerIds::None + } else if num_new_layers == 1 { + LayerIds::One(new_layers[0]) + } else if num_new_layers == num_layers { + LayerIds::All + } else { + new_layers.sort_unstable(); + new_layers.dedup(); + LayerIds::Multiple(new_layers.into()) + } + } + } + } + + pub(crate) fn get_layer_name(&self, layer: usize) -> ArcStr { + self.edge_meta.get_layer_name_by_id(layer) + } + + pub(crate) fn graph_earliest_time(&self) -> Option { + Some(self.earliest_time.get()).filter(|t| *t != i64::MAX) + } + + pub(crate) fn graph_latest_time(&self) -> Option { + Some(self.latest_time.get()).filter(|t| *t != i64::MIN) + } + + pub(crate) fn load_from_file>(path: P) -> Result> { + let f = std::fs::File::open(path)?; + let mut reader = std::io::BufReader::new(f); + bincode::deserialize_from(&mut reader) + } + + pub(crate) fn save_to_file>( + &self, + path: P, + ) -> Result<(), Box> { + let f = std::fs::File::create(path)?; + let mut writer = std::io::BufWriter::new(f); + bincode::serialize_into(&mut writer, self) + } + + #[inline] + pub(crate) fn global_vertex_id(&self, v: VID) -> u64 { + let node = self.storage.get_node(v); + node.global_id() + } + + pub(crate) fn vertex_name(&self, v: VID) -> String { + let node = self.storage.get_node(v); + node.name + .clone() + .unwrap_or_else(|| node.global_id().to_string()) + } + + #[inline] + pub(crate) fn node_entry(&self, v: VID) -> Entry<'_, VertexStore, N> { + self.storage.get_node(v.into()) + } + + pub(crate) fn edge_refs(&self) -> impl Iterator + Send { + self.storage.edge_refs() + } + + #[inline] + pub(crate) fn edge_entry(&self, e: EID) -> Entry<'_, EdgeStore, N> { + self.storage.get_edge(e.into()) + } +} + +impl TemporalGraph { + pub(crate) fn internal_num_vertices(&self) -> usize { + self.storage.nodes.len() + } + #[inline] + pub(crate) fn num_edges(&self, layers: &LayerIds, filter: Option<&EdgeFilter>) -> usize { + match filter { + None => match layers { + LayerIds::All => self.storage.edges.len(), + _ => { + let guard = self.storage.edges.read_lock(); + guard.par_iter().filter(|e| e.has_layer(layers)).count() + } + }, + Some(filter) => { + let guard = self.storage.edges.read_lock(); + guard.par_iter().filter(|e| filter(e, layers)).count() + } + } + } + + #[inline] + pub(crate) fn degree( + &self, + v: VID, + dir: Direction, + layers: &LayerIds, + filter: Option<&EdgeFilter>, + ) -> usize { + let node_store = self.storage.get_node(v); + match filter { + None => node_store.degree(layers, dir), + Some(filter) => { + let edges_locked = self.storage.edges.read_lock(); + node_store + .edge_tuples(layers, dir) + .filter(|e| filter(edges_locked.get(e.pid().into()), layers)) + .dedup_by(|e1, e2| e1.remote() == e2.remote()) + .count() + } + } + } + + #[inline] + fn update_time(&self, time: TimeIndexEntry) { + let t = *time.t(); + self.earliest_time.update(t); + self.latest_time.update(t); + } + + /// return local id for vertex, initialising storage if vertex does not exist yet + pub(crate) fn resolve_vertex(&self, id: u64, name: Option<&str>) -> VID { + *(self.logical_to_physical.entry(id).or_insert_with(|| { + let name = name.map(|s| s.to_owned()); + let node_store = VertexStore::empty(id, name); + self.storage.push_node(node_store) + })) + } + + #[inline] + pub(crate) fn add_vertex_no_props( + &self, + time: TimeIndexEntry, + v_id: VID, + ) -> EntryMut { + self.update_time(time); + // get the node and update the time index + let mut node = self.storage.get_node_mut(v_id); + node.update_time(time); + node + } + + pub(crate) fn add_vertex_internal( + &self, + time: TimeIndexEntry, + v_id: VID, + props: Vec<(usize, Prop)>, + ) -> Result<(), GraphError> { + let mut node = self.add_vertex_no_props(time, v_id); + for (id, prop) in props { + node.add_prop(time, id, prop)?; + } + Ok(()) + } + + pub(crate) fn add_edge_properties_internal( + &self, + edge_id: EID, + props: Vec<(usize, Prop)>, + layer: usize, + ) -> Result<(), IllegalMutate> { + let mut edge = self.storage.get_edge_mut(edge_id.into()); + + let mut layer = edge.layer_mut(layer); + for (prop_id, prop) in props { + layer.add_constant_prop(prop_id, prop).map_err(|err| { + IllegalMutate::from_source(err, &self.edge_meta.get_prop_name(prop_id, true)) + })?; + } + Ok(()) + } + + pub(crate) fn add_constant_properties( + &self, + props: Vec<(usize, Prop)>, + ) -> Result<(), GraphError> { + for (id, prop) in props { + self.graph_props.add_constant_prop(id, prop)?; + } + Ok(()) + } + + pub(crate) fn add_properties( + &self, + t: TimeIndexEntry, + props: Vec<(usize, Prop)>, + ) -> Result<(), GraphError> { + for (prop_id, prop) in props { + self.graph_props.add_prop(t, prop_id, prop)?; + } + Ok(()) + } + + pub(crate) fn get_constant_prop(&self, id: usize) -> Option { + self.graph_props.get_constant(id) + } + + pub(crate) fn get_temporal_prop(&self, id: usize) -> Option> { + self.graph_props.get_temporal_prop(id) + } + + pub(crate) fn const_prop_names(&self) -> ArcReadLockedVec { + self.graph_props.constant_names() + } + + pub(crate) fn temporal_property_names(&self) -> ArcReadLockedVec { + self.graph_props.temporal_names() + } + + pub(crate) fn delete_edge( + &self, + t: TimeIndexEntry, + src_id: VID, + dst_id: VID, + layer: usize, + ) -> Result<(), GraphError> { + self.link_nodes(src_id, dst_id, t, layer, |new_edge| { + new_edge.deletions_mut(layer).insert(t); + Ok(()) + })?; + Ok(()) + } + + fn get_or_allocate_layer(&self, layer: Option<&str>) -> usize { + layer + .map(|layer| self.edge_meta.get_or_create_layer_id(layer)) + .unwrap_or(0) + } + + fn link_nodes Result<(), GraphError>>( + &self, + src_id: VID, + dst_id: VID, + t: TimeIndexEntry, + layer: usize, + edge_fn: F, + ) -> Result { + let mut node_pair = self.storage.pair_node_mut(src_id.into(), dst_id.into()); + self.update_time(t); + let src = node_pair.get_mut_i(); + + let edge_id = match src.find_edge(dst_id, &LayerIds::All) { + Some(edge_id) => { + let mut edge = self.storage.get_edge_mut(edge_id); + edge_fn(&mut edge)?; + edge_id + } + None => { + let mut edge = EdgeStore::new(src_id, dst_id); + edge_fn(&mut edge)?; + self.storage.push_edge(edge) + } + }; + + src.add_edge(dst_id, Direction::OUT, layer, edge_id); + src.update_time(t); + let dst = node_pair.get_mut_j(); + dst.add_edge(src_id, Direction::IN, layer, edge_id); + dst.update_time(t); + Ok(edge_id) + } + + pub(crate) fn add_edge_internal( + &self, + t: TimeIndexEntry, + src_id: VID, + dst_id: VID, + props: Vec<(usize, Prop)>, + layer: usize, + ) -> Result { + // get the entries for the src and dst nodes + self.link_nodes(src_id, dst_id, t, layer, move |edge| { + edge.additions_mut(layer).insert(t); + let mut edge_layer = edge.layer_mut(layer); + for (prop_id, prop_value) in props { + edge_layer.add_prop(t, prop_id, prop_value)?; + } + Ok(()) + }) + } + + #[inline] + pub(crate) fn vertex_ids(&self) -> impl Iterator { + (0..self.storage.nodes.len()).map(|i| i.into()) + } + + pub(crate) fn locked_edges(&self) -> impl Iterator> { + self.storage.locked_edges() + } + + pub(crate) fn find_edge(&self, src: VID, dst: VID, layer_id: &LayerIds) -> Option { + let node = self.storage.get_node(src.into()); + node.find_edge(dst, layer_id) + } + + pub(crate) fn resolve_vertex_ref(&self, v: VertexRef) -> Option { + match v { + VertexRef::Internal(vid) => Some(vid), + VertexRef::External(gid) => { + let v_id = self.logical_to_physical.get(&gid)?; + Some((*v_id).into()) + } + } + } + + pub(crate) fn vertex(&self, v: VID) -> Vertex { + let node = self.storage.get_node(v.into()); + Vertex::from_entry(node, self) + } + + pub(crate) fn vertex_arc(&self, v: VID) -> ArcVertex { + let node = self.storage.get_node_arc(v.into()); + ArcVertex::from_entry(node, self.vertex_meta.clone()) + } + + pub(crate) fn edge_arc(&self, e: EID) -> ArcEdge { + let edge = self.storage.get_edge_arc(e.into()); + ArcEdge::from_entry(edge, self.edge_meta.clone()) + } + + #[inline] + pub(crate) fn edge(&self, e: EID) -> EdgeView { + let edge = self.storage.get_edge(e.into()); + EdgeView::from_entry(edge, self) + } + + /// Checks if the same string value already exists and returns a pointer to the same existing value if it exists, + /// otherwise adds the string to the pool. + pub(crate) fn resolve_str(&self, value: ArcStr) -> ArcStr { + match self.string_pool.get(&value) { + Some(value) => value.clone(), + None => { + if self.string_pool.insert(value.clone()) { + value + } else { + self.string_pool + .get(&value) + .expect("value exists due to insert above returning false") + .clone() + } + } + } + } +} + +#[cfg(test)] +mod test_additions { + use crate::prelude::*; + use rayon::{join, prelude::*}; + #[test] + fn add_edge_and_read_props_concurrent() { + let g = Graph::new(); + for t in 0..1000 { + join( + || g.add_edge(t, 1, 2, [("test", true)], None), + || { + // if the edge exists already, it should have the property set + g.window(t, t + 1) + .edge(1, 2) + .map(|e| assert!(e.properties().get("test").is_some())) + }, + ); + } + } +} diff --git a/raphtory/src/core/entities/graph/tgraph_storage.rs b/raphtory/src/core/entities/graph/tgraph_storage.rs new file mode 100644 index 0000000000..ddbd40fb82 --- /dev/null +++ b/raphtory/src/core/entities/graph/tgraph_storage.rs @@ -0,0 +1,208 @@ +use crate::core::{ + entities::{ + edges::{edge_ref::EdgeRef, edge_store::EdgeStore}, + vertices::vertex_store::VertexStore, + LayerIds, EID, VID, + }, + storage::{self, ArcEntry, Entry, EntryMut, PairEntryMut}, + Direction, +}; +use rayon::prelude::{ParallelBridge, ParallelIterator}; +use serde::{Deserialize, Serialize}; +use std::{ + ops::{Deref, Range}, + sync::Arc, +}; + +#[derive(Debug, Deserialize, Serialize, PartialEq)] +pub(crate) struct GraphStorage { + // node storage with having (id, time_index, properties, adj list for each layer) + pub(crate) nodes: storage::RawStorage, + + // edge storage with having (src, dst, time_index, properties) for each layer + pub(crate) edges: storage::RawStorage, +} + +impl GraphStorage { + pub(crate) fn new() -> Self { + Self { + nodes: storage::RawStorage::new(), + edges: storage::RawStorage::new(), + } + } + + pub(crate) fn push_node(&self, node: VertexStore) -> VID { + self.nodes + .push(node, |vid, node| node.vid = vid.into()) + .into() + } + + pub(crate) fn push_edge(&self, edge: EdgeStore) -> EID { + self.edges + .push(edge, |eid, edge| edge.eid = eid.into()) + .into() + } + + #[inline] + pub(crate) fn get_node_mut(&self, id: VID) -> EntryMut<'_, VertexStore> { + self.nodes.entry_mut(id.into()) + } + + #[inline] + pub(crate) fn get_edge_mut(&self, id: EID) -> EntryMut<'_, EdgeStore> { + self.edges.entry_mut(id.into()) + } + + #[inline] + pub(crate) fn get_node(&self, id: VID) -> Entry<'_, VertexStore, N> { + self.nodes.entry(id.into()) + } + + pub(crate) fn get_node_arc(&self, id: VID) -> ArcEntry { + self.nodes.entry_arc(id.into()) + } + + pub(crate) fn get_edge_arc(&self, id: EID) -> ArcEntry { + self.edges.entry_arc(id.into()) + } + + #[inline] + pub(crate) fn get_edge(&self, id: EID) -> Entry<'_, EdgeStore, N> { + self.edges.entry(id.into()) + } + + pub(crate) fn pair_node_mut(&self, i: VID, j: VID) -> PairEntryMut<'_, VertexStore> { + self.nodes.pair_entry_mut(i.into(), j.into()) + } + + fn lock(&self) -> LockedGraphStorage { + LockedGraphStorage::new(self) + } + + pub(crate) fn locked_nodes(&self) -> LockedIter { + LockedIter { + from: 0, + to: self.nodes.len(), + locked_gs: Arc::new(self.lock()), + phantom: std::marker::PhantomData, + } + } + + pub(crate) fn locked_edges(&self) -> impl Iterator> { + self.edges.read_lock().into_iter() + } + + pub(crate) fn edge_refs(&self) -> impl Iterator + Send { + self.edges + .read_lock() + .into_iter() + .map(|entry| EdgeRef::from(entry)) + } +} + +pub(crate) struct LockedIter { + from: usize, + to: usize, + locked_gs: Arc>, + phantom: std::marker::PhantomData, +} + +impl Iterator for LockedIter { + type Item = GraphEntry; + + fn next(&mut self) -> Option { + if self.from < self.to { + let node = Some(GraphEntry { + locked_gs: self.locked_gs.clone(), + i: self.from, + _marker: std::marker::PhantomData, + }); + self.from += 1; + node + } else { + None + } + } +} + +impl<'a, const N: usize> Iterator for LockedIter { + type Item = GraphEntry; + + fn next(&mut self) -> Option { + if self.from < self.to { + let node = Some(GraphEntry { + locked_gs: self.locked_gs.clone(), + i: self.from, + _marker: std::marker::PhantomData, + }); + self.from += 1; + node + } else { + None + } + } +} + +pub struct GraphEntry { + locked_gs: Arc>, + i: usize, + _marker: std::marker::PhantomData, +} + +// impl new +impl<'a, const N: usize, T> GraphEntry { + pub(crate) fn new(gs: Arc>, i: usize) -> Self { + Self { + locked_gs: gs, + i, + _marker: std::marker::PhantomData, + } + } + + pub(crate) fn index(&self) -> usize { + self.i + } + + pub(crate) fn locked_gs(&self) -> &Arc> { + &self.locked_gs + } +} + +impl<'a, const N: usize> Deref for GraphEntry { + type Target = VertexStore; + + fn deref(&self) -> &Self::Target { + self.locked_gs.get_node(self.i) + } +} + +impl<'a, const N: usize> Deref for GraphEntry { + type Target = EdgeStore; + + fn deref(&self) -> &Self::Target { + self.locked_gs.get_edge(self.i) + } +} + +#[derive(Debug)] +pub(crate) struct LockedGraphStorage { + nodes: storage::ReadLockedStorage, + edges: storage::ReadLockedStorage, +} + +impl LockedGraphStorage { + pub(crate) fn new(storage: &GraphStorage) -> Self { + Self { + nodes: storage.nodes.read_lock(), + edges: storage.edges.read_lock(), + } + } + + pub(crate) fn get_node(&self, id: usize) -> &VertexStore { + self.nodes.get(id) + } + + pub(crate) fn get_edge(&self, id: usize) -> &EdgeStore { + self.edges.get(id) + } +} diff --git a/raphtory/src/core/entities/graph/timer.rs b/raphtory/src/core/entities/graph/timer.rs new file mode 100644 index 0000000000..f7eda2d487 --- /dev/null +++ b/raphtory/src/core/entities/graph/timer.rs @@ -0,0 +1,108 @@ +use serde::{Deserialize, Serialize}; +use std::sync::atomic::{AtomicI64, Ordering}; + +pub(crate) trait TimeCounterTrait { + fn cmp(a: i64, b: i64) -> bool; + fn counter(&self) -> &AtomicI64; + + fn update(&self, new_value: i64) { + let mut current_value = self.get(); + while Self::cmp(new_value, current_value) { + match self.counter().compare_exchange_weak( + current_value, + new_value, + Ordering::Relaxed, + Ordering::Relaxed, + ) { + Ok(_) => break, + Err(value) => current_value = value, + } + } + } + fn get(&self) -> i64; +} + +#[derive(Serialize, Deserialize, Debug)] +pub(crate) struct MinCounter { + counter: AtomicI64, +} + +impl MinCounter { + pub fn new() -> Self { + Self { + counter: AtomicI64::new(i64::MAX), + } + } +} + +impl TimeCounterTrait for MinCounter { + fn cmp(new_value: i64, current_value: i64) -> bool { + new_value < current_value + } + + fn counter(&self) -> &AtomicI64 { + &self.counter + } + + fn get(&self) -> i64 { + self.counter.load(Ordering::Relaxed) + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub(crate) struct MaxCounter { + counter: AtomicI64, +} + +impl MaxCounter { + pub fn new() -> Self { + Self { + counter: AtomicI64::new(i64::MIN), + } + } +} + +impl TimeCounterTrait for MaxCounter { + fn cmp(a: i64, b: i64) -> bool { + a > b + } + fn get(&self) -> i64 { + self.counter.load(Ordering::Relaxed) + } + + fn counter(&self) -> &AtomicI64 { + &self.counter + } +} + +#[cfg(test)] +mod test { + + use super::*; + + #[test] + fn min_counter() { + let counter = MinCounter::new(); + counter.update(0); + assert_eq!(counter.get(), 0); + counter.update(1); + assert_eq!(counter.get(), 0); + counter.update(0); + assert_eq!(counter.get(), 0); + counter.update(-1); + assert_eq!(counter.get(), -1); + } + + #[test] + fn max_counter() { + let counter = MaxCounter::new(); + counter.update(0); + assert_eq!(counter.get(), 0); + counter.update(-1); + assert_eq!(counter.get(), 0); + counter.update(0); + assert_eq!(counter.get(), 0); + counter.update(1); + assert_eq!(counter.get(), 1); + } +} diff --git a/raphtory/src/core/entities/mod.rs b/raphtory/src/core/entities/mod.rs new file mode 100644 index 0000000000..8b0db056e3 --- /dev/null +++ b/raphtory/src/core/entities/mod.rs @@ -0,0 +1,185 @@ +#![allow(unused)] + +use std::{ops::Deref, sync::Arc}; + +use crate::core::entities::edges::edge_ref::EdgeRef; +use edges::edge::ERef; +use graph::{tgraph::TGraph, tgraph_storage::GraphEntry}; +use serde::{Deserialize, Serialize}; +use vertices::{vertex_ref::VertexRef, vertex_store::VertexStore}; + +use super::{storage::Entry, Direction}; + +pub mod edges; +pub mod graph; +pub mod properties; +pub mod vertices; + +// the only reason this is public is because the phisical ids of the vertices don't move +#[repr(transparent)] +#[derive( + Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize, Serialize, Default, +)] +pub struct VID(pub usize); + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub(crate) struct LocalID { + pub(crate) bucket: usize, + pub(crate) offset: usize, +} + +impl From for VID { + fn from(id: usize) -> Self { + VID(id) + } +} + +impl From for usize { + fn from(id: VID) -> Self { + id.0 + } +} + +#[repr(transparent)] +#[derive( + Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize, Serialize, Default, +)] +pub struct EID(pub usize); + +impl From for usize { + fn from(id: EID) -> Self { + id.0 + } +} + +impl From for EID { + fn from(id: usize) -> Self { + EID(id) + } +} + +pub(crate) enum VRef<'a, const N: usize> { + Entry(Entry<'a, VertexStore, N>), // returned from graph.vertex + LockedEntry(GraphEntry), // returned from locked_vertices +} + +// return index -> usize for VRef +impl<'a, const N: usize> VRef<'a, N> { + fn index(&'a self) -> usize { + match self { + VRef::Entry(e) => e.index(), + VRef::LockedEntry(ge) => ge.index(), + } + } + + fn edge_ref(&self, edge_id: EID, graph: &'a TGraph) -> ERef<'a, N> { + match self { + VRef::Entry(_) => ERef::ERef(graph.edge_entry(edge_id)), + VRef::LockedEntry(ge) => ERef::ELock { + lock: ge.locked_gs().clone(), + eid: edge_id, + }, + } + } +} + +impl<'a, const N: usize> Deref for VRef<'a, N> { + type Target = VertexStore; + + fn deref(&self) -> &Self::Target { + match self { + VRef::Entry(e) => e, + VRef::LockedEntry(e) => e, + } + } +} + +pub(crate) trait GraphItem<'a, const N: usize> { + fn from_edge_ids( + src: VID, + dst: VID, + e_id: ERef<'a, N>, + dir: Direction, + graph: &'a TGraph, + ) -> Self; +} + +#[derive(Clone, Debug)] +pub enum LayerIds { + None, + All, + One(usize), + Multiple(Arc<[usize]>), +} + +impl LayerIds { + pub fn find(&self, layer_id: usize) -> Option { + match self { + LayerIds::All => Some(layer_id), + LayerIds::One(id) => { + if *id == layer_id { + Some(layer_id) + } else { + None + } + } + LayerIds::Multiple(ids) => ids.binary_search(&layer_id).ok().map(|_| layer_id), + LayerIds::None => None, + } + } + + pub fn constrain_from_edge(self, e: EdgeRef) -> LayerIds { + match e.layer() { + None => self, + Some(l) => self + .find(*l) + .map(|l| LayerIds::One(l)) + .unwrap_or(LayerIds::None), + } + } + + pub fn contains(&self, layer_id: &usize) -> bool { + self.find(*layer_id).is_some() + } +} + +impl From> for LayerIds { + fn from(mut v: Vec) -> Self { + match v.len() { + 0 => LayerIds::All, + 1 => LayerIds::One(v[0]), + _ => { + v.sort_unstable(); + v.dedup(); + LayerIds::Multiple(v.into()) + } + } + } +} + +impl From<[usize; N]> for LayerIds { + fn from(v: [usize; N]) -> Self { + match v.len() { + 0 => LayerIds::All, + 1 => LayerIds::One(v[0]), + _ => { + let mut v = v.to_vec(); + v.sort_unstable(); + v.dedup(); + LayerIds::Multiple(v.into()) + } + } + } +} + +impl From for LayerIds { + fn from(id: usize) -> Self { + LayerIds::One(id) + } +} + +impl From> for LayerIds { + fn from(id: Arc<[usize]>) -> Self { + LayerIds::Multiple(id) + } +} diff --git a/raphtory/src/core/entities/properties/graph_props.rs b/raphtory/src/core/entities/properties/graph_props.rs new file mode 100644 index 0000000000..56e335882e --- /dev/null +++ b/raphtory/src/core/entities/properties/graph_props.rs @@ -0,0 +1,141 @@ +use crate::core::{ + entities::{ + graph::tgraph::FxDashMap, + properties::{ + props::{ArcReadLockedVec, DictMapper}, + tprop::TProp, + }, + }, + storage::{lazy_vec::IllegalSet, locked_view::LockedView, timeindex::TimeIndexEntry}, + utils::errors::{GraphError, IllegalMutate, MutateGraphError}, + ArcStr, Prop, PropType, +}; +use parking_lot::RwLockReadGuard; +use serde::{Deserialize, Serialize}; +use std::{ + ops::{Deref, DerefMut}, + sync::Arc, +}; + +#[derive(Serialize, Deserialize, Debug)] +pub struct GraphProps { + constant_mapper: DictMapper, + temporal_mapper: DictMapper, + constant: FxDashMap>, + temporal: FxDashMap, +} + +impl GraphProps { + pub(crate) fn new() -> Self { + Self { + constant_mapper: DictMapper::default(), + temporal_mapper: DictMapper::default(), + constant: FxDashMap::default(), + temporal: FxDashMap::default(), + } + } + + #[inline] + pub fn const_prop_meta(&self) -> &DictMapper { + &self.constant_mapper + } + + #[inline] + pub fn temporal_prop_meta(&self) -> &DictMapper { + &self.temporal_mapper + } + + #[inline] + pub(crate) fn resolve_property(&self, name: &str, is_static: bool) -> usize { + if is_static { + self.constant_mapper.get_or_create_id(name) + } else { + self.temporal_mapper.get_or_create_id(name) + } + } + + pub(crate) fn add_constant_prop( + &self, + prop_id: usize, + prop: Prop, + ) -> Result<(), MutateGraphError> { + let mut prop_entry = self.constant.entry(prop_id).or_insert(None); + match prop_entry.deref_mut() { + Some(old_value) => { + if !(old_value == &prop) { + return Err(MutateGraphError::IllegalGraphPropertyChange { + name: self.constant_mapper.get_name(prop_id).to_string(), + old_value: old_value.clone(), + new_value: prop, + }); + } + } + None => { + (*prop_entry) = Some(prop); + } + } + Ok(()) + } + + pub(crate) fn add_prop( + &self, + t: TimeIndexEntry, + prop_id: usize, + prop: Prop, + ) -> Result<(), GraphError> { + let mut prop_entry = self.temporal.entry(prop_id).or_insert(TProp::default()); + (*prop_entry).set(t, prop) + } + + pub(crate) fn get_constant(&self, id: usize) -> Option { + let entry = self.constant.get(&id)?; + entry.as_ref().cloned() + } + + pub(crate) fn get_temporal_prop(&self, prop_id: usize) -> Option> { + let entry = self.temporal.get(&prop_id)?; + Some(LockedView::DashMap(entry)) + } + + pub fn get_const_prop_id(&self, name: &str) -> Option { + self.constant_mapper.get_id(name) + } + + pub fn get_temporal_id(&self, name: &str) -> Option { + self.temporal_mapper.get_id(name) + } + + pub fn get_const_prop_name(&self, prop_id: usize) -> ArcStr { + self.constant_mapper.get_name(prop_id) + } + + pub fn get_temporal_name(&self, prop_id: usize) -> ArcStr { + self.temporal_mapper.get_name(prop_id) + } + + pub fn get_constant_dtype(&self, prop_id: usize) -> Option { + self.constant + .get(&prop_id) + .and_then(|v| v.as_ref().map(|v| v.dtype())) + } + + pub fn get_temporal_dtype(&self, prop_id: usize) -> Option { + self.temporal.get(&prop_id).map(|v| v.dtype()) + } + + pub(crate) fn constant_names(&self) -> ArcReadLockedVec { + self.constant_mapper.get_keys() + } + + pub(crate) fn const_prop_ids(&self) -> impl Iterator { + 0..self.constant_mapper.len() + } + + pub(crate) fn temporal_names(&self) -> ArcReadLockedVec { + self.temporal_mapper.get_keys() + } + + pub(crate) fn temporal_ids(&self) -> impl Iterator { + 0..self.temporal_mapper.len() + } +} diff --git a/raphtory/src/core/entities/properties/mod.rs b/raphtory/src/core/entities/properties/mod.rs new file mode 100644 index 0000000000..c5875f71b6 --- /dev/null +++ b/raphtory/src/core/entities/properties/mod.rs @@ -0,0 +1,4 @@ +pub mod graph_props; +pub mod props; +pub mod tcell; +pub mod tprop; diff --git a/raphtory/src/core/entities/properties/props.rs b/raphtory/src/core/entities/properties/props.rs new file mode 100644 index 0000000000..695cc16948 --- /dev/null +++ b/raphtory/src/core/entities/properties/props.rs @@ -0,0 +1,462 @@ +use crate::core::{ + entities::{graph::tgraph::FxDashMap, properties::tprop::TProp}, + storage::{ + lazy_vec::{IllegalSet, LazyVec}, + locked_view::LockedView, + timeindex::TimeIndexEntry, + }, + utils::errors::{GraphError, IllegalMutate, MutateGraphError}, + ArcStr, Prop, PropType, +}; +use lock_api; +use parking_lot::{RwLock, RwLockReadGuard}; +use serde::{Deserialize, Serialize}; +use std::{ + borrow::Borrow, + fmt::Debug, + hash::Hash, + ops::Deref, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, +}; +use tantivy::HasLen; + +type ArcRwLockReadGuard = lock_api::ArcRwLockReadGuard; + +#[derive(Serialize, Deserialize, Default, Debug, PartialEq)] +pub struct Props { + // properties + constant_props: LazyVec>, + temporal_props: LazyVec, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Clone)] +enum PropId { + Static(usize), + Temporal(usize), +} + +impl Props { + pub fn new() -> Self { + Self { + constant_props: LazyVec::Empty, + temporal_props: LazyVec::Empty, + } + } + + pub fn add_prop( + &mut self, + t: TimeIndexEntry, + prop_id: usize, + prop: Prop, + ) -> Result<(), GraphError> { + self.temporal_props.update(prop_id, |p| p.set(t, prop)) + } + + pub fn add_constant_prop( + &mut self, + prop_id: usize, + prop: Prop, + ) -> Result<(), IllegalSet>> { + self.constant_props.set(prop_id, Some(prop)) + } + + pub fn temporal_props(&self, prop_id: usize) -> Box + '_> { + let o = self.temporal_props.get(prop_id); + if let Some(t_prop) = o { + Box::new(t_prop.iter()) + } else { + Box::new(std::iter::empty()) + } + } + + pub fn temporal_props_window( + &self, + prop_id: usize, + t_start: i64, + t_end: i64, + ) -> Box + '_> { + let o = self.temporal_props.get(prop_id); + if let Some(t_prop) = o { + Box::new(t_prop.iter_window(t_start..t_end)) + } else { + Box::new(std::iter::empty()) + } + } + + pub fn const_prop(&self, prop_id: usize) -> Option<&Prop> { + let prop = self.constant_props.get(prop_id)?; + prop.as_ref() + } + + pub fn temporal_prop(&self, prop_id: usize) -> Option<&TProp> { + self.temporal_props.get(prop_id) + } + + pub fn const_prop_ids(&self) -> impl Iterator + '_ { + self.constant_props.filled_ids() + } + + pub fn temporal_prop_ids(&self) -> impl Iterator + '_ { + self.temporal_props.filled_ids() + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Meta { + meta_prop_temporal: PropMapper, + meta_prop_constant: PropMapper, + meta_layer: DictMapper, +} + +impl Meta { + pub fn const_prop_meta(&self) -> &PropMapper { + &self.meta_prop_constant + } + + pub fn temporal_prop_meta(&self) -> &PropMapper { + &self.meta_prop_temporal + } + + pub fn layer_meta(&self) -> &DictMapper { + &self.meta_layer + } + + pub fn new() -> Self { + let meta_layer = DictMapper::default(); + meta_layer.get_or_create_id("_default"); + Self { + meta_prop_temporal: PropMapper::default(), + meta_prop_constant: PropMapper::default(), + meta_layer, // layer 0 is the default layer + } + } + + #[inline] + pub fn resolve_prop_id( + &self, + prop: &str, + dtype: PropType, + is_static: bool, + ) -> Result { + if is_static { + self.meta_prop_constant + .get_or_create_and_validate(prop, dtype) + } else { + self.meta_prop_temporal + .get_or_create_and_validate(prop, dtype) + } + } + + #[inline] + pub fn get_prop_id(&self, name: &str, is_static: bool) -> Option { + if is_static { + self.meta_prop_constant.get_id(name) + } else { + self.meta_prop_temporal.get_id(name) + } + } + + #[inline] + pub fn get_or_create_layer_id(&self, name: &str) -> usize { + self.meta_layer.get_or_create_id(name) + } + + #[inline] + pub fn get_layer_id(&self, name: &str) -> Option { + self.meta_layer.map.get(name).as_deref().copied() + } + + pub fn get_layer_name_by_id(&self, id: usize) -> ArcStr { + self.meta_layer.get_name(id) + } + + pub fn get_all_layers(&self) -> Vec { + self.meta_layer + .map + .iter() + .map(|entry| *entry.value()) + .collect() + } + + pub fn get_all_property_names(&self, is_static: bool) -> ArcReadLockedVec { + if is_static { + self.meta_prop_constant.get_keys() + } else { + self.meta_prop_temporal.get_keys() + } + } + + pub fn get_prop_name(&self, prop_id: usize, is_static: bool) -> ArcStr { + if is_static { + self.meta_prop_constant.get_name(prop_id) + } else { + self.meta_prop_temporal.get_name(prop_id) + } + } +} + +#[derive(Serialize, Deserialize, Default, Debug)] +pub struct DictMapper { + map: FxDashMap, + reverse_map: Arc>>, //FIXME: a boxcar vector would be a great fit if it was serializable... +} + +#[derive(Debug)] +pub struct ArcReadLockedVec { + guard: ArcRwLockReadGuard>, +} + +impl Deref for ArcReadLockedVec { + type Target = Vec; + + #[inline] + fn deref(&self) -> &Self::Target { + self.guard.deref() + } +} + +impl IntoIterator for ArcReadLockedVec { + type Item = T; + type IntoIter = LockedIter; + + fn into_iter(self) -> Self::IntoIter { + let guard = self.guard; + let len = guard.len(); + let pos = 0; + LockedIter { guard, pos, len } + } +} + +pub struct LockedIter { + guard: ArcRwLockReadGuard>, + pos: usize, + len: usize, +} + +impl Iterator for LockedIter { + type Item = T; + + fn next(&mut self) -> Option { + if self.pos < self.len { + let next_val = Some(self.guard[self.pos].clone()); + self.pos += 1; + next_val + } else { + None + } + } +} + +impl DictMapper { + pub fn get_or_create_id(&self, name: &Q) -> usize + where + ArcStr: Borrow, + Q: Hash + Eq + ?Sized + ToOwned, + T: Into, + { + if let Some(existing_id) = self.map.get(name) { + return *existing_id; + } + + let name = name.to_owned().into(); + let new_id = self.map.entry(name.clone()).or_insert_with(|| { + let mut reverse = self.reverse_map.write(); + let id = reverse.len(); + reverse.push(name); + id + }); + *new_id + } + + pub fn get_id(&self, name: &str) -> Option { + self.map.get(name).map(|id| *id) + } + + pub fn get_name(&self, id: usize) -> ArcStr { + let guard = self.reverse_map.read(); + guard + .get(id) + .map(|v| v.clone()) + .expect("internal ids should always be mapped to a name") + } + + pub fn get_keys(&self) -> ArcReadLockedVec { + ArcReadLockedVec { + guard: self.reverse_map.read_arc(), + } + } + + pub fn len(&self) -> usize { + self.reverse_map.read().len() + } + + pub fn is_empty(&self) -> bool { + self.reverse_map.read().is_empty() + } +} + +#[derive(Default, Debug, Serialize, Deserialize)] +pub struct PropMapper { + id_mapper: DictMapper, + dtypes: Arc>>, +} + +impl Deref for PropMapper { + type Target = DictMapper; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.id_mapper + } +} + +impl PropMapper { + fn get_or_create_and_validate(&self, prop: &str, dtype: PropType) -> Result { + let id = self.id_mapper.get_or_create_id(prop); + let dtype_read = self.dtypes.read_recursive(); + if let Some(old_type) = dtype_read.get(id) { + if !matches!(old_type, PropType::Empty) { + return if *old_type == dtype { + Ok(id) + } else { + Err(GraphError::PropertyTypeError { + name: prop.to_owned(), + expected: *old_type, + actual: dtype, + }) + }; + } + } + drop(dtype_read); // drop the read lock and wait for write lock as type did not exist yet + let mut dtype_write = self.dtypes.write(); + match dtype_write.get(id) { + Some(&old_type) => { + if matches!(old_type, PropType::Empty) { + // vector already resized but this id is not filled yet, set the dtype and return id + dtype_write[id] = dtype; + Ok(id) + } else { + // already filled because a different thread won the race for this id, check the type matches + if old_type == dtype { + Ok(id) + } else { + Err(GraphError::PropertyTypeError { + name: prop.to_owned(), + expected: old_type, + actual: dtype, + }) + } + } + } + None => { + // vector not resized yet, resize it and set the dtype and return id + dtype_write.resize(id + 1, PropType::Empty); + dtype_write[id] = dtype; + Ok(id) + } + } + } + + pub fn get_dtype(&self, prop_id: usize) -> Option { + self.dtypes.read_recursive().get(prop_id).copied() + } +} + +#[cfg(test)] +mod test { + use super::*; + use rand::seq::SliceRandom; + use rayon::prelude::*; + use std::{collections::HashMap, sync::Arc, thread}; + + #[test] + fn test_dict_mapper() { + let mapper = DictMapper::default(); + assert_eq!(mapper.get_or_create_id("test"), 0); + assert_eq!(mapper.get_or_create_id("test"), 0); + assert_eq!(mapper.get_or_create_id("test2"), 1); + assert_eq!(mapper.get_or_create_id("test2"), 1); + assert_eq!(mapper.get_or_create_id("test"), 0); + } + + #[quickcheck] + fn check_dict_mapper_concurrent_write(write: Vec) -> bool { + let n = 100; + let mapper: DictMapper = DictMapper::default(); + + // create n maps from strings to ids in parallel + let res: Vec> = (0..n) + .into_par_iter() + .map(|_| { + let mut ids: HashMap = Default::default(); + let mut rng = rand::thread_rng(); + let mut write_s = write.clone(); + write_s.shuffle(&mut rng); + for s in write_s { + let id = mapper.get_or_create_id(s.as_str()); + ids.insert(s, id); + } + ids + }) + .collect(); + + // check that all maps are the same and that all strings have been assigned an id + let res_0 = &res[0]; + res[1..n].iter().all(|v| res_0 == v) && write.iter().all(|v| mapper.get_id(v).is_some()) + } + + // map 5 strings to 5 ids from 4 threads concurrently 1000 times + #[test] + fn test_dict_mapper_concurrent() { + use std::{sync::Arc, thread}; + + let mapper = Arc::new(DictMapper::default()); + let mut threads = Vec::new(); + for _ in 0..4 { + let mapper = Arc::clone(&mapper); + threads.push(thread::spawn(move || { + for _ in 0..1000 { + mapper.get_or_create_id("test"); + mapper.get_or_create_id("test2"); + mapper.get_or_create_id("test3"); + mapper.get_or_create_id("test4"); + mapper.get_or_create_id("test5"); + } + })); + } + + for thread in threads { + thread.join().unwrap(); + } + + let mut actual = vec!["test", "test2", "test3", "test4", "test5"] + .into_iter() + .map(|name| mapper.get_or_create_id(name)) + .collect::>(); + actual.sort(); + + assert_eq!(actual, vec![0, 1, 2, 3, 4]); + } + + #[test] + fn test_prop_mapper_concurrent() { + let values = [Prop::I64(1), Prop::U16(0), Prop::Bool(true), Prop::F64(0.0)]; + let input_len = values.len(); + + let mapper = Arc::new(PropMapper::default()); + let threads: Vec<_> = values + .into_iter() + .map(move |v| { + let mapper = mapper.clone(); + thread::spawn(move || mapper.get_or_create_and_validate("test", v.dtype())) + }) + .flat_map(|t| t.join()) + .collect(); + + assert_eq!(threads.len(), input_len); // no errors + assert_eq!(threads.into_iter().flatten().count(), 1); // only one result (which ever was first) + } +} diff --git a/raphtory/src/core/tcell.rs b/raphtory/src/core/entities/properties/tcell.rs similarity index 61% rename from raphtory/src/core/tcell.rs rename to raphtory/src/core/entities/properties/tcell.rs index bfd23ea19d..de17baa33f 100644 --- a/raphtory/src/core/tcell.rs +++ b/raphtory/src/core/entities/properties/tcell.rs @@ -1,47 +1,50 @@ -use std::{collections::BTreeMap, fmt::Debug, ops::Range}; - +use crate::core::storage::{ + sorted_vec_map::SVM, + timeindex::{AsTime, TimeIndexEntry}, +}; use serde::{Deserialize, Serialize}; - -use crate::core::sorted_vec_map::SVM; +use std::{collections::BTreeMap, fmt::Debug, ops::Range}; #[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize)] - // TCells represent a value in time that can be set at multiple times and keeps a history -pub(crate) enum TCell { +pub enum TCell { #[default] Empty, - TCell1(i64, A), - TCellCap(SVM), - TCellN(BTreeMap), + TCell1(TimeIndexEntry, A), + TCellCap(SVM), + TCellN(BTreeMap), } const BTREE_CUTOFF: usize = 128; -impl TCell { - pub fn new(t: i64, value: A) -> Self { +impl TCell { + pub fn new(t: TimeIndexEntry, value: A) -> Self { TCell::TCell1(t, value) } - pub fn set(&mut self, t: i64, value: A) { + pub fn set(&mut self, t: TimeIndexEntry, value: A) { match self { TCell::Empty => { *self = TCell::TCell1(t, value); } TCell::TCell1(t0, value0) => { - if t != *t0 { - let mut svm = SVM::new(); - svm.insert(t, value); - svm.insert(*t0, value0.clone()); - *self = TCell::TCellCap(svm) + if &t != t0 { + if let TCell::TCell1(t0, value0) = std::mem::take(self) { + let mut svm = SVM::new(); + svm.insert(t, value); + svm.insert(t0, value0); + *self = TCell::TCellCap(svm) + } } } TCell::TCellCap(svm) => { if svm.len() < BTREE_CUTOFF { svm.insert(t, value.clone()); } else { - let mut btm: BTreeMap = BTreeMap::new(); - for (k, v) in svm.iter() { - btm.insert(*k, v.clone()); + let svm = std::mem::take(svm); + let mut btm: BTreeMap = BTreeMap::new(); + for (k, v) in svm.into_iter() { + btm.insert(k, v); } btm.insert(t, value.clone()); *self = TCell::TCellN(btm) @@ -53,6 +56,15 @@ impl TCell { } } + pub fn at(&self, ti: &TimeIndexEntry) -> Option<&A> { + match self { + TCell::Empty => None, + TCell::TCell1(t, v) => (t == ti).then_some(v), + TCell::TCellCap(svm) => svm.get(ti), + TCell::TCellN(btm) => btm.get(ti), + } + } + #[allow(dead_code)] pub fn iter(&self) -> Box + '_> { match self { @@ -66,9 +78,9 @@ impl TCell { pub fn iter_t(&self) -> Box + '_> { match self { TCell::Empty => Box::new(std::iter::empty()), - TCell::TCell1(t, value) => Box::new(std::iter::once((t, value))), - TCell::TCellCap(svm) => Box::new(svm.iter()), - TCell::TCellN(btm) => Box::new(btm.iter()), + TCell::TCell1(t, value) => Box::new(std::iter::once((t.t(), value))), + TCell::TCellCap(svm) => Box::new(svm.iter().map(|(ti, v)| (ti.t(), v))), + TCell::TCellN(btm) => Box::new(btm.iter().map(|(ti, v)| (ti.t(), v))), } } @@ -77,14 +89,18 @@ impl TCell { match self { TCell::Empty => Box::new(std::iter::empty()), TCell::TCell1(t, value) => { - if r.contains(t) { + if r.contains(t.t()) { Box::new(std::iter::once(value)) } else { Box::new(std::iter::empty()) } } - TCell::TCellCap(svm) => Box::new(svm.range(r).map(|(_, value)| value)), - TCell::TCellN(btm) => Box::new(btm.range(r).map(|(_, value)| value)), + TCell::TCellCap(svm) => { + Box::new(svm.range(TimeIndexEntry::range(r)).map(|(_, value)| value)) + } + TCell::TCellN(btm) => { + Box::new(btm.range(TimeIndexEntry::range(r)).map(|(_, value)| value)) + } } } @@ -92,14 +108,35 @@ impl TCell { match self { TCell::Empty => Box::new(std::iter::empty()), TCell::TCell1(t, value) => { - if r.contains(t) { - Box::new(std::iter::once((t, value))) + if r.contains(t.t()) { + Box::new(std::iter::once((t.t(), value))) } else { Box::new(std::iter::empty()) } } - TCell::TCellCap(svm) => Box::new(svm.range(r)), - TCell::TCellN(btm) => Box::new(btm.range(r)), + TCell::TCellCap(svm) => Box::new( + svm.range(TimeIndexEntry::range(r)) + .map(|(ti, v)| (ti.t(), v)), + ), + TCell::TCellN(btm) => Box::new( + btm.range(TimeIndexEntry::range(r)) + .map(|(ti, v)| (ti.t(), v)), + ), + } + } + + pub fn last_before(&self, t: i64) -> Option<(&i64, &A)> { + match self { + TCell::Empty => None, + TCell::TCell1(t2, v) => (t2.t() < &t).then_some((t2.t(), v)), + TCell::TCellCap(map) => map + .range(TimeIndexEntry::range(i64::MIN..t)) + .last() + .map(|(ti, v)| (ti.t(), v)), + TCell::TCellN(map) => map + .range(TimeIndexEntry::range(i64::MIN..t)) + .last() + .map(|(ti, v)| (ti.t(), v)), } } } @@ -107,19 +144,20 @@ impl TCell { #[cfg(test)] mod tcell_tests { use super::TCell; + use crate::{core::storage::timeindex::TimeIndexEntry, db::api::view::TimeIndex}; #[test] fn set_new_value_for_tcell_initialized_as_empty() { let mut tcell = TCell::default(); - tcell.set(16, String::from("lobster")); + tcell.set(TimeIndexEntry::start(16), String::from("lobster")); assert_eq!(tcell.iter().collect::>(), vec!["lobster"]); } #[test] fn every_new_update_to_the_same_prop_is_recorded_as_history() { - let mut tcell = TCell::new(1, "Pometry"); - tcell.set(2, "Pometry Inc."); + let mut tcell = TCell::new(TimeIndexEntry::start(1), "Pometry"); + tcell.set(TimeIndexEntry::start(2), "Pometry Inc."); assert_eq!( tcell.iter_t().collect::>(), @@ -129,8 +167,8 @@ mod tcell_tests { #[test] fn new_update_with_the_same_time_to_a_prop_is_ignored() { - let mut tcell = TCell::new(1, "Pometry"); - tcell.set(1, "Pometry Inc."); + let mut tcell = TCell::new(TimeIndexEntry::start(1), "Pometry"); + tcell.set(TimeIndexEntry::start(1), "Pometry Inc."); assert_eq!(tcell.iter_t().collect::>(), vec![(&1, &"Pometry")]); } @@ -145,14 +183,14 @@ mod tcell_tests { assert_eq!(tcell.iter_t().collect::>(), vec![]); - let tcell = TCell::new(3, "Pometry"); + let tcell = TCell::new(TimeIndexEntry::start(3), "Pometry"); assert_eq!(tcell.iter().collect::>(), vec![&"Pometry"]); assert_eq!(tcell.iter_t().collect::>(), vec![(&3, &"Pometry")]); - let mut tcell = TCell::new(2, "Pometry"); - tcell.set(1, "Inc. Pometry"); + let mut tcell = TCell::new(TimeIndexEntry::start(2), "Pometry"); + tcell.set(TimeIndexEntry::start(1), "Inc. Pometry"); assert_eq!( // Results are ordered by time @@ -168,7 +206,7 @@ mod tcell_tests { let mut tcell: TCell = TCell::default(); for n in 1..130 { - tcell.set(n, n) + tcell.set(TimeIndexEntry::start(n), n) } assert_eq!(tcell.iter_t().count(), 129); @@ -189,7 +227,7 @@ mod tcell_tests { vec![] ); - let tcell = TCell::new(3, "Pometry"); + let tcell = TCell::new(TimeIndexEntry::start(3), "Pometry"); assert_eq!( tcell.iter_window(3..4).collect::>(), @@ -201,9 +239,9 @@ mod tcell_tests { vec![(&3, &"Pometry")] ); - let mut tcell = TCell::new(3, "Pometry"); - tcell.set(1, "Pometry Inc."); - tcell.set(2, "Raphtory"); + let mut tcell = TCell::new(TimeIndexEntry::start(3), "Pometry"); + tcell.set(TimeIndexEntry::start(1), "Pometry Inc."); + tcell.set(TimeIndexEntry::start(2), "Raphtory"); assert_eq!( tcell.iter_window_t(2..3).collect::>(), @@ -241,7 +279,7 @@ mod tcell_tests { let mut tcell: TCell = TCell::default(); for n in 1..130 { - tcell.set(n, n) + tcell.set(TimeIndexEntry::start(n), n) } assert_eq!(tcell.iter_window_t(i64::MIN..i64::MAX).count(), 129); diff --git a/raphtory/src/core/entities/properties/tprop.rs b/raphtory/src/core/entities/properties/tprop.rs new file mode 100644 index 0000000000..4d10d7f66f --- /dev/null +++ b/raphtory/src/core/entities/properties/tprop.rs @@ -0,0 +1,564 @@ +use crate::{ + core::{ + entities::{ + properties::{props::DictMapper, tcell::TCell}, + LayerIds, + }, + storage::{locked_view::LockedView, timeindex::TimeIndexEntry}, + utils::errors::GraphError, + ArcStr, Prop, PropType, + }, + db::graph::graph::Graph, +}; +use chrono::NaiveDateTime; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use std::{collections::HashMap, iter, ops::Range, sync::Arc}; + +// TODO TProp struct could be replaced with Option>, with the only issue (or advantage) that then the type can change? + +#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)] +pub enum TProp { + #[default] + Empty, + Str(TCell), + U8(TCell), + U16(TCell), + I32(TCell), + I64(TCell), + U32(TCell), + U64(TCell), + F32(TCell), + F64(TCell), + Bool(TCell), + DTime(TCell), + Graph(TCell), + List(TCell>>), + Map(TCell>>), +} + +impl TProp { + pub fn dtype(&self) -> PropType { + match self { + TProp::Empty => PropType::Empty, + TProp::Str(_) => PropType::Str, + TProp::U8(_) => PropType::U8, + TProp::U16(_) => PropType::U16, + TProp::I32(_) => PropType::I32, + TProp::I64(_) => PropType::I64, + TProp::U32(_) => PropType::U32, + TProp::U64(_) => PropType::U64, + TProp::F32(_) => PropType::F32, + TProp::F64(_) => PropType::F64, + TProp::Bool(_) => PropType::Bool, + TProp::DTime(_) => PropType::DTime, + TProp::Graph(_) => PropType::Graph, + TProp::List(_) => PropType::List, + TProp::Map(_) => PropType::Map, + } + } + + pub(crate) fn from(t: TimeIndexEntry, prop: Prop) -> Self { + match prop { + Prop::Str(value) => TProp::Str(TCell::new(t, value)), + Prop::I32(value) => TProp::I32(TCell::new(t, value)), + Prop::I64(value) => TProp::I64(TCell::new(t, value)), + Prop::U8(value) => TProp::U8(TCell::new(t, value)), + Prop::U16(value) => TProp::U16(TCell::new(t, value)), + Prop::U32(value) => TProp::U32(TCell::new(t, value)), + Prop::U64(value) => TProp::U64(TCell::new(t, value)), + Prop::F32(value) => TProp::F32(TCell::new(t, value)), + Prop::F64(value) => TProp::F64(TCell::new(t, value)), + Prop::Bool(value) => TProp::Bool(TCell::new(t, value)), + Prop::DTime(value) => TProp::DTime(TCell::new(t, value)), + Prop::Graph(value) => TProp::Graph(TCell::new(t, value)), + Prop::List(value) => TProp::List(TCell::new(t, value)), + Prop::Map(value) => TProp::Map(TCell::new(t, value)), + } + } + + pub(crate) fn set(&mut self, t: TimeIndexEntry, prop: Prop) -> Result<(), GraphError> { + if matches!(self, TProp::Empty) { + *self = TProp::from(t, prop); + } else { + match (self, prop) { + (TProp::Empty, prop) => {} + + (TProp::Str(cell), Prop::Str(a)) => { + cell.set(t, a); + } + (TProp::I32(cell), Prop::I32(a)) => { + cell.set(t, a); + } + (TProp::I64(cell), Prop::I64(a)) => { + cell.set(t, a); + } + (TProp::U32(cell), Prop::U32(a)) => { + cell.set(t, a); + } + (TProp::U8(cell), Prop::U8(a)) => { + cell.set(t, a); + } + (TProp::U16(cell), Prop::U16(a)) => { + cell.set(t, a); + } + (TProp::U64(cell), Prop::U64(a)) => { + cell.set(t, a); + } + (TProp::F32(cell), Prop::F32(a)) => { + cell.set(t, a); + } + (TProp::F64(cell), Prop::F64(a)) => { + cell.set(t, a); + } + (TProp::Bool(cell), Prop::Bool(a)) => { + cell.set(t, a); + } + (TProp::DTime(cell), Prop::DTime(a)) => { + cell.set(t, a); + } + (TProp::Graph(cell), Prop::Graph(a)) => { + cell.set(t, a); + } + (TProp::List(cell), Prop::List(a)) => { + cell.set(t, a); + } + (TProp::Map(cell), Prop::Map(a)) => { + cell.set(t, a); + } + _ => return Err(GraphError::IncorrectPropertyType), + }; + } + Ok(()) + } + + pub(crate) fn at(&self, ti: &TimeIndexEntry) -> Option { + match self { + TProp::Empty => None, + TProp::Str(cell) => cell.at(ti).map(|v| Prop::Str(v.clone())), + TProp::I32(cell) => cell.at(ti).map(|v| Prop::I32(*v)), + TProp::I64(cell) => cell.at(ti).map(|v| Prop::I64(*v)), + TProp::U32(cell) => cell.at(ti).map(|v| Prop::U32(*v)), + TProp::U8(cell) => cell.at(ti).map(|v| Prop::U8(*v)), + TProp::U16(cell) => cell.at(ti).map(|v| Prop::U16(*v)), + TProp::U64(cell) => cell.at(ti).map(|v| Prop::U64(*v)), + TProp::F32(cell) => cell.at(ti).map(|v| Prop::F32(*v)), + TProp::F64(cell) => cell.at(ti).map(|v| Prop::F64(*v)), + TProp::Bool(cell) => cell.at(ti).map(|v| Prop::Bool(*v)), + TProp::DTime(cell) => cell.at(ti).map(|v| Prop::DTime(*v)), + TProp::Graph(cell) => cell.at(ti).map(|v| Prop::Graph(v.clone())), + TProp::List(cell) => cell.at(ti).map(|v| Prop::List(v.clone())), + TProp::Map(cell) => cell.at(ti).map(|v| Prop::Map(v.clone())), + } + } + + pub(crate) fn last_before(&self, t: i64) -> Option<(i64, Prop)> { + match self { + TProp::Empty => None, + TProp::Str(cell) => cell.last_before(t).map(|(t, v)| (*t, Prop::Str(v.clone()))), + TProp::I32(cell) => cell.last_before(t).map(|(t, v)| (*t, Prop::I32(*v))), + TProp::I64(cell) => cell.last_before(t).map(|(t, v)| (*t, Prop::I64(*v))), + TProp::U8(cell) => cell.last_before(t).map(|(t, v)| (*t, Prop::U8(*v))), + TProp::U16(cell) => cell.last_before(t).map(|(t, v)| (*t, Prop::U16(*v))), + TProp::U32(cell) => cell.last_before(t).map(|(t, v)| (*t, Prop::U32(*v))), + TProp::U64(cell) => cell.last_before(t).map(|(t, v)| (*t, Prop::U64(*v))), + TProp::F32(cell) => cell.last_before(t).map(|(t, v)| (*t, Prop::F32(*v))), + TProp::F64(cell) => cell.last_before(t).map(|(t, v)| (*t, Prop::F64(*v))), + TProp::Bool(cell) => cell.last_before(t).map(|(t, v)| (*t, Prop::Bool(*v))), + TProp::DTime(cell) => cell.last_before(t).map(|(t, v)| (*t, Prop::DTime(*v))), + TProp::Graph(cell) => cell + .last_before(t) + .map(|(t, v)| (*t, Prop::Graph(v.clone()))), + TProp::List(cell) => cell + .last_before(t) + .map(|(t, v)| (*t, Prop::List(v.clone()))), + TProp::Map(cell) => cell.last_before(t).map(|(t, v)| (*t, Prop::Map(v.clone()))), + } + } + + pub(crate) fn iter(&self) -> Box + '_> { + match self { + TProp::Empty => Box::new(iter::empty()), + TProp::Str(cell) => Box::new( + cell.iter_t() + .map(|(t, value)| (*t, Prop::Str(value.clone()))), + ), + TProp::I32(cell) => Box::new(cell.iter_t().map(|(t, value)| (*t, Prop::I32(*value)))), + TProp::I64(cell) => Box::new(cell.iter_t().map(|(t, value)| (*t, Prop::I64(*value)))), + TProp::U8(cell) => Box::new(cell.iter_t().map(|(t, value)| (*t, Prop::U8(*value)))), + TProp::U16(cell) => Box::new(cell.iter_t().map(|(t, value)| (*t, Prop::U16(*value)))), + TProp::U32(cell) => Box::new(cell.iter_t().map(|(t, value)| (*t, Prop::U32(*value)))), + TProp::U64(cell) => Box::new(cell.iter_t().map(|(t, value)| (*t, Prop::U64(*value)))), + TProp::F32(cell) => Box::new(cell.iter_t().map(|(t, value)| (*t, Prop::F32(*value)))), + TProp::F64(cell) => Box::new(cell.iter_t().map(|(t, value)| (*t, Prop::F64(*value)))), + TProp::Bool(cell) => Box::new(cell.iter_t().map(|(t, value)| (*t, Prop::Bool(*value)))), + TProp::DTime(cell) => { + Box::new(cell.iter_t().map(|(t, value)| (*t, Prop::DTime(*value)))) + } + TProp::Graph(cell) => Box::new( + cell.iter_t() + .map(|(t, value)| (*t, Prop::Graph(value.clone()))), + ), + TProp::List(cell) => Box::new( + cell.iter_t() + .map(|(t, value)| (*t, Prop::List(value.clone()))), + ), + TProp::Map(cell) => Box::new( + cell.iter_t() + .map(|(t, value)| (*t, Prop::Map(value.clone()))), + ), + } + } + + pub(crate) fn iter_window(&self, r: Range) -> Box + '_> { + match self { + TProp::Empty => Box::new(std::iter::empty()), + TProp::Str(cell) => Box::new( + cell.iter_window_t(r) + .map(|(t, value)| (*t, Prop::Str(value.clone()))), + ), + TProp::I32(cell) => Box::new( + cell.iter_window_t(r) + .map(|(t, value)| (*t, Prop::I32(*value))), + ), + TProp::I64(cell) => Box::new( + cell.iter_window_t(r) + .map(|(t, value)| (*t, Prop::I64(*value))), + ), + TProp::U8(cell) => Box::new( + cell.iter_window_t(r) + .map(|(t, value)| (*t, Prop::U8(*value))), + ), + TProp::U16(cell) => Box::new( + cell.iter_window_t(r) + .map(|(t, value)| (*t, Prop::U16(*value))), + ), + TProp::U32(cell) => Box::new( + cell.iter_window_t(r) + .map(|(t, value)| (*t, Prop::U32(*value))), + ), + TProp::U64(cell) => Box::new( + cell.iter_window_t(r) + .map(|(t, value)| (*t, Prop::U64(*value))), + ), + TProp::F32(cell) => Box::new( + cell.iter_window_t(r) + .map(|(t, value)| (*t, Prop::F32(*value))), + ), + TProp::F64(cell) => Box::new( + cell.iter_window_t(r) + .map(|(t, value)| (*t, Prop::F64(*value))), + ), + TProp::Bool(cell) => Box::new( + cell.iter_window_t(r) + .map(|(t, value)| (*t, Prop::Bool(*value))), + ), + TProp::DTime(cell) => Box::new( + cell.iter_window_t(r) + .map(|(t, value)| (*t, Prop::DTime(*value))), + ), + TProp::Graph(cell) => Box::new( + cell.iter_window_t(r) + .map(|(t, value)| (*t, Prop::Graph(value.clone()))), + ), + TProp::List(cell) => Box::new( + cell.iter_window_t(r) + .map(|(t, value)| (*t, Prop::List(value.clone()))), + ), + TProp::Map(cell) => Box::new( + cell.iter_window_t(r) + .map(|(t, value)| (*t, Prop::Map(value.clone()))), + ), + } + } +} + +pub struct LockedLayeredTProp<'a> { + tprop: Vec>, +} + +impl<'a> LockedLayeredTProp<'a> { + pub(crate) fn new(tprop: Vec>) -> Self { + Self { tprop } + } + + pub(crate) fn last_before(&self, t: i64) -> Option<(i64, Prop)> { + self.tprop + .iter() + .flat_map(|p| p.last_before(t)) + .max_by_key(|v| v.0) + } + + pub(crate) fn iter(&self) -> impl Iterator + '_ { + self.tprop + .iter() + .map(|p| p.iter()) + .kmerge_by(|a, b| a.0 < b.0) + } + + pub(crate) fn iter_window(&self, r: Range) -> impl Iterator + '_ { + self.tprop + .iter() + .map(|p| p.iter_window(r.clone())) + .kmerge_by(|a, b| a.0 < b.0) + } + + pub(crate) fn at(&self, ti: &TimeIndexEntry) -> Option { + self.tprop.iter().find_map(|p| p.at(ti)) + } +} + +#[cfg(test)] +mod tprop_tests { + use super::*; + + #[test] + fn set_new_value_for_tprop_initialized_as_empty() { + let mut tprop = TProp::Empty; + tprop.set(1.into(), Prop::I32(10)); + + assert_eq!(tprop.iter().collect::>(), vec![(1, Prop::I32(10))]); + } + + #[test] + fn every_new_update_to_the_same_prop_is_recorded_as_history() { + let mut tprop = TProp::from(1.into(), "Pometry".into()); + tprop.set(2.into(), "Pometry Inc.".into()); + + assert_eq!( + tprop.iter().collect::>(), + vec![(1, "Pometry".into()), (2, "Pometry Inc.".into())] + ); + } + + #[test] + fn new_update_with_the_same_time_to_a_prop_is_ignored() { + let mut tprop = TProp::from(1.into(), "Pometry".into()); + tprop.set(1.into(), "Pometry Inc.".into()); + + assert_eq!( + tprop.iter().collect::>(), + vec![(1, "Pometry".into())] + ); + } + + #[test] + fn updates_to_prop_can_be_iterated() { + let tprop = TProp::default(); + + assert_eq!(tprop.iter().collect::>(), vec![]); + + let mut tprop = TProp::from(1.into(), "Pometry".into()); + tprop.set(2.into(), "Pometry Inc.".into()); + + assert_eq!( + tprop.iter().collect::>(), + vec![ + (1, Prop::Str("Pometry".into())), + (2, Prop::Str("Pometry Inc.".into())) + ] + ); + + let mut tprop = TProp::from(1.into(), Prop::I32(2022)); + tprop.set(2.into(), Prop::I32(2023)); + + assert_eq!( + tprop.iter().collect::>(), + vec![(1, Prop::I32(2022)), (2, Prop::I32(2023))] + ); + + let mut tprop = TProp::from(1.into(), Prop::I64(2022)); + tprop.set(2.into(), Prop::I64(2023)); + + assert_eq!( + tprop.iter().collect::>(), + vec![(1, Prop::I64(2022)), (2, Prop::I64(2023))] + ); + + let mut tprop = TProp::from(1.into(), Prop::F32(10.0)); + tprop.set(2.into(), Prop::F32(11.0)); + + assert_eq!( + tprop.iter().collect::>(), + vec![(1, Prop::F32(10.0)), (2, Prop::F32(11.0))] + ); + + let mut tprop = TProp::from(1.into(), Prop::F64(10.0)); + tprop.set(2.into(), Prop::F64(11.0)); + + assert_eq!( + tprop.iter().collect::>(), + vec![(1, Prop::F64(10.0)), (2, Prop::F64(11.0))] + ); + + let mut tprop = TProp::from(1.into(), Prop::U32(1)); + tprop.set(2.into(), Prop::U32(2)); + + assert_eq!( + tprop.iter().collect::>(), + vec![(1, Prop::U32(1)), (2, Prop::U32(2))] + ); + + let mut tprop = TProp::from(1.into(), Prop::U64(1)); + tprop.set(2.into(), Prop::U64(2)); + + assert_eq!( + tprop.iter().collect::>(), + vec![(1, Prop::U64(1)), (2, Prop::U64(2))] + ); + + let mut tprop = TProp::from(1.into(), Prop::U8(1)); + tprop.set(2.into(), Prop::U8(2)); + + assert_eq!( + tprop.iter().collect::>(), + vec![(1, Prop::U8(1)), (2, Prop::U8(2))] + ); + + let mut tprop = TProp::from(1.into(), Prop::U16(1)); + tprop.set(2.into(), Prop::U16(2)); + + assert_eq!( + tprop.iter().collect::>(), + vec![(1, Prop::U16(1)), (2, Prop::U16(2))] + ); + + let mut tprop = TProp::from(1.into(), Prop::Bool(true)); + tprop.set(2.into(), Prop::Bool(true)); + + assert_eq!( + tprop.iter().collect::>(), + vec![(1, Prop::Bool(true)), (2, Prop::Bool(true))] + ); + } + + #[test] + fn updates_to_prop_can_be_window_iterated() { + let tprop = TProp::default(); + + assert_eq!( + tprop.iter_window(i64::MIN..i64::MAX).collect::>(), + vec![] + ); + + let mut tprop = TProp::from(3.into(), Prop::Str("Pometry".into())); + tprop.set(1.into(), Prop::Str("Pometry Inc.".into())); + tprop.set(2.into(), Prop::Str("Raphtory".into())); + + assert_eq!( + tprop.iter_window(2..3).collect::>(), + vec![(2, Prop::Str("Raphtory".into()))] + ); + + assert_eq!(tprop.iter_window(4..5).collect::>(), vec![]); + + assert_eq!( + // Results are ordered by time + tprop.iter_window(1..i64::MAX).collect::>(), + vec![ + (1, Prop::Str("Pometry Inc.".into())), + (2, Prop::Str("Raphtory".into())), + (3, Prop::Str("Pometry".into())) + ] + ); + + assert_eq!( + tprop.iter_window(3..i64::MAX).collect::>(), + vec![(3, Prop::Str("Pometry".into()))] + ); + + assert_eq!( + tprop.iter_window(2..i64::MAX).collect::>(), + vec![ + (2, Prop::Str("Raphtory".into())), + (3, Prop::Str("Pometry".into())) + ] + ); + + assert_eq!(tprop.iter_window(5..i64::MAX).collect::>(), vec![]); + + assert_eq!( + tprop.iter_window(i64::MIN..4).collect::>(), + // Results are ordered by time + vec![ + (1, Prop::Str("Pometry Inc.".into())), + (2, Prop::Str("Raphtory".into())), + (3, Prop::Str("Pometry".into())) + ] + ); + + assert_eq!(tprop.iter_window(i64::MIN..1).collect::>(), vec![]); + + let mut tprop = TProp::from(1.into(), Prop::I32(2022)); + tprop.set(2.into(), Prop::I32(2023)); + + assert_eq!( + tprop.iter_window(i64::MIN..i64::MAX).collect::>(), + vec![(1, Prop::I32(2022)), (2, Prop::I32(2023))] + ); + + let mut tprop = TProp::from(1.into(), Prop::I64(2022)); + tprop.set(2.into(), Prop::I64(2023)); + + assert_eq!( + tprop.iter_window(i64::MIN..i64::MAX).collect::>(), + vec![(1, Prop::I64(2022)), (2, Prop::I64(2023))] + ); + + let mut tprop = TProp::from(1.into(), Prop::F32(10.0)); + tprop.set(2.into(), Prop::F32(11.0)); + + assert_eq!( + tprop.iter_window(i64::MIN..i64::MAX).collect::>(), + vec![(1, Prop::F32(10.0)), (2, Prop::F32(11.0))] + ); + + let mut tprop = TProp::from(1.into(), Prop::F64(10.0)); + tprop.set(2.into(), Prop::F64(11.0)); + + assert_eq!( + tprop.iter_window(i64::MIN..i64::MAX).collect::>(), + vec![(1, Prop::F64(10.0)), (2, Prop::F64(11.0))] + ); + + let mut tprop = TProp::from(1.into(), Prop::U32(1)); + tprop.set(2.into(), Prop::U32(2)); + + assert_eq!( + tprop.iter_window(i64::MIN..i64::MAX).collect::>(), + vec![(1, Prop::U32(1)), (2, Prop::U32(2))] + ); + + let mut tprop = TProp::from(1.into(), Prop::U64(1)); + tprop.set(2.into(), Prop::U64(2)); + + assert_eq!( + tprop.iter_window(i64::MIN..i64::MAX).collect::>(), + vec![(1, Prop::U64(1)), (2, Prop::U64(2))] + ); + + let mut tprop = TProp::from(1.into(), Prop::U8(1)); + tprop.set(2.into(), Prop::U8(2)); + + assert_eq!( + tprop.iter_window(i64::MIN..i64::MAX).collect::>(), + vec![(1, Prop::U8(1)), (2, Prop::U8(2))] + ); + + let mut tprop = TProp::from(1.into(), Prop::U16(1)); + tprop.set(2.into(), Prop::U16(2)); + + assert_eq!( + tprop.iter_window(i64::MIN..i64::MAX).collect::>(), + vec![(1, Prop::U16(1)), (2, Prop::U16(2))] + ); + + let mut tprop = TProp::from(1.into(), Prop::Bool(true)); + tprop.set(2.into(), Prop::Bool(true)); + + assert_eq!( + tprop.iter_window(i64::MIN..i64::MAX).collect::>(), + vec![(1, Prop::Bool(true)), (2, Prop::Bool(true))] + ); + } +} diff --git a/raphtory/src/core/vertex.rs b/raphtory/src/core/entities/vertices/input_vertex.rs similarity index 64% rename from raphtory/src/core/vertex.rs rename to raphtory/src/core/entities/vertices/input_vertex.rs index 22d5ae364f..ceb6f0511b 100644 --- a/raphtory/src/core/vertex.rs +++ b/raphtory/src/core/entities/vertices/input_vertex.rs @@ -4,11 +4,11 @@ //! This trait allows you to use a variety of types as input vertices, including //! `u64`, `&str`, and `String`. -use crate::core::{utils, Prop}; +use crate::core::utils::hashing; pub trait InputVertex: Clone { fn id(&self) -> u64; - fn name_prop(&self) -> Option; + fn id_str(&self) -> Option<&str>; } impl InputVertex for u64 { @@ -16,18 +16,18 @@ impl InputVertex for u64 { *self } - fn name_prop(&self) -> Option { + fn id_str(&self) -> Option<&str> { None } } impl<'a> InputVertex for &'a str { fn id(&self) -> u64 { - self.parse().unwrap_or(utils::calculate_hash(self)) + self.parse().unwrap_or(hashing::calculate_hash(self)) } - fn name_prop(&self) -> Option { - Some(Prop::Str(self.to_string())) + fn id_str(&self) -> Option<&str> { + Some(self) } } @@ -37,7 +37,7 @@ impl InputVertex for String { s.id() } - fn name_prop(&self) -> Option { - Some(Prop::Str(self.to_string())) + fn id_str(&self) -> Option<&str> { + Some(self) } } diff --git a/raphtory/src/core/entities/vertices/mod.rs b/raphtory/src/core/entities/vertices/mod.rs new file mode 100644 index 0000000000..3ad56d82de --- /dev/null +++ b/raphtory/src/core/entities/vertices/mod.rs @@ -0,0 +1,5 @@ +pub mod input_vertex; +pub mod structure; +pub mod vertex; +pub mod vertex_ref; +pub mod vertex_store; diff --git a/raphtory/src/core/entities/vertices/structure/adj.rs b/raphtory/src/core/entities/vertices/structure/adj.rs new file mode 100644 index 0000000000..154ef88006 --- /dev/null +++ b/raphtory/src/core/entities/vertices/structure/adj.rs @@ -0,0 +1,155 @@ +use crate::core::{ + entities::{ + edges::edge_ref::{Dir, EdgeRef}, + vertices::structure::adjset::AdjSet, + EID, VID, + }, + Direction, +}; +use core::panic; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, PartialEq, Default)] +pub enum Adj { + #[default] + Solo, + List { + // local: + out: AdjSet, + into: AdjSet, + }, +} + +impl Adj { + pub(crate) fn get_edge(&self, v: VID, dir: Direction) -> Option { + match self { + Adj::Solo => None, + Adj::List { out, into } => match dir { + Direction::OUT => out.find(v), + Direction::IN => into.find(v), + Direction::BOTH => self + .get_edge(v, Direction::OUT) + .or_else(|| self.get_edge(v, Direction::IN)), + }, + } + } + + pub(crate) fn new_out(v: VID, e: EID) -> Self { + Adj::List { + out: AdjSet::new(v, e), + into: AdjSet::default(), + } + } + + pub(crate) fn new_into(v: VID, e: EID) -> Self { + Adj::List { + into: AdjSet::new(v, e), + out: AdjSet::default(), + } + } + + pub(crate) fn add_edge_into(&mut self, v: VID, e: EID) { + match self { + Adj::Solo => *self = Self::new_into(v, e), + Adj::List { into, .. } => into.push(v, e), + } + } + + pub(crate) fn add_edge_out(&mut self, v: VID, e: EID) { + match self { + Adj::Solo => *self = Self::new_out(v, e), + Adj::List { out, .. } => out.push(v, e), + } + } + + pub(crate) fn iter(&self, dir: Direction) -> Box + Send + '_> { + match self { + Adj::Solo => Box::new(std::iter::empty()), + Adj::List { out, into } => match dir { + Direction::OUT => Box::new(out.iter()), + Direction::IN => Box::new(into.iter()), + Direction::BOTH => Box::new(out.iter().merge(into.iter())), + }, + } + } + + pub(crate) fn iter_eref( + &self, + dir: Direction, + local: VID, + ) -> Box + Send + '_> { + match self { + Adj::Solo => Box::new(std::iter::empty()), + Adj::List { out, into } => match dir { + Direction::OUT => Box::new( + out.iter() + .map(move |(remote, e)| EdgeRef::new(e, local, remote, Dir::Out)), + ), + Direction::IN => Box::new( + into.iter() + .map(move |(remote, e)| EdgeRef::new(e, local, remote, Dir::Into)), + ), + Direction::BOTH => Box::new( + out.iter() + .map(move |(remote, e)| EdgeRef::new(e, local, remote, Dir::Out)) + .merge( + into.iter() + .map(move |(remote, e)| EdgeRef::new(e, local, remote, Dir::Into)), + ), + ), + }, + } + } + + pub(crate) fn vertex_iter(&self, dir: Direction) -> impl Iterator + Send + '_ { + self.iter(dir).map(|(v, _)| v) + } + + pub(crate) fn degree(&self, dir: Direction) -> usize { + match self { + Adj::Solo => 0, + Adj::List { out, into } => match dir { + Direction::OUT => out.len(), + Direction::IN => into.len(), + Direction::BOTH => out + .iter() + .merge(into.iter()) + .dedup_by(|v1, v2| v1.0 == v2.0) + .count(), + }, + } + } + + pub fn fill_page( + &self, + last: Option, + page: &mut [(VID, EID); P], + dir: Dir, + ) -> usize { + match self { + Adj::Solo => 0, + Adj::List { out, into } => match dir { + Dir::Out => out.fill_page(last, page), + Dir::Into => into.fill_page(last, page), + }, + } + } + pub(crate) fn get_page_vec( + &self, + last: Option, + page_size: usize, + dir: Direction, + ) -> Vec<(VID, EID)> { + match self { + Adj::Solo => Vec::new(), + Adj::List { out, into } => match dir { + Direction::OUT => out.get_page_vec(last, page_size), + Direction::IN => into.get_page_vec(last, page_size), + _ => panic!( + "Cannot get page vec for both direction, need to be handled by the caller" + ), + }, + } + } +} diff --git a/raphtory/src/core/entities/vertices/structure/adjset.rs b/raphtory/src/core/entities/vertices/structure/adjset.rs new file mode 100644 index 0000000000..7789737989 --- /dev/null +++ b/raphtory/src/core/entities/vertices/structure/adjset.rs @@ -0,0 +1,304 @@ +//! A data structure for efficiently storing and querying the temporal adjacency set of a node in a temporal graph. + +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use std::{collections::BTreeMap, hash::Hash}; + +const SMALL_SET: usize = 1024; + +/** + * Temporal adjacency set can track when adding edge v -> u + * does u exist already + * and if it does what is the edge metadata + * + * */ +#[derive(Debug, Default, Serialize, Deserialize, PartialEq)] +pub enum AdjSet + Copy + Send + Sync> { + #[default] + Empty, + One(K, V), + Small { + vs: Vec, // the neighbours + edges: Vec, // edge metadata + }, + Large { + vs: BTreeMap, // this is equiv to vs and edges + }, + // TODO: if we use BTreeSet<(K, Option)> we could implement intersections and support edge label queries such as a && b +} + +impl + Copy + Send + Sync> AdjSet { + pub fn len(&self) -> usize { + match self { + AdjSet::Empty => 0, + AdjSet::One(_, _) => 1, + AdjSet::Small { vs, .. } => vs.len(), + AdjSet::Large { vs } => vs.len(), + } + } + + pub fn is_empty(&self) -> bool { + match self { + AdjSet::Empty => true, + AdjSet::One(_, _) => false, + AdjSet::Small { vs, .. } => vs.is_empty(), + AdjSet::Large { vs } => vs.is_empty(), + } + } + pub fn new(v: K, e: V) -> Self { + Self::One(v, e) + } + + pub fn push(&mut self, v: K, e: V) { + match self { + AdjSet::Empty => { + *self = Self::new(v, e); + } + AdjSet::One(vv, ee) => { + if *vv < v { + *self = Self::Small { + vs: vec![*vv, v], + edges: vec![*ee, e], + } + } else if *vv > v { + *self = Self::Small { + vs: vec![v, *vv], + edges: vec![e, *ee], + } + } + } + AdjSet::Small { vs, edges } => match vs.binary_search(&v) { + Ok(_) => {} + Err(i) => { + if vs.len() < SMALL_SET { + vs.insert(i, v); + edges.insert(i, e); + } else { + let mut map = + BTreeMap::from_iter(vs.iter().copied().zip(edges.iter().copied())); + map.insert(v, e); + *self = Self::Large { vs: map } + } + } + }, + AdjSet::Large { vs } => { + vs.insert(v, e); + } + } + } + + pub fn iter(&self) -> Box + Send + '_> { + match self { + AdjSet::Empty => Box::new(std::iter::empty()), + AdjSet::One(v, e) => Box::new(std::iter::once((*v, *e))), + AdjSet::Small { vs, edges } => Box::new(vs.iter().copied().zip(edges.iter().copied())), + AdjSet::Large { vs } => Box::new(vs.iter().map(|(k, v)| (*k, *v))), + } + } + + pub fn vertices(&self) -> Box + Send + '_> { + match self { + AdjSet::Empty => Box::new(std::iter::empty()), + AdjSet::One(v, ..) => Box::new(std::iter::once(*v)), + AdjSet::Small { vs, .. } => Box::new(vs.iter().copied()), + AdjSet::Large { vs } => Box::new(vs.keys().copied()), + } + } + + pub fn find(&self, v: K) -> Option { + match self { + AdjSet::Empty => None, + AdjSet::One(vv, e) => (*vv == v).then_some(*e), + AdjSet::Small { vs, edges } => vs.binary_search(&v).ok().map(|i| edges[i]), + AdjSet::Large { vs } => vs.get(&v).copied(), + } + } + + /// puts elements into page and returns number of returned elements + pub fn fill_page(&self, last: Option, page: &mut [(K, V); P]) -> usize { + match self { + AdjSet::Empty => 0, + AdjSet::One(v, i) => { + if let Some(l) = last { + if l < *v { + page[0] = (*v, *i); + 1 + } else { + 0 + } + } else { + page[0] = (*v, *i); + 1 + } + } + AdjSet::Small { vs, edges } => { + if let Some(l) = last { + let i = match vs.binary_search(&l) { + Ok(i) => i + 1, + Err(i) => i, + }; + + if i >= vs.len() { + return 0; + } + + let mut index = 0; + vs[i..] + .iter() + .zip(edges[i..].iter()) + .take(P) + .for_each(|(a, b)| { + page[index] = (*a, *b); + index += 1; + }); + index + } else { + let mut index = 0; + vs.iter().zip(edges.iter()).take(P).for_each(|(a, b)| { + page[index] = (*a, *b); + index += 1; + }); + index + } + } + AdjSet::Large { vs } => { + if let Some(l) = last { + let mut index = 0; + vs.range(l..).skip(1).take(P).for_each(|(a, b)| { + page[index] = (*a, *b); + index += 1; + }); + index + } else { + let mut index = 0; + vs.iter().take(P).for_each(|(a, b)| { + page[index] = (*a, *b); + index += 1 + }); + index + } + } + } + } + pub fn get_page_vec(&self, last: Option, page_size: usize) -> Vec<(K, V)> { + match self { + AdjSet::Empty => vec![], + AdjSet::One(v, i) => { + if let Some(l) = last { + if l < *v { + vec![(*v, *i)] + } else { + vec![] + } + } else { + vec![(*v, *i)] + } + } + AdjSet::Small { vs, edges } => { + if let Some(l) = last { + let i = match vs.binary_search(&l) { + Ok(i) => i + 1, + Err(i) => i, + }; + + if i >= vs.len() { + return vec![]; + } + + vs[i..] + .iter() + .zip(edges[i..].iter()) + .take(page_size) + .map(|(a, b)| (*a, *b)) + .collect() + } else { + vs.iter() + .zip(edges.iter()) + .take(page_size) + .map(|(a, b)| (*a, *b)) + .collect() + } + } + AdjSet::Large { vs } => { + if let Some(l) = last { + vs.range(l..) + .skip(1) + .take(page_size) + .map(|(a, b)| (*a, *b)) + .collect() + } else { + vs.iter().take(page_size).map(|(a, b)| (*a, *b)).collect() + } + } + } + } +} + +#[cfg(test)] +mod tadjset_tests { + use super::*; + + #[quickcheck] + fn insert_fuzz(input: Vec) -> bool { + let mut ts: AdjSet = AdjSet::default(); + + for (e, i) in input.iter().enumerate() { + ts.push(*i, e); + } + + let res = input.iter().all(|i| ts.find(*i).is_some()); + if !res { + let ts_vec: Vec<(usize, usize)> = ts.iter().collect(); + println!("Input: {:?}", input); + println!("TAdjSet: {:?}", ts_vec); + } + res + } + + #[test] + fn insert() { + let mut ts: AdjSet = AdjSet::default(); + + ts.push(7, 5); + let actual = ts.iter().collect::>(); + let expected: Vec<(usize, usize)> = vec![(7, 5)]; + assert_eq!(actual, expected) + } + + #[test] + fn insert_large() { + let mut ts: AdjSet = AdjSet::default(); + + for i in 0..SMALL_SET + 2 { + ts.push(i, i); + } + + for i in 0..SMALL_SET + 2 { + assert_eq!(ts.find(i), Some(i)); + } + } + + #[test] + fn insert_twice() { + let mut ts: AdjSet = AdjSet::default(); + + ts.push(7, 9); + ts.push(7, 9); + + let actual = ts.iter().collect::>(); + let expected: Vec<(usize, usize)> = vec![(7, 9)]; + assert_eq!(actual, expected); + } + + #[test] + fn insert_two_different() { + let mut ts: AdjSet = AdjSet::default(); + + ts.push(1, 0); + ts.push(7, 1); + + let actual = ts.iter().collect::>(); + let expected: Vec<(usize, usize)> = vec![(1, 0), (7, 1)]; + assert_eq!(actual, expected); + } +} diff --git a/raphtory/src/core/entities/vertices/structure/iter.rs b/raphtory/src/core/entities/vertices/structure/iter.rs new file mode 100644 index 0000000000..a54e0afa4b --- /dev/null +++ b/raphtory/src/core/entities/vertices/structure/iter.rs @@ -0,0 +1,92 @@ +use crate::core::{ + entities::{edges::edge::EdgeView, graph::tgraph::TGraph, VRef, EID, VID}, + Direction, +}; +use itertools::Merge; +use std::sync::Arc; + +pub struct Paged<'a, const N: usize> { + guard: Arc>, + data: Vec<(VID, EID)>, + i: usize, + size: usize, + dir: Direction, + layer_id: usize, + src: VID, + graph: &'a TGraph, +} + +impl<'a, const N: usize> Paged<'a, N> { + pub(crate) fn new( + guard: Arc>, + dir: Direction, + layer_id: usize, + src: VID, + graph: &'a TGraph, + ) -> Self { + Paged { + guard, + data: Vec::new(), + i: 0, + size: 16, + dir, + layer_id, + src, + graph, + } + } +} + +impl<'a, const N: usize> Iterator for Paged<'a, N> { + type Item = EdgeView<'a, N>; + + fn next(&mut self) -> Option { + if let Some(t) = self.data.get(self.i) { + self.i += 1; + let e_id = self.guard.edge_ref(t.1, self.graph); + let edge = EdgeView::from_edge_ids(self.src, t.0, e_id, self.dir, self.graph); + return Some(edge); + } + + if let Some(last) = self.data.last() { + self.data = self + .guard + .edges_from_last(self.layer_id, self.dir, Some(last.0), self.size) + } else { + // fetch the first page + self.data = self + .guard + .edges_from_last(self.layer_id, self.dir, None, self.size) + } + + if self.data.is_empty() { + return None; + } else { + self.i = 1; + let e_id = self.guard.edge_ref(self.data[0].1, self.graph); + return Some(EdgeView::from_edge_ids( + self.src, + self.data[0].0, + e_id, + self.dir, + self.graph, + )); + } + } +} + +pub enum PagedIter<'a, const N: usize> { + Page(Paged<'a, N>), + Merged(Merge, Paged<'a, N>>), +} + +impl<'a, const N: usize> Iterator for PagedIter<'a, N> { + type Item = EdgeView<'a, N>; + + fn next(&mut self) -> Option { + match self { + PagedIter::Page(p) => p.next(), + PagedIter::Merged(c) => c.next(), + } + } +} diff --git a/raphtory/src/core/entities/vertices/structure/mod.rs b/raphtory/src/core/entities/vertices/structure/mod.rs new file mode 100644 index 0000000000..9eaadd3beb --- /dev/null +++ b/raphtory/src/core/entities/vertices/structure/mod.rs @@ -0,0 +1,3 @@ +pub mod adj; +pub mod adjset; +pub mod iter; diff --git a/raphtory/src/core/entities/vertices/vertex.rs b/raphtory/src/core/entities/vertices/vertex.rs new file mode 100644 index 0000000000..346aa97066 --- /dev/null +++ b/raphtory/src/core/entities/vertices/vertex.rs @@ -0,0 +1,169 @@ +use crate::core::{ + entities::{ + edges::{edge::EdgeView, edge_ref::EdgeRef, edge_store::EdgeStore}, + graph::tgraph::TGraph, + properties::{ + props::{DictMapper, Meta}, + tprop::TProp, + }, + vertices::{ + structure::iter::{Paged, PagedIter}, + vertex_store::VertexStore, + }, + LayerIds, VRef, VID, + }, + storage::{ + locked_view::LockedView, + timeindex::{TimeIndex, TimeIndexEntry, TimeIndexOps}, + ArcEntry, Entry, + }, + Direction, Prop, +}; +use itertools::Itertools; +use std::{ops::Range, sync::Arc}; + +pub struct Vertex<'a, const N: usize> { + node: VRef<'a, N>, + pub graph: &'a TGraph, +} + +impl<'a, const N: usize> Vertex<'a, N> { + pub fn id(&self) -> VID { + self.node.index().into() + } + + pub(crate) fn new(node: VRef<'a, N>, graph: &'a TGraph) -> Self { + Vertex { node, graph } + } + + pub(crate) fn from_entry(node: Entry<'a, VertexStore, N>, graph: &'a TGraph) -> Self { + Self::new(VRef::Entry(node), graph) + } + + pub fn temporal_properties( + &'a self, + prop_id: usize, + window: Option>, + ) -> impl Iterator + 'a { + self.node.temporal_properties(prop_id, window) + } + + pub fn neighbours<'b>( + &'a self, + layers: Vec<&'b str>, + dir: Direction, + ) -> impl Iterator> + 'a { + let layer_ids = layers + .iter() + .filter_map(|str| self.graph.vertex_meta.get_layer_id(str)) + .collect_vec(); + + (*self.node) + .neighbours(layer_ids.into(), dir) + .map(move |dst| self.graph.vertex(dst)) + } + + pub(crate) fn additions(self) -> Option>> { + match self.node { + VRef::Entry(entry) => { + let t_index = entry.map(|entry| entry.timestamps()); + Some(t_index) + } + _ => None, + } + } + + pub(crate) fn temporal_property(self, prop_id: usize) -> Option> { + match self.node { + VRef::Entry(entry) => { + entry.temporal_property(prop_id)?; + + let t_index = entry.map(|entry| entry.temporal_property(prop_id).unwrap()); + Some(t_index) + } + _ => None, + } + } +} + +impl<'a, const N: usize> IntoIterator for Vertex<'a, N> { + type Item = Vertex<'a, N>; + type IntoIter = std::iter::Once; + + fn into_iter(self) -> Self::IntoIter { + std::iter::once(self) + } +} + +pub struct ArcVertex { + e: ArcEntry, + meta: Arc, +} + +impl ArcVertex { + pub(crate) fn from_entry(e: ArcEntry, meta: Arc) -> Self { + ArcVertex { e, meta } + } + + pub fn edge_tuples( + &self, + layers: LayerIds, + dir: Direction, + ) -> impl Iterator + '_ { + self.e.edge_tuples(&layers, dir) + } + + pub fn neighbours(&self, layers: LayerIds, dir: Direction) -> impl Iterator + '_ { + self.e.neighbours(layers, dir) + } +} + +pub(crate) struct ArcEdge { + e: ArcEntry, + meta: Arc, +} + +impl ArcEdge { + pub(crate) fn from_entry(e: ArcEntry, meta: Arc) -> Self { + ArcEdge { e, meta } + } + + pub(crate) fn timestamps_and_layers( + &self, + layer: LayerIds, + ) -> impl Iterator + Send + '_ { + let adds = self.e.additions(); + adds.iter() + .enumerate() + .filter_map(|(layer_id, t)| { + layer + .find(layer_id) + .map(|l| t.iter().map(move |tt| (l, tt))) + }) + .kmerge_by(|a, b| a.1 < b.1) + } + + pub(crate) fn layers(&self) -> impl Iterator + '_ { + self.e.layer_ids_iter() + } + + pub(crate) fn layers_window(&self, w: Range) -> impl Iterator + '_ { + self.e.layer_ids_window_iter(w) + } + + pub(crate) fn timestamps_and_layers_window( + &self, + layer: LayerIds, + w: Range, + ) -> impl Iterator + '_ { + let adds = self.e.additions(); + adds.iter() + .enumerate() + .filter_map(|(layer_id, t)| { + layer + .find(layer_id) + .map(|l| t.range_iter(w.clone()).map(move |tt| (l, tt))) + }) + .kmerge_by(|a, b| a.1 < b.1) + } +} diff --git a/raphtory/src/core/entities/vertices/vertex_ref.rs b/raphtory/src/core/entities/vertices/vertex_ref.rs new file mode 100644 index 0000000000..8720f9aada --- /dev/null +++ b/raphtory/src/core/entities/vertices/vertex_ref.rs @@ -0,0 +1,27 @@ +use crate::core::entities::{vertices::input_vertex::InputVertex, VID}; + +#[derive(Copy, Clone, PartialOrd, PartialEq, Debug)] +pub enum VertexRef { + Internal(VID), + External(u64), +} + +impl VertexRef { + /// Makes a new vertex reference from an internal `VID`. + /// Values are unchecked and the vertex is assumed to exist so use with caution! + pub fn new(vid: VID) -> Self { + VertexRef::Internal(vid) + } +} + +impl From for VertexRef { + fn from(value: V) -> Self { + VertexRef::External(value.id()) + } +} + +impl From for VertexRef { + fn from(value: VID) -> Self { + VertexRef::Internal(value) + } +} diff --git a/raphtory/src/core/entities/vertices/vertex_store.rs b/raphtory/src/core/entities/vertices/vertex_store.rs new file mode 100644 index 0000000000..b49565f8a9 --- /dev/null +++ b/raphtory/src/core/entities/vertices/vertex_store.rs @@ -0,0 +1,458 @@ +use crate::{ + core::{ + entities::{ + edges::edge_ref::{Dir, EdgeRef}, + properties::{props::Props, tprop::TProp}, + vertices::structure::{adj, adj::Adj}, + LayerIds, EID, VID, + }, + storage::{ + iter::Iter, + lazy_vec::IllegalSet, + timeindex::{AsTime, TimeIndex, TimeIndexEntry, TimeIndexOps}, + ArcEntry, + }, + utils::errors::{GraphError, MutateGraphError}, + Direction, Prop, + }, + prelude::Graph, +}; +use itertools::Itertools; +use serde::{Deserialize, Serialize}; +use std::{ + iter, + ops::{Deref, Range}, + sync::Arc, +}; + +#[derive(Serialize, Deserialize, Debug, Default, PartialEq)] +pub struct VertexStore { + pub(crate) global_id: u64, + pub(crate) name: Option, + pub(crate) vid: VID, + // all the timestamps that have been seen by this vertex + timestamps: TimeIndex, + // each layer represents a separate view of the graph + pub(crate) layers: Vec, + // props for vertex + pub(crate) props: Option, +} + +impl VertexStore { + pub fn new(global_id: u64, t: TimeIndexEntry) -> Self { + let mut layers = Vec::with_capacity(1); + layers.push(Adj::Solo); + Self { + global_id, + name: None, + vid: 0.into(), + timestamps: TimeIndex::one(*t.t()), + layers, + props: None, + } + } + + pub fn empty(global_id: u64, name: Option) -> Self { + let mut layers = Vec::with_capacity(1); + layers.push(Adj::Solo); + Self { + global_id, + name, + vid: VID(0), + timestamps: TimeIndex::Empty, + layers, + props: None, + } + } + + pub fn global_id(&self) -> u64 { + self.global_id + } + + pub fn timestamps(&self) -> &TimeIndex { + &self.timestamps + } + + pub fn update_time(&mut self, t: TimeIndexEntry) { + self.timestamps.insert(*t.t()); + } + + pub fn update_name(&mut self, name: &str) { + match &self.name { + None => { + self.name = Some(name.to_owned()); + } + Some(old) => debug_assert_eq!(old, name), // one-to-one mapping between name and id, name should never change + } + } + + pub fn add_prop( + &mut self, + t: TimeIndexEntry, + prop_id: usize, + prop: Prop, + ) -> Result<(), GraphError> { + let props = self.props.get_or_insert_with(Props::new); + props.add_prop(t, prop_id, prop) + } + + pub fn add_constant_prop( + &mut self, + prop_id: usize, + prop: Prop, + ) -> Result<(), IllegalSet>> { + let props = self.props.get_or_insert_with(Props::new); + props.add_constant_prop(prop_id, prop) + } + + #[inline(always)] + pub(crate) fn find_edge(&self, dst: VID, layer_id: &LayerIds) -> Option { + match layer_id { + LayerIds::All => match self.layers.len() { + 0 => None, + 1 => self.layers[0].get_edge(dst, Direction::OUT), + _ => self + .layers + .iter() + .find_map(|layer| layer.get_edge(dst, Direction::OUT)), + }, + LayerIds::One(layer_id) => self + .layers + .get(*layer_id) + .and_then(|layer| layer.get_edge(dst, Direction::OUT)), + LayerIds::Multiple(layers) => layers.iter().find_map(|layer_id| { + self.layers + .get(*layer_id) + .and_then(|layer| layer.get_edge(dst, Direction::OUT)) + }), + LayerIds::None => None, + } + } + + pub(crate) fn add_edge(&mut self, v_id: VID, dir: Direction, layer: usize, edge_id: EID) { + if layer >= self.layers.len() { + self.layers.resize_with(layer + 1, || Adj::Solo); + } + + match dir { + Direction::IN => self.layers[layer].add_edge_into(v_id, edge_id), + Direction::OUT => self.layers[layer].add_edge_out(v_id, edge_id), + _ => {} + } + } + + pub(crate) fn temporal_properties( + &self, + prop_id: usize, + window: Option>, + ) -> impl Iterator + '_ { + if let Some(window) = window { + self.props + .as_ref() + .map(|ps| ps.temporal_props_window(prop_id, window.start, window.end)) + .unwrap_or_else(|| Box::new(iter::empty())) + } else { + self.props + .as_ref() + .map(|ps| ps.temporal_props(prop_id)) + .unwrap_or_else(|| Box::new(iter::empty())) + } + } + + pub(crate) fn const_prop(&self, prop_id: usize) -> Option<&Prop> { + self.props.as_ref().and_then(|ps| ps.const_prop(prop_id)) + } + + #[inline] + pub(crate) fn edge_tuples<'a>( + &'a self, + layers: &LayerIds, + d: Direction, + ) -> Box + Send + 'a> { + let self_id = self.vid; + let iter: Box + Send> = match d { + Direction::OUT => self.merge_layers(layers, Direction::OUT, self_id), + Direction::IN => self.merge_layers(layers, Direction::IN, self_id), + Direction::BOTH => Box::new( + self.edge_tuples(layers, Direction::OUT) + .merge_by(self.edge_tuples(layers, Direction::IN), |e1, e2| { + e1.remote() < e2.remote() + }), + ), + }; + iter + } + + fn merge_layers( + &self, + layers: &LayerIds, + d: Direction, + self_id: VID, + ) -> Box + Send + '_> { + match layers { + LayerIds::All => Box::new( + self.layers + .iter() + .map(|adj| self.iter_adj(adj, d, self_id)) + .kmerge_by(|e1, e2| e1.remote() < e2.remote()) + .dedup(), + ), + LayerIds::One(id) => { + if let Some(layer) = self.layers.get(*id) { + Box::new(self.iter_adj(layer, d, self_id)) + } else { + Box::new(iter::empty()) + } + } + LayerIds::Multiple(ids) => Box::new( + ids.iter() + .filter_map(|id| self.layers.get(*id)) + .map(|layer| self.iter_adj(layer, d, self_id)) + .kmerge_by(|e1, e2| e1.remote() < e2.remote()) + .dedup(), + ), + LayerIds::None => Box::new(iter::empty()), + } + } + + fn iter_adj<'a>( + &'a self, + layer: &'a Adj, + d: Direction, + self_id: VID, + ) -> impl Iterator + Send + '_ { + let iter: Box + Send> = match d { + Direction::IN => Box::new( + layer + .iter(d) + .map(move |(src_pid, e_id)| EdgeRef::new_incoming(e_id, src_pid, self_id)), + ), + Direction::OUT => Box::new( + layer + .iter(d) + .map(move |(dst_pid, e_id)| EdgeRef::new_outgoing(e_id, self_id, dst_pid)), + ), + _ => Box::new(iter::empty()), + }; + iter + } + + pub(crate) fn degree(&self, layers: &LayerIds, d: Direction) -> usize { + match layers { + LayerIds::All => match self.layers.len() { + 0 => 0, + 1 => self.layers[0].degree(d), + _ => self + .layers + .iter() + .map(|l| l.vertex_iter(d)) + .kmerge() + .dedup() + .count(), + }, + LayerIds::One(l) => self + .layers + .get(*l) + .map(|layer| layer.degree(d)) + .unwrap_or(0), + LayerIds::None => 0, + LayerIds::Multiple(ids) => ids + .iter() + .flat_map(|l_id| self.layers.get(*l_id).map(|layer| layer.vertex_iter(d))) + .kmerge() + .dedup() + .count(), + } + } + + // every neighbour apears once in the iterator + // this is important because it calculates degree + pub(crate) fn neighbours<'a>( + &'a self, + layers: LayerIds, + d: Direction, + ) -> Box + Send + 'a> { + match layers { + LayerIds::All => { + let iter = self + .layers + .iter() + .enumerate() + .map(|(layer_id, _)| self.neighbours(layer_id.into(), d)) + .kmerge() + .dedup(); + Box::new(iter) + } + LayerIds::One(one) => { + let iter = self + .layers + .get(one) + .map(|layer| self.neighbours_from_adj(layer, d, layers)) + .unwrap_or(Box::new(iter::empty())); + Box::new(iter) + } + LayerIds::Multiple(layers) => { + let iter = layers + .iter() + .filter_map(|l| self.layers.get(*l)) + .map(|layer| self.neighbours_from_adj(layer, d, layers.clone().into())) + .kmerge() + .dedup(); + Box::new(iter) + } + LayerIds::None => Box::new(iter::empty()), + } + } + + fn neighbours_from_adj<'a>( + &'a self, + layer: &'a Adj, + d: Direction, + layers: LayerIds, + ) -> Box + Send + '_> { + let iter: Box + Send> = match d { + Direction::IN => Box::new(layer.iter(d).map(|(from_v, _)| from_v)), + Direction::OUT => Box::new(layer.iter(d).map(|(to_v, _)| to_v)), + Direction::BOTH => Box::new( + self.neighbours(layers.clone(), Direction::OUT) + .merge(self.neighbours(layers, Direction::IN)) + .dedup(), + ), + }; + iter + } + + pub(crate) fn edges_from_last( + &self, + layer_id: usize, + dir: Direction, + last: Option, + page_size: usize, + ) -> Vec<(VID, EID)> { + self.layers[layer_id].get_page_vec(last, page_size, dir) + } + + pub(crate) fn const_prop_ids(&self) -> impl Iterator + '_ { + self.props + .as_ref() + .into_iter() + .flat_map(|ps| ps.const_prop_ids()) + } + + pub(crate) fn temporal_property(&self, prop_id: usize) -> Option<&TProp> { + self.props.as_ref().and_then(|ps| ps.temporal_prop(prop_id)) + } + + pub(crate) fn temporal_prop_ids(&self) -> impl Iterator + '_ { + self.props + .as_ref() + .into_iter() + .flat_map(|ps| ps.temporal_prop_ids()) + } + + pub(crate) fn active(&self, w: Range) -> bool { + self.timestamps.active(w) + } +} + +impl ArcEntry { + pub fn into_layers(self) -> LockedLayers { + let len = self.layers.len(); + LockedLayers { + entry: self, + pos: 0, + len, + } + } + + pub fn into_layer(self, offset: usize) -> Option { + (offset < self.layers.len()).then_some(LockedLayer { + entry: self, + offset, + }) + } +} + +pub struct LockedLayers { + entry: ArcEntry, + pos: usize, + len: usize, +} + +impl Iterator for LockedLayers { + type Item = LockedLayer; + + fn next(&mut self) -> Option { + if self.pos < self.len { + let layer = LockedLayer { + entry: self.entry.clone(), + offset: self.pos, + }; + self.pos += 1; + Some(layer) + } else { + None + } + } + + fn size_hint(&self) -> (usize, Option) { + (self.len, Some(self.len)) + } +} + +pub struct LockedLayer { + entry: ArcEntry, + offset: usize, +} + +impl Deref for LockedLayer { + type Target = Adj; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.entry.layers[self.offset] + } +} + +impl LockedLayer { + pub fn into_tuples(self, dir: Dir) -> PagedAdjIter<256> { + let mut page = [(VID(0), EID(0)); 256]; + let page_size = self.fill_page(None, &mut page, dir); + PagedAdjIter { + layer: self, + page, + page_offset: 0, + page_size, + dir, + } + } +} + +pub struct PagedAdjIter { + layer: LockedLayer, + page: [(VID, EID); P], + page_offset: usize, + page_size: usize, + dir: Dir, +} + +impl Iterator for PagedAdjIter

{ + type Item = (VID, EID); + + fn next(&mut self) -> Option { + if self.page_offset < self.page_size { + let item = self.page[self.page_offset]; + self.page_offset += 1; + Some(item) + } else if self.page_size == P { + // Was a full page, there may be more items + let last = self.page[P - 1].0; + self.page_offset = 0; + self.page_size = self.layer.fill_page(Some(last), &mut self.page, self.dir); + self.next() + } else { + // Was a partial page, no more items + None + } + } +} diff --git a/raphtory/src/core/lsm.rs b/raphtory/src/core/lsm.rs deleted file mode 100644 index d820469dc0..0000000000 --- a/raphtory/src/core/lsm.rs +++ /dev/null @@ -1,203 +0,0 @@ -//! A data structure for storing and querying temporal graph data using a Log-Structured Merge Tree (LSM). - -use std::fmt::Debug; - -use itertools::Itertools; -use serde::{Deserialize, Serialize}; - -static MERGE_SORT_SIZE: usize = 64; - -#[repr(transparent)] -#[derive(Debug, Serialize, Deserialize, Default)] -pub struct SortedVec { - vs: Vec, -} - -impl SortedVec { - pub fn insert(&mut self, k: K) { - match self.vs.binary_search(&k) { - Ok(i) | Err(i) => self.vs.insert(i, k), - } - } - - pub fn find(&self, k: K) -> Option<&K> { - self.vs.binary_search(&k).map(|i| &self.vs[i]).ok() - } - - pub fn new() -> Self { - SortedVec { vs: vec![] } - } -} - -//FIXME: naive LSM like implementation, add benches and more tests -#[derive(Debug, PartialEq, Default, Serialize, Deserialize)] -pub struct LSMSet { - vs: Vec, -} - -impl LSMSet { - pub fn new() -> Self { - LSMSet { vs: vec![] } - } - - pub fn len(&self) -> usize { - self.vs.len() // not technically correct - } - - pub fn insert(&mut self, k: K) { - // until we reach MERGE_SORT_SIZE elements we optimistically just add k to the end of the vec - self.vs.push(k); - - if self.vs.len() >= MERGE_SORT_SIZE - && (self.vs.len() - MERGE_SORT_SIZE) % MERGE_SORT_SIZE == 0 - { - // we need to sort the entire thing and dedup - self.vs.sort_unstable(); - // FIXME: why don't we have a sort and dedup? - self.vs.dedup(); - } - } - - fn sorted0(&self) -> &[K] { - &self.vs[0..self.vs.len() / MERGE_SORT_SIZE] - } - - fn unsorted0(&self) -> &[K] { - &self.vs[self.vs.len() / MERGE_SORT_SIZE..] - } - - /* - * - * find k otherwise find the smallest value that is greater than k - * - */ - fn find_local_unsorted<'a, 'b>(k: &'a K, unsorted: &'b [K]) -> Option<&'b K> { - let mut alt: Option<&K> = None; - - for k0 in unsorted.iter() { - if k0 == k { - // awesome - return Some(k0); - } else if k0 > k { - let next_k_alt = alt.get_or_insert(k0); - *next_k_alt = Ord::min(next_k_alt, k0); - } - } - - alt - } - - fn find_local<'a, 'b>(k: &'a K, sorted: &'b [K]) -> Option<&'b K> { - match sorted.binary_search(k) { - Ok(i) => Some(&sorted[i]), - Err(j) if j < sorted.len() => Some(&sorted[j]), - _ => None, - } - } - - pub fn find(&self, k: K) -> Option<&K> { - let a = Self::find_local_unsorted(&k, self.unsorted0()); - let b = Self::find_local(&k, self.sorted0()); - - match (a, b) { - (Some(a1), Some(b1)) => Some(Ord::min(a1, b1)), - (a1 @ Some(_), None) => a1, - (None, a1 @ Some(_)) => a1, - _ => None, - } - } - - pub fn iter(&self) -> Box + '_> { - Box::new( - [self.sorted_cur(), self.sorted()] - .into_iter() - .kmerge() - .dedup(), - ) - } - - fn sorted_cur(&self) -> Box + '_> { - Box::new(self.unsorted0().iter().sorted()) - } - - fn sorted(&self) -> Box + '_> { - Box::new(self.sorted0().iter()) - } -} - -impl> From for LSMSet { - fn from(i: I) -> Self { - let new_sorted = i.sorted().collect_vec(); - LSMSet { vs: new_sorted } - } -} - -#[cfg(test)] -mod lsmset_tests { - use std::collections::BTreeSet; - - use super::*; - - #[test] - fn insert() { - let mut s = LSMSet::default(); - - s.insert(4); - s.insert(9); - s.insert(1); - } - - #[test] - fn insert_find() { - let mut s = LSMSet::default(); - - s.insert((4, 1)); - s.insert((4, 4)); - s.insert((1, 1)); - s.insert((1, 2)); - s.insert((4, 3)); - - assert_eq!(s.find((4, 1)), Some(&(4, 1))); - assert_eq!(s.find((1, 2)), Some(&(1, 2))); - assert_eq!(s.find((1, 3)), Some(&(4, 1))); - assert_eq!(s.find((1, 2)), Some(&(1, 2))); - - let mut ss = BTreeSet::default(); - - ss.insert((4, 1)); - ss.insert((4, 4)); - ss.insert((1, 1)); - ss.insert((1, 2)); - ss.insert((4, 3)); - - assert_eq!(ss.range((4, 2)..).next(), Some(&(4, 3))); - - assert_eq!(s.find((4, 2)), Some(&(4, 3))); - assert_eq!(s.find((1, 3)), Some(&(4, 1))); - } - - #[test] - fn iter() { - let mut s = LSMSet::default(); - - s.insert((4, 1)); - s.insert((4, 4)); - s.insert((1, 1)); - s.insert((1, 2)); - s.insert((4, 3)); - - let all = s.iter().collect_vec(); - - assert_eq!(all, vec![&(1, 1), &(1, 2), &(4, 1), &(4, 3), &(4, 4)]) - } - - #[test] - fn example() { - let mut ss = BTreeSet::default(); - - ss.insert((1, 2)); - - let expected: Option<&(i32, i32)> = None; - assert_eq!(ss.range((4, 2)..).next(), expected); - } -} diff --git a/raphtory/src/core/misc.rs b/raphtory/src/core/misc.rs deleted file mode 100644 index 8b13789179..0000000000 --- a/raphtory/src/core/misc.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/raphtory/src/core/mod.rs b/raphtory/src/core/mod.rs index 99e6868f52..27b295d126 100644 --- a/raphtory/src/core/mod.rs +++ b/raphtory/src/core/mod.rs @@ -24,49 +24,120 @@ //! * `macOS` //! -use crate::db::graph::Graph; -use crate::db::view_api::GraphViewOps; +use crate::db::{api::view::GraphViewOps, graph::graph::Graph}; use chrono::NaiveDateTime; use serde::{Deserialize, Serialize}; -use std::fmt; +use std::{ + borrow::Borrow, + cmp::Ordering, + collections::HashMap, + fmt, + fmt::{Display, Formatter}, + ops::Deref, + sync::Arc, +}; #[cfg(test)] extern crate core; -mod adj; -pub mod agg; -mod edge_layer; -pub mod edge_ref; -mod lazy_vec; -pub mod lsm; -mod props; -mod sorted_vec_map; +pub mod entities; pub mod state; -pub mod tadjset; -mod tcell; -pub mod tgraph; -pub mod tgraph_shard; -pub mod time; -pub mod timeindex; -mod tprop; +pub(crate) mod storage; pub mod utils; -pub mod vertex; -pub mod vertex_ref; -type Time = i64; +/// this is here because Arc annoyingly doesn't implement all the expected comparisons +#[derive(Clone, Debug, Eq, Ord, Hash, Serialize, Deserialize)] +pub struct ArcStr(pub(crate) Arc); + +impl Display for ArcStr { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + Display::fmt(&self.0, f) + } +} + +impl>> From for ArcStr { + fn from(value: T) -> Self { + ArcStr(value.into()) + } +} + +impl From for String { + fn from(value: ArcStr) -> Self { + value.to_string() + } +} +impl Deref for ArcStr { + type Target = Arc; + + #[inline] + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Borrow for ArcStr { + #[inline] + fn borrow(&self) -> &str { + self.0.borrow() + } +} + +impl AsRef for ArcStr +where + T: ?Sized, + ::Target: AsRef, +{ + fn as_ref(&self) -> &T { + self.deref().as_ref() + } +} + +impl + ?Sized> PartialEq for ArcStr { + fn eq(&self, other: &T) -> bool { + >::borrow(self).eq(other.borrow()) + } +} + +impl> PartialOrd for ArcStr { + fn partial_cmp(&self, other: &T) -> Option { + >::borrow(self).partial_cmp(other.borrow()) + } +} /// Denotes the direction of an edge. Can be incoming, outgoing or both. -#[derive(Clone, Copy, PartialEq, Debug)] +#[derive(Clone, Copy, PartialEq, PartialOrd, Debug)] pub enum Direction { OUT, IN, BOTH, } +#[derive(Copy, Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)] +pub enum PropType { + #[default] + Empty, + Str, + U8, + U16, + I32, + I64, + U32, + U64, + F32, + F64, + Bool, + List, + Map, + DTime, + Graph, +} + /// Denotes the types of properties allowed to be stored in the graph. #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] pub enum Prop { - Str(String), + Str(ArcStr), + U8(u8), + U16(u16), I32(i32), I64(i64), U32(u32), @@ -74,13 +145,98 @@ pub enum Prop { F32(f32), F64(f64), Bool(bool), + List(Arc>), + Map(Arc>), DTime(NaiveDateTime), Graph(Graph), } +impl PartialOrd for Prop { + fn partial_cmp(&self, other: &Self) -> Option { + match (self, other) { + (Prop::Str(a), Prop::Str(b)) => a.partial_cmp(b), + (Prop::U8(a), Prop::U8(b)) => a.partial_cmp(b), + (Prop::U16(a), Prop::U16(b)) => a.partial_cmp(b), + (Prop::I32(a), Prop::I32(b)) => a.partial_cmp(b), + (Prop::I64(a), Prop::I64(b)) => a.partial_cmp(b), + (Prop::U32(a), Prop::U32(b)) => a.partial_cmp(b), + (Prop::U64(a), Prop::U64(b)) => a.partial_cmp(b), + (Prop::F32(a), Prop::F32(b)) => a.partial_cmp(b), + (Prop::F64(a), Prop::F64(b)) => a.partial_cmp(b), + (Prop::Bool(a), Prop::Bool(b)) => a.partial_cmp(b), + (Prop::DTime(a), Prop::DTime(b)) => a.partial_cmp(b), + _ => None, + } + } +} + +impl Prop { + pub fn dtype(&self) -> PropType { + match self { + Prop::Str(_) => PropType::Str, + Prop::U8(_) => PropType::U8, + Prop::U16(_) => PropType::U16, + Prop::I32(_) => PropType::I32, + Prop::I64(_) => PropType::I64, + Prop::U32(_) => PropType::U32, + Prop::U64(_) => PropType::U64, + Prop::F32(_) => PropType::F32, + Prop::F64(_) => PropType::F64, + Prop::Bool(_) => PropType::Bool, + Prop::List(_) => PropType::List, + Prop::Map(_) => PropType::Map, + Prop::DTime(_) => PropType::DTime, + Prop::Graph(_) => PropType::Graph, + } + } + + pub fn str>(s: S) -> Prop { + Prop::Str(s.into()) + } + + pub fn add(self, other: Prop) -> Option { + match (self, other) { + (Prop::U8(a), Prop::U8(b)) => Some(Prop::U8(a + b)), + (Prop::U16(a), Prop::U16(b)) => Some(Prop::U16(a + b)), + (Prop::I32(a), Prop::I32(b)) => Some(Prop::I32(a + b)), + (Prop::I64(a), Prop::I64(b)) => Some(Prop::I64(a + b)), + (Prop::U32(a), Prop::U32(b)) => Some(Prop::U32(a + b)), + (Prop::U64(a), Prop::U64(b)) => Some(Prop::U64(a + b)), + (Prop::F32(a), Prop::F32(b)) => Some(Prop::F32(a + b)), + (Prop::F64(a), Prop::F64(b)) => Some(Prop::F64(a + b)), + (Prop::Str(a), Prop::Str(b)) => Some(Prop::Str((a.to_string() + &b).into())), + _ => None, + } + } + + pub fn divide(self, other: Prop) -> Option { + match (self, other) { + (Prop::U8(a), Prop::U8(b)) if b != 0 => Some(Prop::U8(a / b)), + (Prop::U16(a), Prop::U16(b)) if b != 0 => Some(Prop::U16(a / b)), + (Prop::I32(a), Prop::I32(b)) if b != 0 => Some(Prop::I32(a / b)), + (Prop::I64(a), Prop::I64(b)) if b != 0 => Some(Prop::I64(a / b)), + (Prop::U32(a), Prop::U32(b)) if b != 0 => Some(Prop::U32(a / b)), + (Prop::U64(a), Prop::U64(b)) if b != 0 => Some(Prop::U64(a / b)), + (Prop::F32(a), Prop::F32(b)) if b != 0.0 => Some(Prop::F32(a / b)), + (Prop::F64(a), Prop::F64(b)) if b != 0.0 => Some(Prop::F64(a / b)), + _ => None, + } + } +} + pub trait PropUnwrap: Sized { - fn into_str(self) -> Option; - fn unwrap_str(self) -> String { + fn into_u8(self) -> Option; + fn unwrap_u8(self) -> u8 { + self.into_u8().unwrap() + } + + fn into_u16(self) -> Option; + fn unwrap_u16(self) -> u16 { + self.into_u16().unwrap() + } + + fn into_str(self) -> Option; + fn unwrap_str(self) -> ArcStr { self.into_str().unwrap() } @@ -119,6 +275,16 @@ pub trait PropUnwrap: Sized { self.into_bool().unwrap() } + fn into_list(self) -> Option>>; + fn unwrap_list(self) -> Arc> { + self.into_list().unwrap() + } + + fn into_map(self) -> Option>>; + fn unwrap_map(self) -> Arc> { + self.into_map().unwrap() + } + fn into_dtime(self) -> Option; fn unwrap_dtime(self) -> NaiveDateTime { self.into_dtime().unwrap() @@ -131,7 +297,15 @@ pub trait PropUnwrap: Sized { } impl PropUnwrap for Option

{ - fn into_str(self) -> Option { + fn into_u8(self) -> Option { + self.and_then(|p| p.into_u8()) + } + + fn into_u16(self) -> Option { + self.and_then(|p| p.into_u16()) + } + + fn into_str(self) -> Option { self.and_then(|p| p.into_str()) } @@ -163,6 +337,14 @@ impl PropUnwrap for Option

{ self.and_then(|p| p.into_bool()) } + fn into_list(self) -> Option>> { + self.and_then(|p| p.into_list()) + } + + fn into_map(self) -> Option>> { + self.and_then(|p| p.into_map()) + } + fn into_dtime(self) -> Option { self.and_then(|p| p.into_dtime()) } @@ -173,7 +355,23 @@ impl PropUnwrap for Option

{ } impl PropUnwrap for Prop { - fn into_str(self) -> Option { + fn into_u8(self) -> Option { + if let Prop::U8(s) = self { + Some(s) + } else { + None + } + } + + fn into_u16(self) -> Option { + if let Prop::U16(s) = self { + Some(s) + } else { + None + } + } + + fn into_str(self) -> Option { if let Prop::Str(s) = self { Some(s) } else { @@ -237,6 +435,22 @@ impl PropUnwrap for Prop { } } + fn into_list(self) -> Option>> { + if let Prop::List(v) = self { + Some(v) + } else { + None + } + } + + fn into_map(self) -> Option>> { + if let Prop::Map(v) = self { + Some(v) + } else { + None + } + } + fn into_dtime(self) -> Option { if let Prop::DTime(v) = self { Some(v) @@ -258,6 +472,8 @@ impl fmt::Display for Prop { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Prop::Str(value) => write!(f, "{}", value), + Prop::U8(value) => write!(f, "{}", value), + Prop::U16(value) => write!(f, "{}", value), Prop::I32(value) => write!(f, "{}", value), Prop::I64(value) => write!(f, "{}", value), Prop::U32(value) => write!(f, "{}", value), @@ -269,9 +485,179 @@ impl fmt::Display for Prop { Prop::Graph(value) => write!( f, "Graph(num_vertices={}, num_edges={})", - value.num_vertices(), - value.num_edges() + value.count_vertices(), + value.count_edges() ), + Prop::List(value) => { + write!(f, "{:?}", value) + } + Prop::Map(value) => { + write!(f, "{:?}", value) + } } } } + +// From impl for Prop + +impl From for Prop { + fn from(value: ArcStr) -> Self { + Prop::Str(value) + } +} + +impl From<&ArcStr> for Prop { + fn from(value: &ArcStr) -> Self { + Prop::Str(value.clone()) + } +} + +impl From for Prop { + fn from(value: String) -> Self { + Prop::Str(value.into()) + } +} +impl From<&String> for Prop { + fn from(s: &String) -> Self { + Prop::Str(s.as_str().into()) + } +} + +impl From> for Prop { + fn from(s: Arc) -> Self { + Prop::Str(s.into()) + } +} + +impl From<&Arc> for Prop { + fn from(value: &Arc) -> Self { + Prop::Str(value.clone().into()) + } +} + +impl From<&str> for Prop { + fn from(s: &str) -> Self { + Prop::Str(s.to_owned().into()) + } +} + +impl From for Prop { + fn from(i: i32) -> Self { + Prop::I32(i) + } +} + +impl From for Prop { + fn from(i: u8) -> Self { + Prop::U8(i) + } +} + +impl From for Prop { + fn from(i: u16) -> Self { + Prop::U16(i) + } +} + +impl From for Prop { + fn from(i: i64) -> Self { + Prop::I64(i) + } +} + +impl From for Prop { + fn from(u: u32) -> Self { + Prop::U32(u) + } +} + +impl From for Prop { + fn from(u: u64) -> Self { + Prop::U64(u) + } +} + +impl From for Prop { + fn from(f: f32) -> Self { + Prop::F32(f) + } +} + +impl From for Prop { + fn from(f: f64) -> Self { + Prop::F64(f) + } +} + +impl From for Prop { + fn from(b: bool) -> Self { + Prop::Bool(b) + } +} + +impl From> for Prop { + fn from(value: HashMap) -> Self { + Prop::Map(Arc::new(value)) + } +} + +impl From> for Prop { + fn from(value: Vec) -> Self { + Prop::List(Arc::new(value)) + } +} + +impl From<&Prop> for Prop { + fn from(value: &Prop) -> Self { + value.clone() + } +} + +pub trait IntoPropMap { + fn into_prop_map(self) -> Prop; +} + +impl, K: Into, V: Into> IntoPropMap for I { + fn into_prop_map(self) -> Prop { + Prop::Map(Arc::new( + self.into_iter() + .map(|(k, v)| (k.into(), v.into())) + .collect(), + )) + } +} + +pub trait IntoPropList { + fn into_prop_list(self) -> Prop; +} + +impl, K: Into> IntoPropList for I { + fn into_prop_list(self) -> Prop { + Prop::List(Arc::new(self.into_iter().map(|v| v.into()).collect())) + } +} + +pub trait IntoProp { + fn into_prop(self) -> Prop; +} + +impl> IntoProp for T { + fn into_prop(self) -> Prop { + self.into() + } +} + +#[cfg(test)] +mod test_arc_str { + use crate::core::ArcStr; + use std::sync::Arc; + + #[test] + fn can_compare_with_str() { + let test: ArcStr = "test".into(); + assert_eq!(test, "test"); + assert_eq!(test, "test".to_string()); + assert_eq!(test, Arc::from("test")); + assert_eq!(&test, &"test".to_string()) + } +} diff --git a/raphtory/src/core/props.rs b/raphtory/src/core/props.rs deleted file mode 100644 index 3737f8f986..0000000000 --- a/raphtory/src/core/props.rs +++ /dev/null @@ -1,298 +0,0 @@ -use crate::core::lazy_vec::{IllegalSet, LazyVec}; -use crate::core::tprop::TProp; -use crate::core::Prop; -use itertools::Itertools; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::fmt::Debug; - -#[derive(thiserror::Error, Debug, PartialEq)] -#[error("cannot mutate static property '{name}'")] -pub struct IllegalMutate { - pub name: String, - pub source: IllegalSet>, -} - -impl IllegalMutate { - fn from(source: IllegalSet>, props: &Props) -> IllegalMutate { - let id = PropId::Static(source.index); - IllegalMutate { - name: props.reverse_id(&id).to_string(), - source, - } - } -} - -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Hash, Clone)] -enum PropId { - Static(usize), - Temporal(usize), -} - -impl PropId { - #[allow(dead_code)] - pub(crate) fn new(id: usize, static_: bool) -> PropId { - if static_ { - PropId::Static(id) - } else { - PropId::Temporal(id) - } - } - pub(crate) fn get_id(&self) -> usize { - match self { - PropId::Static(id) => *id, - PropId::Temporal(id) => *id, - } - } - pub(crate) fn is_static(&self) -> bool { - match self { - PropId::Static(_) => true, - PropId::Temporal(_) => false, - } - } -} - -#[derive(Default, Debug, Serialize, Deserialize, PartialEq)] -pub(crate) struct Props { - // Mapping between property name and property id - prop_ids: HashMap, // TODO: change name back to prop_ids - - // Vector of vertices properties. Each index represents vertex local (physical) id - static_props: Vec>>, - temporal_props: Vec>, -} - -impl Props { - // GETTERS: - - fn get_prop_id(&self, name: &str, should_be_static: bool) -> Option { - match self.prop_ids.get(name) { - Some(prop_id) if prop_id.is_static() == should_be_static => Some(prop_id.get_id()), - _ => None, - } - } - - #[allow(unused_variables)] - fn reverse_id(&self, id: &PropId) -> &str { - self.prop_ids.iter().find(|&(k, v)| v == id).unwrap().0 - } - - fn get_or_default(&self, vector: &Vec>, id: usize, name: &str) -> A - where - A: PartialEq + Default + Clone + Debug, - { - match self.get_prop_id(name, true) { - Some(prop_id) => { - let props = vector.get(id).unwrap_or(&LazyVec::Empty); - props.get(prop_id).cloned().unwrap_or(Default::default()) - } - None => Default::default(), - } - } - - pub(crate) fn static_prop(&self, id: usize, name: &str) -> Option { - self.get_or_default(&self.static_props, id, name) - } - - pub(crate) fn temporal_prop(&self, id: usize, name: &str) -> Option<&TProp> { - // TODO: we should be able to use self.get_or_default() here - let prop_id = self.get_prop_id(name, false)?; - let props = self.temporal_props.get(id).unwrap_or(&LazyVec::Empty); - props.get(prop_id) - } - - fn get_names( - &self, - vector: &Vec>, - id: usize, - should_be_static: bool, - ) -> Vec - where - A: Clone + Default + PartialEq + Debug, - { - match vector.get(id) { - Some(props) => { - let ids = props.filled_ids().into_iter(); - if should_be_static { - ids.map(|id| self.reverse_id(&PropId::Static(id)).to_string()) - .collect_vec() - } else { - ids.map(|id| self.reverse_id(&PropId::Temporal(id)).to_string()) - .collect_vec() - } - } - None => vec![], - } - } - - pub fn static_names(&self, id: usize) -> Vec { - self.get_names(&self.static_props, id, true) - } - - pub fn temporal_names(&self, id: usize) -> Vec { - self.get_names(&self.temporal_props, id, false) - } - - // SETTERS: - - fn grow_and_get_slot(vector: &mut Vec, id: usize) -> &mut A - where - A: Default, - { - if vector.len() <= id { - vector.resize_with(id + 1, || Default::default()); - } - // now props_storage.len() >= id + 1: - vector.get_mut(id).unwrap() - } - - fn get_or_allocate_id(&mut self, name: &str, should_be_static: bool) -> Result { - match self.prop_ids.get(name) { - None => { - let new_prop_id = if should_be_static { - let static_prop_ids = self.prop_ids.iter().filter(|&(_, v)| v.is_static()); - let new_id = static_prop_ids.count(); - PropId::Static(new_id) - } else { - let static_prop_ids = self.prop_ids.iter().filter(|&(_, v)| !v.is_static()); - let new_id = static_prop_ids.count(); - PropId::Temporal(new_id) - }; - self.prop_ids.insert(name.to_string(), new_prop_id.clone()); - Ok(new_prop_id.get_id()) - } - Some(id) if id.is_static() == should_be_static => Ok(id.get_id()), - _ => Err(()), - } - } - - fn translate_props( - &mut self, - props: &Vec<(String, Prop)>, - should_be_static: bool, - ) -> Vec<(usize, Prop)> { - // TODO: return Result - props - .iter() - .map(|(name, prop)| { - ( - self.get_or_allocate_id(name, should_be_static).unwrap(), - prop.clone(), - ) - }) - .collect_vec() - } - - pub fn upsert_temporal_props(&mut self, t: i64, id: usize, props: &Vec<(String, Prop)>) { - if !props.is_empty() { - let translated_props = self.translate_props(props, false); - let vertex_slot: &mut LazyVec = - Self::grow_and_get_slot(&mut self.temporal_props, id); - for (prop_id, prop) in translated_props { - vertex_slot.update_or_set(prop_id, |p| p.set(t, &prop), TProp::from(t, &prop)); - } - } - } - - pub fn set_static_props( - &mut self, - id: usize, - props: &Vec<(String, Prop)>, - ) -> Result<(), IllegalMutate> { - if !props.is_empty() { - let translated_props = self.translate_props(props, true); - let vertex_slot: &mut LazyVec> = - Self::grow_and_get_slot(&mut self.static_props, id); - for (prop_id, prop) in translated_props { - if let Err(e) = vertex_slot.set(prop_id, Some(prop)) { - return Err(IllegalMutate::from(e, &self)); - } - } - } - Ok(()) - } -} - -#[cfg(test)] -mod props_tests { - use super::*; - - #[test] - fn return_prop_id_if_prop_name_found() { - let mut props = Props::default(); - props - .prop_ids - .insert(String::from("key1"), PropId::Temporal(0)); - props - .prop_ids - .insert(String::from("key2"), PropId::Temporal(1)); - - assert_eq!(props.get_or_allocate_id("key2", false), Ok(1)); - } - - #[test] - fn return_new_prop_id_if_prop_name_not_found() { - let mut props = Props::default(); - assert_eq!(props.get_or_allocate_id("key1", false), Ok(0)); - assert_eq!(props.get_or_allocate_id("key2", false), Ok(1)); - } - - #[test] - fn insert_new_vertex_prop() { - let mut props = Props::default(); - props.upsert_temporal_props(1, 0, &vec![("bla".to_string(), Prop::I32(10))]); - - let prop_id = props.get_or_allocate_id("bla", false).unwrap(); - assert_eq!( - props - .temporal_props - .get(0) - .unwrap() - .get(prop_id) - .unwrap() - .iter() - .collect::>(), - vec![(&1, Prop::I32(10))] - ) - } - - #[test] - fn update_existing_vertex_prop() { - let mut props = Props::default(); - props.upsert_temporal_props(1, 0, &vec![("bla".to_string(), Prop::I32(10))]); - props.upsert_temporal_props(2, 0, &vec![("bla".to_string(), Prop::I32(10))]); - - let prop_id = props.get_or_allocate_id("bla", false).unwrap(); - assert_eq!( - props - .temporal_props - .get(0) - .unwrap() - .get(prop_id) - .unwrap() - .iter() - .collect::>(), - vec![(&1, Prop::I32(10)), (&2, Prop::I32(10))] - ) - } - - #[test] - fn new_update_with_the_same_time_to_a_vertex_prop_is_ignored() { - let mut props = Props::default(); - props.upsert_temporal_props(1, 0, &vec![("bla".to_string(), Prop::I32(10))]); - props.upsert_temporal_props(1, 0, &vec![("bla".to_string(), Prop::I32(20))]); - - let prop_id = props.get_or_allocate_id("bla", false).unwrap(); - assert_eq!( - props - .temporal_props - .get(0) - .unwrap() - .get(prop_id) - .unwrap() - .iter() - .collect::>(), - vec![(&1, Prop::I32(10))] - ) - } -} diff --git a/raphtory/src/core/state/accumulator_id.rs b/raphtory/src/core/state/accumulator_id.rs index 74779d2894..96ad01e743 100644 --- a/raphtory/src/core/state/accumulator_id.rs +++ b/raphtory/src/core/state/accumulator_id.rs @@ -1,4 +1,4 @@ -use crate::core::agg::{Accumulator, Init, InitAcc}; +use crate::core::state::agg::{Accumulator, Init, InitAcc}; #[derive(Debug)] pub struct AccId> { @@ -39,13 +39,13 @@ unsafe impl> Sync for AccId: Send + Sync + 'static { fn zero() -> A; @@ -404,8 +403,9 @@ mod agg_test { #[test] fn avg_def() { - use crate::core::agg::{ - topk::TopK, topk::TopKHeap, Accumulator, AvgDef, MaxDef, MinDef, SumDef, + use super::{ + topk::{TopK, TopKHeap}, + Accumulator, AvgDef, MaxDef, MinDef, SumDef, }; let mut avg = AvgDef::::zero(); diff --git a/raphtory/src/core/state/compute_state.rs b/raphtory/src/core/state/compute_state.rs index d440c86572..771a805287 100644 --- a/raphtory/src/core/state/compute_state.rs +++ b/raphtory/src/core/state/compute_state.rs @@ -1,13 +1,9 @@ -use rustc_hash::FxHashMap; -use std::collections::HashMap; - -use crate::core::vertex_ref::LocalVertexRef; -use crate::{core::agg::Accumulator, db::view_api::internal::GraphViewInternalOps}; - use super::{ - container::{merge_2_vecs, DynArray, MapArray, VecArray}, + container::{merge_2_vecs, DynArray, VecArray}, StateType, }; +use crate::{core::state::agg::Accumulator, db::api::view::GraphViewOps}; +use std::collections::HashMap; pub trait ComputeState: std::fmt::Debug + Clone + Send + Sync { fn clone_current_into_other(&mut self, ss: usize); @@ -30,10 +26,7 @@ pub trait ComputeState: std::fmt::Debug + Clone + Send + Sync { i: usize, ) -> Option<&A>; - fn iter(&self, ss: usize) -> Box + '_>; - - fn iter_keys(&self) -> Box + '_>; - fn iter_keys_changed(&self, ss: usize) -> Box + '_>; + fn iter(&self, ss: usize, extend_to: usize) -> Box + '_>; fn agg>(&mut self, ss: usize, a: IN, ki: usize) where @@ -47,10 +40,9 @@ pub trait ComputeState: std::fmt::Debug + Clone + Send + Sync { where A: StateType; - fn finalize, G: GraphViewInternalOps>( + fn finalize, G: GraphViewOps>( &self, ss: usize, - shard_id: usize, g: &G, ) -> HashMap where @@ -65,186 +57,6 @@ pub trait ComputeState: std::fmt::Debug + Clone + Send + Sync { OUT: StateType; } -#[derive(Debug)] -pub struct ComputeStateMap(Box); - -impl ComputeStateMap { - fn current_mut(&mut self) -> &mut dyn DynArray { - self.0.as_mut() - } - - fn current(&self) -> &dyn DynArray { - self.0.as_ref() - } -} - -impl Clone for ComputeStateMap { - fn clone(&self) -> Self { - ComputeStateMap(self.0.clone_array()) - } -} - -impl ComputeState for ComputeStateMap { - fn clone_current_into_other(&mut self, ss: usize) { - self.0.copy_over(ss); - } - - fn reset_resetable_states(&mut self, ss: usize) { - self.0.reset(ss); - } - - fn new_mutable_primitive(zero: T) -> Self { - ComputeStateMap(Box::new(MapArray:: { - map: FxHashMap::default(), - zero, - })) - } - - fn read>( - &self, - ss: usize, - i: usize, - ) -> Option - where - OUT: std::fmt::Debug, - { - let current = self - .current() - .as_any() - .downcast_ref::>() - .unwrap(); - - current - .map - .get(&(i as u64)) - .map(|v| ACC::finish(&v[ss % 2])) - } - - fn read_ref>( - &self, - ss: usize, - i: usize, - ) -> Option<&A> { - let current = self - .current() - .as_any() - .downcast_ref::>() - .unwrap(); - current.map.get(&(i as u64)).map(|v| &v[ss % 2]) - } - - fn iter(&self, ss: usize) -> Box + '_> { - let current = self - .current() - .as_any() - .downcast_ref::>() - .unwrap(); - Box::new( - current - .map - .iter() - .map(move |(k, v)| (*k as usize, &v[ss % 2])), - ) - } - - fn iter_keys(&self) -> Box + '_> { - self.current().iter_keys() - } - - fn iter_keys_changed(&self, ss: usize) -> Box + '_> { - self.current().iter_keys_changed(ss) - } - - fn agg>(&mut self, ss: usize, a: IN, i: usize) - where - A: StateType, - { - let current = self - .current_mut() - .as_mut_any() - .downcast_mut::>() - .unwrap(); - let entry = current - .map - .entry(i as u64) - .or_insert_with(|| [current.zero.clone(), current.zero.clone()]); - ACC::add0(&mut entry[ss % 2], a); - } - - fn combine>(&mut self, ss: usize, a: &A, i: usize) - where - A: StateType, - { - let current = self - .current_mut() - .as_mut_any() - .downcast_mut::>() - .unwrap(); - let zero = current.zero.clone(); - let entry = current - .map - .entry(i as u64) - .or_insert_with(|| [zero.clone(), zero.clone()]); - ACC::combine(&mut entry[ss % 2], a); - } - - fn merge>(&mut self, other: &Self, ss: usize) - where - A: StateType, - { - other.iter::(ss).for_each(|(i, a)| { - self.combine::(ss, a, i); - }); - } - - fn finalize, G: GraphViewInternalOps>( - &self, - ss: usize, - _shard_id: usize, - _g: &G, - ) -> HashMap - where - OUT: StateType, - A: 'static, - { - let current = self - .current() - .as_any() - .downcast_ref::>() - .unwrap(); - current - .map - .iter() - .map(|(c, v)| { - // println!("c? = {}", c); - ( - _g.vertex_name(_g.localise_vertex_unchecked((*c).into())), - ACC::finish(&v[ss % 2]), - ) - }) - .collect() - } - - fn fold, F, B>(&self, ss: usize, b: B, f: F) -> B - where - F: FnOnce(B, &u64, OUT) -> B + Copy, - A: 'static, - B: std::fmt::Debug, - OUT: StateType, - { - let current = self - .current() - .as_any() - .downcast_ref::>() - .unwrap(); - current - .map - .iter() - .map(|(k, v)| (k, ACC::finish(&v[ss % 2]))) - .fold(b, |b, (k, out)| f(b, k, out)) - } -} - #[derive(Debug)] pub struct ComputeStateVec(Box); @@ -306,24 +118,20 @@ impl ComputeState for ComputeStateVec { vec.current(ss).get(i) } - fn iter(&self, ss: usize) -> Box + '_> { + fn iter(&self, ss: usize, extend_to: usize) -> Box + '_> { let vec = self .current() .as_any() .downcast_ref::>() .unwrap(); - let iter = vec.current(ss).iter().enumerate(); + let zero = vec.zero(); + let inner_vec = vec.current(ss); + let vec_len = inner_vec.len(); + let extend_iter = std::iter::repeat(zero).take(extend_to - vec_len); + let iter = inner_vec.iter().chain(extend_iter); Box::new(iter) } - fn iter_keys(&self) -> Box + '_> { - todo!() - } - - fn iter_keys_changed(&self, _ss: usize) -> Box + '_> { - todo!() - } - fn agg>(&mut self, ss: usize, a: IN, ki: usize) where A: StateType, @@ -381,10 +189,9 @@ impl ComputeState for ComputeStateVec { merge_2_vecs(v, v_other, |a, b| ACC::combine(a, b)); } - fn finalize, G: GraphViewInternalOps>( + fn finalize, G: GraphViewOps>( &self, ss: usize, - shard_id: usize, g: &G, ) -> HashMap where @@ -402,10 +209,8 @@ impl ComputeState for ComputeStateVec { .iter() .enumerate() .map(|(p_id, a)| { - let v_ref = LocalVertexRef::new(p_id, shard_id); - let out = ACC::finish(a); - (g.vertex_name(v_ref), out) + (g.vertex_name(p_id.into()), out) }) .collect() } diff --git a/raphtory/src/core/state/container.rs b/raphtory/src/core/state/container.rs index 8fdb52e769..956bea9f27 100644 --- a/raphtory/src/core/state/container.rs +++ b/raphtory/src/core/state/container.rs @@ -1,8 +1,5 @@ -use std::any::Any; - -use rustc_hash::FxHashMap; - use super::StateType; +use std::any::Any; pub trait DynArray: std::fmt::Debug + Send + Sync { fn as_any(&self) -> &dyn Any; @@ -12,14 +9,6 @@ pub trait DynArray: std::fmt::Debug + Send + Sync { // used for map array fn copy_over(&mut self, ss: usize); fn reset(&mut self, ss: usize); - fn iter_keys(&self) -> Box + '_>; - fn iter_keys_changed(&self, ss: usize) -> Box + '_>; -} - -#[derive(Debug, Clone, PartialEq)] -pub(crate) struct MapArray { - pub(crate) map: FxHashMap, - pub(crate) zero: T, } #[derive(Debug, Clone, PartialEq)] @@ -62,12 +51,8 @@ impl VecArray { } } - fn previous(&self, ss: usize) -> &Vec { - if ss % 2 == 0 { - &self.odd - } else { - &self.even - } + pub(crate) fn zero(&self) -> &T { + &self.zero } } @@ -129,65 +114,4 @@ impl DynArray for VecArray { *v = zero.clone(); } } - - fn iter_keys(&self) -> Box + '_> { - todo!() - } - - fn iter_keys_changed(&self, _ss: usize) -> Box + '_> { - todo!() - } -} - -impl DynArray for MapArray -where - T: StateType, -{ - fn as_any(&self) -> &dyn Any { - self - } - - fn as_mut_any(&mut self) -> &mut dyn Any { - self - } - - fn clone_array(&self) -> Box { - Box::new(self.clone()) - } - - fn copy_from(&mut self, other: &dyn DynArray) { - let other = other.as_any().downcast_ref::>().unwrap(); - self.map = other.map.clone(); - } - - fn copy_over(&mut self, ss: usize) { - for val in self.map.values_mut() { - let i = ss % 2; - let j = (ss + 1) % 2; - val[j] = val[i].clone(); - } - } - - fn iter_keys(&self) -> Box + '_> { - Box::new(self.map.keys().copied()) - } - - fn iter_keys_changed(&self, ss: usize) -> Box + '_> { - Box::new(self.map.iter().filter_map(move |(k, v)| { - let i = ss % 2; - let j = (ss + 1) % 2; - if v[i] != v[j] { - Some(*k) - } else { - None - } - })) - } - - fn reset(&mut self, ss: usize) { - for val in self.map.values_mut() { - let i = (ss + 1) % 2; - val[i] = self.zero.clone(); - } - } } diff --git a/raphtory/src/core/state/mod.rs b/raphtory/src/core/state/mod.rs index 2059d4728f..c6d2cf227a 100644 --- a/raphtory/src/core/state/mod.rs +++ b/raphtory/src/core/state/mod.rs @@ -1,7 +1,8 @@ pub mod accumulator_id; +pub mod agg; pub mod compute_state; pub mod container; -pub mod shard_state; +pub mod morcel_state; pub mod shuffle_state; pub trait StateType: PartialEq + Clone + std::fmt::Debug + Send + Sync + 'static {} @@ -15,13 +16,11 @@ mod state_test { use crate::{ core::state::{ - accumulator_id::accumulators, - compute_state::{ComputeStateMap, ComputeStateVec}, - container::merge_2_vecs, - shard_state::ShardComputeState, - shuffle_state::ShuffleComputeState, + accumulator_id::accumulators, compute_state::ComputeStateVec, container::merge_2_vecs, + morcel_state::MorcelComputeState, shuffle_state::ShuffleComputeState, }, - db::graph::Graph, + db::{api::mutation::AdditionOps, graph::graph::Graph}, + prelude::NO_PROPS, }; #[quickcheck] @@ -43,22 +42,22 @@ mod state_test { } } - fn tiny_graph(n_shards: usize) -> Graph { - let g = Graph::new(n_shards); + fn tiny_graph() -> Graph { + let g = Graph::new(); - g.add_vertex(1, 1, &vec![]).unwrap(); - g.add_vertex(1, 2, &vec![]).unwrap(); - g.add_vertex(1, 3, &vec![]).unwrap(); + g.add_vertex(1, 1, NO_PROPS).unwrap(); + g.add_vertex(1, 2, NO_PROPS).unwrap(); + g.add_vertex(1, 3, NO_PROPS).unwrap(); g } #[test] fn min_aggregates_for_3_keys() { - let g = tiny_graph(1); + let g = tiny_graph(); let min = accumulators::min(0); - let mut state_map: ShardComputeState = ShardComputeState::new(); + let mut state_map: MorcelComputeState = MorcelComputeState::new(3); // create random vec of numbers let mut rng = rand::thread_rng(); @@ -76,25 +75,25 @@ mod state_test { state_map.accumulate_into(0, 2, a, &min); } - let mut actual = state_map.finalize(0, &min, 0, &g).into_iter().collect_vec(); + let mut actual = state_map.finalize(0, &min, &g).into_iter().collect_vec(); actual.sort(); assert_eq!( actual, vec![ ("1".to_string(), actual_min), ("2".to_string(), actual_min), - ("3".to_string(), actual_min) + ("3".to_string(), actual_min), ] ); } #[test] fn avg_aggregates_for_3_keys() { - let g = tiny_graph(1); + let g = tiny_graph(); let avg = accumulators::avg(0); - let mut state_map: ShardComputeState = ShardComputeState::new(); + let mut state_map: MorcelComputeState = MorcelComputeState::new(3); // create random vec of numbers let mut rng = rand::thread_rng(); @@ -107,31 +106,31 @@ mod state_test { } for a in vec { + state_map.accumulate_into(0, 0, a, &avg); state_map.accumulate_into(0, 1, a, &avg); state_map.accumulate_into(0, 2, a, &avg); - state_map.accumulate_into(0, 3, a, &avg); } let actual_avg = sum / 100; - let mut actual = state_map.finalize(0, &avg, 0, &g).into_iter().collect_vec(); + let mut actual = state_map.finalize(0, &avg, &g).into_iter().collect_vec(); actual.sort(); assert_eq!( actual, vec![ ("1".to_string(), actual_avg), ("2".to_string(), actual_avg), - ("3".to_string(), actual_avg) + ("3".to_string(), actual_avg), ] ); } #[test] fn top3_aggregates_for_3_keys() { - let g = tiny_graph(1); + let g = tiny_graph(); let top3 = accumulators::topk::(0); - let mut state_map: ShardComputeState = ShardComputeState::new(); + let mut state_map: MorcelComputeState = MorcelComputeState::new(3); for a in 0..100 { state_map.accumulate_into(0, 0, a, &top3); @@ -140,10 +139,7 @@ mod state_test { } let expected = vec![99, 98, 97]; - let mut actual = state_map - .finalize(0, &top3, 0, &g) - .into_iter() - .collect_vec(); + let mut actual = state_map.finalize(0, &top3, &g).into_iter().collect_vec(); actual.sort(); @@ -152,18 +148,18 @@ mod state_test { vec![ ("1".to_string(), expected.clone()), ("2".to_string(), expected.clone()), - ("3".to_string(), expected.clone()) + ("3".to_string(), expected.clone()), ] ); } #[test] fn sum_aggregates_for_3_keys() { - let g = tiny_graph(2); + let g = tiny_graph(); let sum = accumulators::sum(0); - let mut state: ShardComputeState = ShardComputeState::new(); + let mut state: MorcelComputeState = MorcelComputeState::new(3); // create random vec of numbers let mut rng = rand::thread_rng(); @@ -176,31 +172,31 @@ mod state_test { } for a in vec { + state.accumulate_into(0, 0, a, &sum); state.accumulate_into(0, 1, a, &sum); state.accumulate_into(0, 2, a, &sum); - state.accumulate_into(0, 3, a, &sum); } - let mut actual = state.finalize(0, &sum, 0, &g).into_iter().collect_vec(); + let mut actual = state.finalize(0, &sum, &g).into_iter().collect_vec(); actual.sort(); assert_eq!( actual, vec![ ("1".to_string(), actual_sum), ("2".to_string(), actual_sum), - ("3".to_string(), actual_sum) + ("3".to_string(), actual_sum), ] ); } #[test] fn sum_aggregates_for_3_keys_2_parts() { - let g = tiny_graph(2); - let sum = accumulators::sum(0); - let mut part1_state: ShuffleComputeState = ShuffleComputeState::new(2); - let mut part2_state: ShuffleComputeState = ShuffleComputeState::new(2); + let mut part1_state: ShuffleComputeState = + ShuffleComputeState::new(3, 2, 2); + let mut part2_state: ShuffleComputeState = + ShuffleComputeState::new(3, 2, 2); // create random vec of numbers let mut rng = rand::thread_rng(); @@ -208,7 +204,7 @@ mod state_test { let mut vec2 = vec![]; let mut actual_sum_1 = 0; let mut actual_sum_2 = 0; - for _ in 0..100 { + for _ in 0..3 { // data for first partition let i = rng.gen_range(0..100); actual_sum_1 += i; @@ -224,75 +220,48 @@ mod state_test { // 2 gets the numbers from part1 // 3 gets the numbers from part2 for a in vec1 { + part1_state.accumulate_into(0, 0, a, &sum); part1_state.accumulate_into(0, 1, a, &sum); - part1_state.accumulate_into(0, 2, a, &sum); } for a in vec2 { - part2_state.accumulate_into(0, 1, a, &sum); - part2_state.accumulate_into(0, 3, a, &sum); + part2_state.accumulate_into(0, 0, a, &sum); + part2_state.accumulate_into(0, 2, a, &sum); } - println!("part1_state: {:?}", part1_state); - println!("part2_state: {:?}", part2_state); - - let mut actual: Vec<(String, i32)> = part1_state - .finalize(&sum, 0, &g, |c| c) - .into_iter() - .collect_vec(); - - actual.sort(); - - assert_eq!( - actual, - vec![ - ("1".to_string(), actual_sum_1), - ("2".to_string(), actual_sum_1), - ] - ); + let actual = part1_state.iter_out(0, sum).collect_vec(); - let mut actual = part2_state - .finalize(&sum, 0, &g, |c| c) - .into_iter() - .collect_vec(); + assert_eq!(actual, vec![(0, actual_sum_1), (1, actual_sum_1), (2, 0)]); - actual.sort(); + let actual = part2_state.iter_out(0, sum).collect_vec(); - assert_eq!( - actual, - vec![ - ("1".to_string(), actual_sum_2), - ("3".to_string(), actual_sum_2) - ] - ); + assert_eq!(actual, vec![(0, actual_sum_2), (1, 0), (2, actual_sum_2)]); - ShuffleComputeState::merge_mut(&mut part1_state, &part2_state, &sum, 0); - let mut actual = part1_state - .finalize(&sum, 0, &g, |c| c) - .into_iter() - .collect_vec(); + ShuffleComputeState::merge_mut(&mut part1_state, &part2_state, sum, 0); - actual.sort(); + let actual = part1_state.iter_out(0, sum).collect_vec(); assert_eq!( actual, vec![ - ("1".to_string(), (actual_sum_1 + actual_sum_2)), - ("2".to_string(), actual_sum_1), - ("3".to_string(), actual_sum_2) + (0, (actual_sum_1 + actual_sum_2)), + (1, actual_sum_1), + (2, actual_sum_2), ] ); } #[test] fn min_sum_aggregates_for_3_keys_2_parts() { - let g = tiny_graph(2); + let g = tiny_graph(); let sum = accumulators::sum(0); let min = accumulators::min(1); - let mut part1_state: ShuffleComputeState = ShuffleComputeState::new(2); - let mut part2_state: ShuffleComputeState = ShuffleComputeState::new(2); + let mut part1_state: ShuffleComputeState = + ShuffleComputeState::new(3, 2, 2); + let mut part2_state: ShuffleComputeState = + ShuffleComputeState::new(3, 2, 2); // create random vec of numbers let mut rng = rand::thread_rng(); @@ -320,20 +289,21 @@ mod state_test { // 2 gets the numbers from part1 // 3 gets the numbers from part2 for a in vec1 { + part1_state.accumulate_into(0, 0, a, &sum); part1_state.accumulate_into(0, 1, a, &sum); - part1_state.accumulate_into(0, 2, a, &sum); + part1_state.accumulate_into(0, 0, a, &min); part1_state.accumulate_into(0, 1, a, &min); - part1_state.accumulate_into(0, 2, a, &min); } for a in vec2 { - part2_state.accumulate_into(0, 1, a, &sum); - part2_state.accumulate_into(0, 3, a, &sum); - part2_state.accumulate_into(0, 1, a, &min); - part2_state.accumulate_into(0, 3, a, &min); + part2_state.accumulate_into(0, 0, a, &sum); + part2_state.accumulate_into(0, 2, a, &sum); + part2_state.accumulate_into(0, 0, a, &min); + part2_state.accumulate_into(0, 2, a, &min); } let mut actual = part1_state + .clone() .finalize(&sum, 0, &g, |c| c) .into_iter() .collect_vec(); @@ -345,10 +315,12 @@ mod state_test { vec![ ("1".to_string(), actual_sum_1), ("2".to_string(), actual_sum_1), + ("3".to_string(), 0), ] ); let mut actual = part1_state + .clone() .finalize(&min, 0, &g, |c| c) .into_iter() .collect_vec(); @@ -360,10 +332,12 @@ mod state_test { vec![ ("1".to_string(), actual_min_1), ("2".to_string(), actual_min_1), + ("3".to_string(), i32::MAX), ] ); let mut actual = part2_state + .clone() .finalize(&sum, 0, &g, |c| c) .into_iter() .collect_vec(); @@ -374,11 +348,13 @@ mod state_test { actual, vec![ ("1".to_string(), actual_sum_2), - ("3".to_string(), actual_sum_2) + ("2".to_string(), 0), + ("3".to_string(), actual_sum_2), ] ); let mut actual = part2_state + .clone() .finalize(&min, 0, &g, |c| c) .into_iter() .collect_vec(); @@ -389,12 +365,14 @@ mod state_test { actual, vec![ ("1".to_string(), actual_min_2), - ("3".to_string(), actual_min_2) + ("2".to_string(), i32::MAX), + ("3".to_string(), actual_min_2), ] ); - ShuffleComputeState::merge_mut(&mut part1_state, &part2_state, &sum, 0); + ShuffleComputeState::merge_mut(&mut part1_state, &part2_state, sum, 0); let mut actual = part1_state + .clone() .finalize(&sum, 0, &g, |c| c) .into_iter() .collect_vec(); @@ -406,12 +384,13 @@ mod state_test { vec![ ("1".to_string(), (actual_sum_1 + actual_sum_2)), ("2".to_string(), actual_sum_1), - ("3".to_string(), actual_sum_2) + ("3".to_string(), actual_sum_2), ] ); - ShuffleComputeState::merge_mut(&mut part1_state, &part2_state, &min, 0); + ShuffleComputeState::merge_mut(&mut part1_state, &part2_state, min, 0); let mut actual = part1_state + .clone() .finalize(&min, 0, &g, |c| c) .into_iter() .collect_vec(); @@ -423,7 +402,7 @@ mod state_test { vec![ ("1".to_string(), actual_min_1.min(actual_min_2)), ("2".to_string(), actual_min_1), - ("3".to_string(), actual_min_2) + ("3".to_string(), actual_min_2), ] ); } diff --git a/raphtory/src/core/state/shard_state.rs b/raphtory/src/core/state/morcel_state.rs similarity index 81% rename from raphtory/src/core/state/shard_state.rs rename to raphtory/src/core/state/morcel_state.rs index 4e15ab2ff3..bbb919b3ff 100644 --- a/raphtory/src/core/state/shard_state.rs +++ b/raphtory/src/core/state/morcel_state.rs @@ -1,16 +1,17 @@ use super::{accumulator_id::AccId, compute_state::ComputeState, StateType}; -use crate::{core::agg::Accumulator, db::view_api::internal::GraphViewInternalOps}; +use crate::{core::state::agg::Accumulator, db::api::view::GraphViewOps}; use rustc_hash::FxHashMap; use std::collections::HashMap; pub const GLOBAL_STATE_KEY: usize = 0; #[derive(Debug, Clone)] -pub struct ShardComputeState { +pub struct MorcelComputeState { + morcel_size: usize, pub(crate) states: FxHashMap, } -impl ShardComputeState { +impl MorcelComputeState { pub(crate) fn copy_over_next_ss(&mut self, ss: usize) { for (_, state) in self.states.iter_mut() { state.clone_current_into_other(ss); @@ -25,31 +26,10 @@ impl ShardComputeState { } } - pub(crate) fn fold, F, B>( + pub fn read_vec, G: GraphViewOps>( &self, ss: usize, - b: B, agg_ref: &AccId, - f: F, - ) -> B - where - F: FnOnce(B, &u64, OUT) -> B + Copy, - A: 'static, - B: std::fmt::Debug, - OUT: StateType, - { - if let Some(state) = self.states.get(&agg_ref.id()) { - state.fold::(ss, b, f) - } else { - b - } - } - - pub fn read_vec, G: GraphViewInternalOps>( - &self, - ss: usize, - agg_ref: &AccId, - shard_id: usize, g: &G, ) -> Option> where @@ -57,7 +37,7 @@ impl ShardComputeState { A: 'static, { let cs = self.states.get(&agg_ref.id())?; - Some(cs.finalize::(ss, shard_id, g)) + Some(cs.finalize::(ss, g)) } pub(crate) fn set_from_other>( @@ -131,8 +111,9 @@ impl ShardComputeState { state.read_ref::(ss, i) } - pub(crate) fn new() -> Self { - ShardComputeState { + pub(crate) fn new(morcel_size: usize) -> Self { + MorcelComputeState { + morcel_size, states: FxHashMap::default(), } } @@ -152,14 +133,28 @@ impl ShardComputeState { .or_insert_with(|| CS::new_mutable_primitive(ACC::zero())); state.agg::(ss, a, key); } + + pub(crate) fn iter>( + &self, + ss: usize, + agg_ref: &AccId, + ) -> Box> + '_> + where + A: StateType, + { + if let Some(state) = self.states.get(&agg_ref.id()) { + Box::new(state.iter(ss, self.morcel_size).map(|v| Some(v))) + } else { + Box::new(std::iter::repeat(None).take(self.morcel_size)) + } + } } -impl ShardComputeState { - pub fn finalize, G: GraphViewInternalOps>( +impl MorcelComputeState { + pub fn finalize, G: GraphViewOps>( &self, ss: usize, agg_ref: &AccId, - shard_id: usize, g: &G, ) -> HashMap where @@ -168,7 +163,7 @@ impl ShardComputeState { { self.states .get(&agg_ref.id()) - .map(|s| s.finalize::(ss, shard_id, g)) + .map(|s| s.finalize::(ss, g)) .unwrap_or(HashMap::::default()) } } diff --git a/raphtory/src/core/state/shuffle_state.rs b/raphtory/src/core/state/shuffle_state.rs index 4a8e4f68ff..f4e87238a9 100644 --- a/raphtory/src/core/state/shuffle_state.rs +++ b/raphtory/src/core/state/shuffle_state.rs @@ -1,87 +1,34 @@ -use crate::db::task::task_state::{Global, Shard}; -use crate::db::view_api::GraphViewOps; -use crate::{ - core::{agg::Accumulator, utils::get_shard_id_from_global_vid}, - db::view_api::internal::GraphViewInternalOps, -}; -use std::borrow::Borrow; -use std::collections::HashMap; -use std::sync::Arc; - use super::{ accumulator_id::AccId, compute_state::ComputeState, - shard_state::{ShardComputeState, GLOBAL_STATE_KEY}, + morcel_state::{MorcelComputeState, GLOBAL_STATE_KEY}, StateType, }; +use crate::{ + core::state::agg::Accumulator, + db::{ + api::view::GraphViewOps, + task::task_state::{Global, Shard}, + }, +}; +use std::{borrow::Borrow, collections::HashMap, sync::Arc}; #[derive(Debug, Clone)] pub struct ShuffleComputeState { - pub global: ShardComputeState, - pub parts: Vec>, + morcel_size: usize, + pub global: MorcelComputeState, + pub parts: Vec>, } // every partition has a struct as such impl ShuffleComputeState { - pub fn fold_state, B, F>( - &self, - ss: usize, - b: B, - agg_ref: &AccId, - f: F, - ) -> B - where - A: StateType, - B: std::fmt::Debug, - OUT: StateType, - F: Fn(B, &u64, OUT) -> B + Copy, - { - let out_b = self - .parts - .iter() - .fold(b, |b, part| part.fold(ss, b, agg_ref, f)); - out_b - } - - pub fn fold_state_internal, B, F>( - &self, - ss: usize, - b: B, - agg_ref: &AccId, - f: F, - ) -> B - where - A: StateType, - B: std::fmt::Debug, - OUT: StateType, - F: Fn(B, usize, usize, OUT) -> B + Copy, - { - let out_b = self.parts.iter().enumerate().fold(b, |b, (part_id, part)| { - part.fold(ss, b, agg_ref, |b, id, out| { - f(b, part_id, *id as usize, out) - }) - }); - out_b + fn resolve_pid(&self, p_id: usize) -> (usize, usize) { + let morcel_id = p_id / self.morcel_size; + let offset = p_id % self.morcel_size; + (morcel_id, offset) } pub fn merge_mut>( - &mut self, - other: &Self, - agg_ref: &AccId, - ss: usize, - ) where - A: StateType, - { - // zip the two partitions - // merge each shard - assert_eq!(self.parts.len(), other.parts.len()); - self.parts - .iter_mut() - .zip(other.parts.iter()) - .for_each(|(s, o)| s.merge(o, agg_ref, ss)); - } - - pub fn merge_mut_2>( &mut self, other: &Self, agg_ref: AccId, @@ -147,61 +94,50 @@ impl ShuffleComputeState { self.global.reset_states(ss, states); } - pub fn new(n_parts: usize) -> Self { - Self { - parts: (0..n_parts) - .into_iter() - .map(|_| ShardComputeState::new()) - .collect(), - global: ShardComputeState::new(), + pub fn new(total_len: usize, n_parts: usize, morcel_size: usize) -> Self { + let last_one_size = total_len % morcel_size; + let mut parts: Vec> = (0..n_parts - 1) + .into_iter() + .map(|_| MorcelComputeState::new(morcel_size)) + .collect(); + + if last_one_size != 0 { + parts.push(MorcelComputeState::new(last_one_size)); + } else { + parts.push(MorcelComputeState::new(morcel_size)); } - } - pub fn keys(&self, part_num: usize) -> impl Iterator + '_ { - self.parts[part_num] - .states - .iter() - .flat_map(|(_, cs)| cs.iter_keys()) + Self { + morcel_size, + parts, + global: MorcelComputeState::new(1), + } } - pub fn changed_keys(&self, part_num: usize, ss: usize) -> impl Iterator + '_ { - self.parts[part_num] - .states - .iter() - .flat_map(move |(_, cs)| cs.iter_keys_changed(ss)) + pub fn global() -> Self { + Self { + morcel_size: 1, + parts: vec![], + global: MorcelComputeState::new(1), + } } pub fn accumulate_into>( &mut self, ss: usize, - into: usize, - a: IN, - agg_ref: &AccId, - ) where - A: StateType, - { - let part = get_shard_id_from_global_vid(into as u64, self.parts.len()); - self.parts[part].accumulate_into(ss, into, a, agg_ref) - } - - pub fn accumulate_into_pid>( - &mut self, - ss: usize, - g_id: u64, p_id: usize, a: IN, agg_ref: &AccId, ) where A: StateType, { - let part = get_shard_id_from_global_vid(g_id, self.parts.len()); - self.parts[part].accumulate_into(ss, p_id, a, agg_ref) + let (morcel_id, offset) = self.resolve_pid(p_id); + self.parts[morcel_id].accumulate_into(ss, offset, a, agg_ref) } pub fn read_with_pid>( &self, ss: usize, - g_id: u64, p_id: usize, agg_ref: &AccId, ) -> Option @@ -209,8 +145,8 @@ impl ShuffleComputeState { A: StateType, OUT: std::fmt::Debug, { - let part = get_shard_id_from_global_vid(g_id, self.parts.len()); - self.parts[part].read::(p_id, agg_ref.id(), ss) + let (morcel_id, offset) = self.resolve_pid(p_id); + self.parts[morcel_id].read::(offset, agg_ref.id(), ss) } pub fn accumulate_global>( @@ -224,46 +160,32 @@ impl ShuffleComputeState { self.global .accumulate_into(ss, GLOBAL_STATE_KEY, a, agg_ref) } - // reads the value from K if it's set we return Ok(a) else we return Err(zero) from the monoid + pub fn read>( &self, ss: usize, - into: usize, + p_id: usize, agg_ref: &AccId, ) -> Option where A: StateType, OUT: std::fmt::Debug, { - let part = get_shard_id_from_global_vid(into as u64, self.parts.len()); - self.parts[part].read::(into, agg_ref.id(), ss) + let (morcel_id, offset) = self.resolve_pid(p_id); + self.parts[morcel_id].read::(offset, agg_ref.id(), ss) } pub fn read_ref>( &self, ss: usize, - into: usize, - agg_ref: &AccId, - ) -> Option<&A> - where - A: StateType, - { - let part = get_shard_id_from_global_vid(into as u64, self.parts.len()); - self.parts[part].read_ref::(into, agg_ref.id(), ss) - } - - pub fn read_ref_with_pid>( - &self, - ss: usize, - g_id: u64, p_id: usize, agg_ref: &AccId, ) -> Option<&A> where A: StateType, { - let part = get_shard_id_from_global_vid(g_id, self.parts.len()); - self.parts[part].read_ref::(p_id, agg_ref.id(), ss) + let (morcel_id, offset) = self.resolve_pid(p_id); + self.parts[morcel_id].read_ref::(offset, agg_ref.id(), ss) } pub fn read_global>( @@ -279,25 +201,6 @@ impl ShuffleComputeState { .read::(GLOBAL_STATE_KEY, agg_ref.id(), ss) } - pub fn read_vec_partition, G: GraphViewInternalOps>( - &self, - ss: usize, - agg_def: &AccId, - g: &G, - ) -> Vec> - where - OUT: StateType, - A: 'static, - { - self.parts - .iter() - .enumerate() - .flat_map(|(shard_id, part)| part.read_vec(ss, agg_def, shard_id, g)) - .collect() - } -} - -impl ShuffleComputeState { pub fn finalize, G: GraphViewOps>( &self, agg_def: &AccId, @@ -307,34 +210,52 @@ impl ShuffleComputeState { ) -> HashMap where OUT: StateType, - A: 'static, + A: StateType, F: Fn(OUT) -> B + Copy, { - let r = self - .parts + self.iter(ss, *agg_def) + .map(|(v_id, a)| { + let out = a + .map(|a| ACC::finish(a)) + .unwrap_or_else(|| ACC::finish(&ACC::zero())); + (g.vertex_name(v_id.into()).to_string(), f(out)) + }) + .collect() + } + + pub fn iter<'a, A: StateType, IN: 'a, OUT: 'a, ACC: Accumulator>( + &'a self, + ss: usize, + acc_id: AccId, + ) -> impl Iterator)> + 'a { + self.parts .iter() + .flat_map(move |part| part.iter(ss, &acc_id)) .enumerate() - .map(|(shard_id, part)| part.finalize(ss, &agg_def, shard_id, g)); + } - r.into_iter() - .flat_map(|c| c.into_iter().map(|(k, v)| (k, f(v)))) - .collect() + pub fn iter_out<'a, A: StateType, IN: 'a, OUT: 'a, ACC: Accumulator>( + &'a self, + ss: usize, + acc_id: AccId, + ) -> impl Iterator + 'a { + self.iter(ss, acc_id).map(|(id, a)| { + let out = a + .map(|a| ACC::finish(a)) + .unwrap_or_else(|| ACC::finish(&ACC::zero())); + (id, out) + }) } } -pub struct EvalGlobalState { +pub struct EvalGlobalState { ss: usize, - g: G, pub(crate) global_state: Global, } -impl EvalGlobalState { - pub fn new(ss: usize, g: G, global_state: Global) -> EvalGlobalState { - Self { - ss, - g, - global_state, - } +impl EvalGlobalState { + pub fn new(ss: usize, global_state: Global) -> EvalGlobalState { + Self { ss, global_state } } pub fn finalize>( @@ -354,6 +275,7 @@ impl EvalGlobalState { } } +#[derive(Debug)] pub struct EvalShardState { ss: usize, g: G, @@ -370,22 +292,25 @@ impl EvalShardState { } pub fn finalize>( - &self, + self, agg_def: &AccId, f: F, ) -> HashMap where OUT: StateType, - A: 'static, + A: StateType, F: Fn(OUT) -> B + Copy, { - self.shard_states - .inner() - .finalize(agg_def, self.ss, &self.g, f) + let inner = self.shard_states.consume(); + if let Ok(inner) = inner { + inner.finalize(agg_def, self.ss, &self.g, f) + } else { + HashMap::new() + } } - pub fn values(self) -> Shard { - self.shard_states + pub fn values(&self) -> &Shard { + &self.shard_states } } @@ -409,19 +334,19 @@ impl EvalLocalState { } pub fn finalize>( - &self, + self, agg_def: &AccId, f: F, ) -> HashMap where OUT: StateType, - A: 'static, + A: StateType, F: Fn(OUT) -> B + Copy, { self.local_states - .iter() + .into_iter() .flat_map(|state| { - if let Some(state) = state.as_ref() { + if let Some(state) = Arc::try_unwrap(state).ok().flatten() { state.finalize(agg_def, self.ss, &self.g, f) } else { HashMap::::new() diff --git a/raphtory/src/core/storage/iter.rs b/raphtory/src/core/storage/iter.rs new file mode 100644 index 0000000000..46c748a5f8 --- /dev/null +++ b/raphtory/src/core/storage/iter.rs @@ -0,0 +1,108 @@ +use super::RawStorage; +use std::{ops::Deref, sync::Arc}; + +pub struct Iter<'a, T: Default, const N: usize> { + raw: &'a RawStorage, + segment: usize, + offset: usize, + current: Option>, +} + +// impl new for Iter +impl<'a, T: Default, const N: usize> Iter<'a, T, N> { + pub fn new(raw: &'a RawStorage) -> Self { + Iter { + raw, + segment: 0, + offset: 0, + current: None, + } + } +} + +type GuardIter<'a, T> = ( + Arc>>, + std::slice::Iter<'a, T>, +); + +pub struct RefT<'a, T, const N: usize> { + _guard: Arc>>, + t: &'a T, + i: usize, +} + +impl<'a, T, const N: usize> Clone for RefT<'_, T, N> { + fn clone(&self) -> Self { + RefT { + _guard: self._guard.clone(), + t: self.t, + i: self.i, + } + } +} + +impl<'a, T, const N: usize> Deref for RefT<'a, T, N> { + type Target = T; + + fn deref(&self) -> &Self::Target { + self.t + } +} + +// simple impl for RefT that returns &T in the value function +impl<'a, T, const N: usize> RefT<'a, T, N> { + pub fn value(&self) -> &T { + self.t + } + + pub fn index(&self) -> usize { + self.i + } +} + +/// # Safety +/// +/// Requires that you ensure the reference does not become invalid. +/// The object has to outlive the reference. +pub unsafe fn change_lifetime_const<'a, 'b, T>(x: &'a T) -> &'b T { + &*(x as *const T) +} + +impl<'a, T: std::fmt::Debug + Default, const N: usize> Iterator for Iter<'a, T, N> { + type Item = RefT<'a, T, N>; + + fn next(&mut self) -> Option { + loop { + if let Some((guard, iter)) = self.current.as_mut() { + if let Some(t) = iter.next() { + let guard = guard.clone(); + let next = Some(RefT { + _guard: guard, + i: (self.offset * N + (self.segment - 1)), + t, + }); + self.offset += 1; + return next; + } + } + + if self.segment >= N { + return None; + } + + // get the next segment + let guard = self.raw.data[self.segment].data.read(); + + // convince the rust compiler that the reference is valid + let raw = unsafe { change_lifetime_const(&*guard) }; + + // grab the iterator + let iter = raw.iter(); + + // set the current segment with the new iterator + self.current = Some((Arc::new(guard), iter)); + self.offset = 0; + self.segment += 1; + } + } +} diff --git a/raphtory/src/core/lazy_vec.rs b/raphtory/src/core/storage/lazy_vec.rs similarity index 70% rename from raphtory/src/core/lazy_vec.rs rename to raphtory/src/core/storage/lazy_vec.rs index 53c548ae80..c1d34765a6 100644 --- a/raphtory/src/core/lazy_vec.rs +++ b/raphtory/src/core/storage/lazy_vec.rs @@ -1,5 +1,6 @@ +use crate::core::utils::errors::GraphError; use serde::{Deserialize, Serialize}; -use std::fmt::Debug; +use std::{fmt::Debug, iter}; #[derive(thiserror::Error, Debug, PartialEq)] #[error("cannot set previous value '{previous_value:?}' to '{new_value:?}' in position '{index}'")] @@ -36,16 +37,17 @@ where LazyVec::LazyVec1(id, value) } - pub(crate) fn filled_ids(&self) -> Vec { + pub(crate) fn filled_ids(&self) -> Box + '_> { match self { - LazyVec::Empty => Default::default(), - LazyVec::LazyVec1(id, _) => vec![*id], - LazyVec::LazyVecN(vector) => vector - .iter() - .enumerate() - .filter(|&(_, value)| *value != Default::default()) - .map(|(id, _)| id) - .collect(), + LazyVec::Empty => Box::new(iter::empty()), + LazyVec::LazyVec1(id, _) => Box::new(iter::once(*id)), + LazyVec::LazyVecN(vector) => Box::new( + vector + .iter() + .enumerate() + .filter(|&(_, value)| *value != Default::default()) + .map(|(id, _)| id), + ), } } @@ -70,7 +72,9 @@ where return Err(IllegalSet::new(id, only_value.clone(), value)); } } else { - let mut vector = vec![Default::default(); usize::max(id, *only_id) + 1]; + let len = usize::max(id, *only_id) + 1; + let mut vector = Vec::with_capacity(len + 1); + vector.resize(len, Default::default()); vector[id] = value; vector[*only_id] = only_value.clone(); *self = LazyVec::LazyVecN(vector) @@ -99,22 +103,27 @@ where } } - pub(crate) fn update_or_set(&mut self, id: usize, updater: F, default: A) + pub(crate) fn update(&mut self, id: usize, updater: F) -> Result<(), GraphError> where - F: FnOnce(&mut A), + F: FnOnce(&mut A) -> Result<(), GraphError>, { match self.get_mut(id) { - Some(value) => updater(value), - None => self - .set(id, default) - .expect("Set failed over a non existing value"), - } + Some(value) => updater(value)?, + None => { + let mut value = A::default(); + updater(&mut value)?; + self.set(id, value) + .expect("Set failed over a non existing value") + } + }; + Ok(()) } } #[cfg(test)] mod lazy_vec_tests { use super::*; + use itertools::Itertools; #[test] fn normal_operation() { @@ -126,17 +135,14 @@ mod lazy_vec_tests { assert_eq!(vec.get(5), Some(&55)); assert_eq!(vec.get(1), Some(&11)); assert_eq!(vec.get(0), Some(&0)); - assert_eq!(vec.get(10), None); // FIXME: this should return the default, 0, as well, there is no need to return Option from get() + assert_eq!(vec.get(10), None); - // FIXME: replace update_or_set() with update() - // the behavior should be the same for both cases, because we should be able to assume that - // any cell is prefilled with default values and can therefore be safely updated - vec.update_or_set(6, |n| *n += 1, 66); + vec.update(6, |n| Ok(*n += 1)); assert_eq!(vec.get(6), Some(&1)); - vec.update_or_set(9, |n| *n += 1, 99); - assert_eq!(vec.get(9), Some(&99)); + vec.update(9, |n| Ok(*n += 1)); + assert_eq!(vec.get(9), Some(&1)); - assert_eq!(vec.filled_ids(), vec![1, 5, 6, 8, 9]); + assert_eq!(vec.filled_ids().collect_vec(), vec![1, 5, 6, 8, 9]); } #[test] diff --git a/raphtory/src/core/storage/locked_view.rs b/raphtory/src/core/storage/locked_view.rs new file mode 100644 index 0000000000..85ce71796a --- /dev/null +++ b/raphtory/src/core/storage/locked_view.rs @@ -0,0 +1,105 @@ +use dashmap::mapref::one::Ref; +use parking_lot::{MappedRwLockReadGuard, RwLockReadGuard}; +use rustc_hash::FxHasher; +use std::{ + borrow::Borrow, + cmp::Ordering, + fmt::{Debug, Formatter}, + hash::{BuildHasherDefault, Hash, Hasher}, + ops::Deref, +}; +use tantivy::directory::Lock; + +pub enum LockedView<'a, T> { + LockMapped(parking_lot::MappedRwLockReadGuard<'a, T>), + Locked(parking_lot::RwLockReadGuard<'a, T>), + DashMap(Ref<'a, usize, T, BuildHasherDefault>), +} + +impl<'a, T, O> AsRef for LockedView<'a, O> +where + T: ?Sized, + as Deref>::Target: AsRef, +{ + fn as_ref(&self) -> &T { + self.deref().as_ref() + } +} + +impl<'a, T> Borrow for LockedView<'a, T> { + fn borrow(&self) -> &T { + self.deref() + } +} + +impl<'a> From> for String { + fn from(value: LockedView<'a, String>) -> Self { + value.deref().clone() + } +} + +impl<'a, T: PartialEq, Rhs, LRhs: Deref> PartialEq for LockedView<'a, T> { + fn eq(&self, other: &LRhs) -> bool { + self.deref() == other.deref() + } +} + +impl<'a, T: Eq> Eq for LockedView<'a, T> {} + +impl<'a, T: PartialOrd, Rhs, LRhs: Deref> PartialOrd + for LockedView<'a, T> +{ + fn partial_cmp(&self, other: &LRhs) -> Option { + self.deref().partial_cmp(other.deref()) + } +} + +impl<'a, T: Ord> Ord for LockedView<'a, T> { + fn cmp(&self, other: &Self) -> Ordering { + self.deref().cmp(other.deref()) + } +} + +impl<'a, T: Hash> Hash for LockedView<'a, T> { + fn hash(&self, state: &mut H) { + self.deref().hash(state) + } +} + +impl<'a, T> From> for LockedView<'a, T> { + fn from(value: MappedRwLockReadGuard<'a, T>) -> Self { + Self::LockMapped(value) + } +} + +impl<'a, T> From> for LockedView<'a, T> { + fn from(value: RwLockReadGuard<'a, T>) -> Self { + Self::Locked(value) + } +} + +impl<'a, T> From>> + for LockedView<'a, T> +{ + fn from(value: Ref<'a, usize, T, BuildHasherDefault>) -> Self { + Self::DashMap(value) + } +} + +impl<'a, T> Deref for LockedView<'a, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + match self { + LockedView::LockMapped(guard) => guard.deref(), + LockedView::DashMap(r) => (*r).deref(), + LockedView::Locked(guard) => guard.deref(), + } + } +} + +impl<'a, T: Debug> Debug for LockedView<'a, T> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "LockedView({:?})", self.deref()) + } +} diff --git a/raphtory/src/core/storage/mod.rs b/raphtory/src/core/storage/mod.rs new file mode 100644 index 0000000000..fc3c5a6d81 --- /dev/null +++ b/raphtory/src/core/storage/mod.rs @@ -0,0 +1,448 @@ +#![allow(unused)] + +pub(crate) mod iter; +pub mod lazy_vec; +pub mod locked_view; +pub mod sorted_vec_map; +pub mod timeindex; + +use self::iter::Iter; +use lock_api; +use locked_view::LockedView; +use parking_lot::{RwLock, RwLockReadGuard}; +use rayon::prelude::*; +use serde::{Deserialize, Serialize}; +use std::{ + array, + fmt::Debug, + iter::FusedIterator, + ops::{Deref, DerefMut}, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, +}; + +type ArcRwLockReadGuard = lock_api::ArcRwLockReadGuard; + +#[inline] +fn resolve(index: usize) -> (usize, usize) { + let bucket = index % N; + let offset = index / N; + (bucket, offset) +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct LockVec { + data: Arc>>, +} + +impl PartialEq for LockVec { + fn eq(&self, other: &Self) -> bool { + let a = self.data.read(); + let b = other.data.read(); + a.deref() == b.deref() + } +} + +impl LockVec { + pub fn new() -> Self { + Self { + data: Arc::new(RwLock::new(Vec::new())), + } + } + + #[inline] + pub fn read_arc_lock(&self) -> ArcRwLockReadGuard> { + RwLock::read_arc(&self.data) + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct RawStorage { + pub(crate) data: Box<[LockVec]>, + len: AtomicUsize, +} + +impl PartialEq for RawStorage { + fn eq(&self, other: &Self) -> bool { + self.data.eq(&other.data) + } +} + +#[derive(Debug)] +pub struct ReadLockedStorage { + locks: [ArcRwLockReadGuard>; N], + len: usize, +} + +impl ReadLockedStorage { + pub(crate) fn get(&self, index: usize) -> &T { + let (bucket, offset) = resolve::(index); + let bucket = &self.locks[bucket]; + &bucket[offset] + } + + pub(crate) fn iter(&self) -> impl Iterator + '_ { + self.locks.iter().flat_map(|v| v.iter()) + } + + pub(crate) fn par_iter(&self) -> impl ParallelIterator + '_ + where + T: Send + Sync, + { + self.locks.par_iter().flat_map(|v| v.par_iter()) + } + + pub(crate) fn into_iter(self) -> impl Iterator> + Send + where + T: Send + Sync + 'static, + { + self.locks + .into_iter() + .enumerate() + .flat_map(|(bucket, data)| { + let arc_data = Arc::new(data); + (0..arc_data.len()).map(move |offset| ArcEntry { + guard: arc_data.clone(), + i: offset, + }) + }) + } + + pub(crate) fn into_par_iter(self) -> impl ParallelIterator> + where + T: Send + Sync + 'static, + { + self.locks + .into_par_iter() + .enumerate() + .flat_map(|(bucket, data)| { + let arc_data = Arc::new(data); + (0..arc_data.len()) + .into_par_iter() + .map(move |offset| ArcEntry { + guard: arc_data.clone(), + i: offset, + }) + }) + } +} + +impl RawStorage { + pub fn count_with_filter bool + Send + Sync>(&self, f: F) -> usize { + self.read_lock().par_iter().filter(|x| f(x)).count() + } +} + +impl RawStorage { + #[inline] + pub fn read_lock(&self) -> ReadLockedStorage { + let guards: [ArcRwLockReadGuard>; N] = + array::from_fn(|i| self.data[i].read_arc_lock()); + ReadLockedStorage { + locks: guards, + len: self.len(), + } + } + + pub fn indices(&self) -> impl Iterator + Send + '_ { + 0..self.len() + } + + pub fn new() -> Self { + let data: [LockVec; N] = array::from_fn(|_| LockVec::new()); + let data = Box::new(data); + Self { + data, + len: AtomicUsize::new(0), + } + } + + pub fn push(&self, mut value: T, f: F) -> usize { + let index = self.len.fetch_add(1, Ordering::SeqCst); + let (bucket, offset) = resolve::(index); + let mut vec = self.data[bucket].data.write(); + if offset >= vec.len() { + vec.resize_with(offset + 1, || Default::default()); + } + f(index, &mut value); + vec[offset] = value; + index + } + + #[inline] + pub fn entry(&self, index: usize) -> Entry<'_, T, N> { + let (bucket, _) = resolve::(index); + let guard = self.data[bucket].data.read_recursive(); + Entry { + offset: index, + guard, + } + } + + #[inline] + pub fn get(&self, index: usize) -> impl Deref + '_ { + let (bucket, offset) = resolve::(index); + let guard = self.data[bucket].data.read_recursive(); + RwLockReadGuard::map(guard, |guard| &guard[offset]) + } + + pub fn entry_arc(&self, index: usize) -> ArcEntry { + let (bucket, offset) = resolve::(index); + let guard = &self.data[bucket].data; + let arc_guard = RwLock::read_arc_recursive(guard); + ArcEntry { + i: offset, + guard: Arc::new(arc_guard), + } + } + + pub fn entry_mut(&self, index: usize) -> EntryMut<'_, T> { + let (bucket, offset) = resolve::(index); + let guard = self.data[bucket].data.write(); + EntryMut { i: offset, guard } + } + + // This helps get the right locks when adding an edge + pub fn pair_entry_mut(&self, i: usize, j: usize) -> PairEntryMut<'_, T> { + let (bucket_i, offset_i) = resolve::(i); + let (bucket_j, offset_j) = resolve::(j); + // always acquire lock for smaller bucket first to avoid deadlock between two updates for the same pair of buckets + if bucket_i < bucket_j { + let guard_i = self.data[bucket_i].data.write(); + let guard_j = self.data[bucket_j].data.write(); + PairEntryMut::Different { + i: offset_i, + j: offset_j, + guard1: guard_i, + guard2: guard_j, + } + } else if bucket_i > bucket_j { + let guard_j = self.data[bucket_j].data.write(); + let guard_i = self.data[bucket_i].data.write(); + PairEntryMut::Different { + i: offset_i, + j: offset_j, + guard1: guard_i, + guard2: guard_j, + } + } else { + PairEntryMut::Same { + i: offset_i, + j: offset_j, + guard: self.data[bucket_i].data.write(), + } + } + } + + #[inline] + pub fn len(&self) -> usize { + self.len.load(Ordering::SeqCst) + } + + pub fn iter(&self) -> Iter { + Iter::new(self) + } +} + +#[derive(Debug)] +pub struct Entry<'a, T: 'static, const N: usize> { + offset: usize, + guard: RwLockReadGuard<'a, Vec>, +} + +impl<'a, T: 'static, const N: usize> Clone for Entry<'a, T, N> { + fn clone(&self) -> Self { + let guard = RwLockReadGuard::rwlock(&self.guard).read_recursive(); + let i = self.offset; + Self { offset: i, guard } + } +} + +#[derive(Debug)] +pub struct ArcEntry { + guard: Arc>>, + i: usize, +} + +impl Clone for ArcEntry { + fn clone(&self) -> Self { + Self { + guard: self.guard.clone(), + i: self.i, + } + } +} + +impl Deref for ArcEntry { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.guard[self.i] + } +} + +impl<'a, T, const N: usize> Entry<'a, T, N> { + pub fn value(&self) -> &T { + let (_, offset) = resolve::(self.offset); + &self.guard[offset] + } + + pub fn index(&self) -> usize { + self.offset + } + + pub fn map &U>(self, f: F) -> LockedView<'a, U> { + let (_, offset) = resolve::(self.offset); + let mapped_guard = RwLockReadGuard::map(self.guard, |guard| { + let what = &guard[offset]; + f(what) + }); + + LockedView::LockMapped(mapped_guard) + } +} + +impl<'a, T, const N: usize> Deref for Entry<'a, T, N> { + type Target = T; + + fn deref(&self) -> &Self::Target { + let (_, offset) = resolve::(self.offset); + &self.guard[offset] + } +} + +pub enum PairEntryMut<'a, T: 'static> { + Same { + i: usize, + j: usize, + guard: parking_lot::RwLockWriteGuard<'a, Vec>, + }, + Different { + i: usize, + j: usize, + guard1: parking_lot::RwLockWriteGuard<'a, Vec>, + guard2: parking_lot::RwLockWriteGuard<'a, Vec>, + }, +} + +impl<'a, T: 'static> PairEntryMut<'a, T> { + pub(crate) fn get_mut_i(&mut self) -> &mut T { + match self { + PairEntryMut::Same { i, guard, .. } => &mut guard[*i], + PairEntryMut::Different { i, guard1, .. } => &mut guard1[*i], + } + } + + pub(crate) fn get_mut_j(&mut self) -> &mut T { + match self { + PairEntryMut::Same { j, guard, .. } => &mut guard[*j], + PairEntryMut::Different { j, guard2, .. } => &mut guard2[*j], + } + } +} + +pub struct EntryMut<'a, T: 'static> { + i: usize, + guard: parking_lot::RwLockWriteGuard<'a, Vec>, +} + +impl<'a, T> Deref for EntryMut<'a, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.guard[self.i] + } +} + +impl<'a, T> DerefMut for EntryMut<'a, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.guard[self.i] + } +} + +#[cfg(test)] +mod test { + use rayon::prelude::{IntoParallelIterator, ParallelIterator}; + + use super::RawStorage; + + #[test] + fn add_5_values_to_storage() { + let storage = RawStorage::::new(); + + for i in 0..5 { + storage.push(i.to_string(), |_, _| {}); + } + + assert_eq!(storage.len(), 5); + + for i in 0..5 { + let entry = storage.entry(i); + assert_eq!(*entry, i.to_string()); + } + + let items_iter = storage.iter(); + + let actual = items_iter.map(|s| (*s).to_owned()).collect::>(); + + assert_eq!(actual, vec!["0", "2", "4", "1", "3"]); + } + + #[test] + fn test_index_correctness() { + let storage = RawStorage::::new(); + + for i in 0..5 { + storage.push(i.to_string(), |_, _| {}); + } + + let items_iter = storage.iter(); + let actual = items_iter + .map(|s| (s.index(), (*s).to_owned())) + .collect::>(); + assert_eq!( + actual, + vec![(0, "0"), (2, "2"), (4, "4"), (1, "1"), (3, "3"),] + .into_iter() + .map(|(i, s)| (i, s.to_string())) + .collect::>() + ); + } + + #[test] + fn test_entry() { + let storage = RawStorage::::new(); + + for i in 0..5 { + storage.push(i.to_string(), |_, _| {}); + } + + for i in 0..5 { + let entry = storage.entry(i); + assert_eq!(*entry, i.to_string()); + } + } + + use pretty_assertions::assert_eq; + + #[quickcheck] + fn concurrent_push(v: Vec) -> bool { + let storage = RawStorage::::new(); + let mut expected = v + .into_par_iter() + .map(|v| { + storage.push(v, |_, _| {}); + v + }) + .collect::>(); + + let mut actual = storage.iter().map(|s| *s).collect::>(); + + actual.sort(); + expected.sort(); + + actual == expected + } +} diff --git a/raphtory/src/core/sorted_vec_map.rs b/raphtory/src/core/storage/sorted_vec_map.rs similarity index 83% rename from raphtory/src/core/sorted_vec_map.rs rename to raphtory/src/core/storage/sorted_vec_map.rs index 2f695bf5c4..c12e04c743 100644 --- a/raphtory/src/core/sorted_vec_map.rs +++ b/raphtory/src/core/storage/sorted_vec_map.rs @@ -1,17 +1,30 @@ -use std::ops::Range; - use serde::{ser::SerializeSeq, Deserialize, Serialize}; -use sorted_vector_map::{map::Entry, SortedVectorMap}; +use sorted_vector_map::SortedVectorMap; +use std::{borrow::Borrow, ops::Range}; // wrapper for SortedVectorMap -#[derive(Debug, PartialEq, Default, Clone)] +#[derive(Debug, PartialEq, Clone)] pub struct SVM(SortedVectorMap); +impl Default for SVM { + fn default() -> Self { + Self::new() + } +} + impl SVM { pub(crate) fn new() -> Self { Self(SortedVectorMap::new()) } + pub(crate) fn get(&self, k: &Q) -> Option<&V> + where + K: Borrow, + Q: Ord + ?Sized, + { + self.0.get(k) + } + pub(crate) fn insert(&mut self, k: K, v: V) -> Option { self.0.insert(k, v) } @@ -31,10 +44,6 @@ impl SVM { pub(crate) fn from_iter>(iter: I) -> Self { Self(SortedVectorMap::from_iter(iter)) } - - pub(crate) fn entry(&mut self, k: K) -> Entry { - self.0.entry(k) - } } impl IntoIterator for SVM diff --git a/raphtory/src/core/storage/timeindex.rs b/raphtory/src/core/storage/timeindex.rs new file mode 100644 index 0000000000..801f937747 --- /dev/null +++ b/raphtory/src/core/storage/timeindex.rs @@ -0,0 +1,480 @@ +use crate::{ + core::{entities::LayerIds, utils::time::error::ParseTimeError}, + db::api::mutation::{internal::InternalAdditionOps, InputTime, TryIntoInputTime}, +}; +use itertools::{Itertools, KMerge}; +use num_traits::Saturating; +use serde::{Deserialize, Serialize}; +use std::{ + cmp::{max, min}, + collections::BTreeSet, + fmt::Debug, + marker::PhantomData, + ops::{Deref, Range}, + sync::Arc, +}; +use tantivy::time::Time; + +use super::locked_view::LockedView; + +#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Ord, PartialOrd, Eq)] +pub struct TimeIndexEntry(i64, usize); + +pub trait AsTime: Debug + Copy + Ord + Eq + Send + Sync { + fn t(&self) -> &i64; + fn range(w: Range) -> Range; +} + +impl From for TimeIndexEntry { + fn from(value: i64) -> Self { + Self::start(value) + } +} + +impl TimeIndexEntry { + pub fn new(t: i64, s: usize) -> Self { + Self(t, s) + } + + pub fn from_input( + g: &G, + t: T, + ) -> Result { + let t = t.try_into_input_time()?; + Ok(match t { + InputTime::Simple(t) => Self::new(t, g.next_event_id()), + InputTime::Indexed(t, s) => Self::new(t, s), + }) + } + + pub fn start(t: i64) -> Self { + Self(t, 0) + } + + pub fn end(t: i64) -> Self { + Self(t.saturating_add(1), 0) + } +} + +impl AsTime for i64 { + fn t(&self) -> &i64 { + self + } + + fn range(w: Range) -> Range { + w + } +} + +impl AsTime for TimeIndexEntry { + fn t(&self) -> &i64 { + &self.0 + } + fn range(w: Range) -> Range { + Self::start(w.start)..Self::start(w.end) + } +} + +#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)] +pub enum TimeIndex { + #[default] + Empty, + One(T), + Set(BTreeSet), +} + +impl TimeIndex { + pub fn is_empty(&self) -> bool { + matches!(self, TimeIndex::Empty) + } + + pub fn one(ti: T) -> Self { + Self::One(ti) + } + pub fn insert(&mut self, ti: T) -> bool { + match self { + TimeIndex::Empty => { + *self = TimeIndex::One(ti); + true + } + TimeIndex::One(t0) => { + if t0 == &ti { + false + } else { + *self = TimeIndex::Set([*t0, ti].into_iter().collect()); + true + } + } + TimeIndex::Set(ts) => ts.insert(ti), + } + } + + pub(crate) fn contains(&self, w: Range) -> bool { + match self { + TimeIndex::Empty => false, + TimeIndex::One(t) => w.contains(t.t()), + TimeIndex::Set(ts) => ts.range(T::range(w)).next().is_some(), + } + } + + pub(crate) fn iter(&self) -> Box + Send + '_> { + match self { + TimeIndex::Empty => Box::new(std::iter::empty()), + TimeIndex::One(t) => Box::new(std::iter::once(t)), + TimeIndex::Set(ts) => Box::new(ts.iter()), + } + } + + pub(crate) fn range_iter( + &self, + w: Range, + ) -> Box + Send + '_> { + match self { + TimeIndex::Empty => Box::new(std::iter::empty()), + TimeIndex::One(t) => { + if w.contains(t.t()) { + Box::new(std::iter::once(t)) + } else { + Box::new(std::iter::empty()) + } + } + TimeIndex::Set(ts) => Box::new(ts.range(T::range(w))), + } + } + + // = note: see issue #65991 for more information + // = note: required when coercing `Box + Send>` into `Box + Send>` + pub(crate) fn range_iter_forward( + &self, + w: Range, + ) -> Box + Send + '_> { + Box::new(self.range_iter(w)) + } +} + +pub enum TimeIndexWindow<'a, T: AsTime> { + Empty, + TimeIndexRange { + timeindex: &'a TimeIndex, + range: Range, + }, + All(&'a TimeIndex), +} + +pub struct LayeredTimeIndexWindow<'a, T: AsTime> { + timeindex: Vec>, +} + +pub enum WindowIter<'a> { + Empty, + TimeIndexRange(Box + Send + 'a>), + All(Box + Send + 'a>), +} + +impl<'a> Iterator for WindowIter<'a> { + type Item = &'a i64; + + fn next(&mut self) -> Option { + match self { + WindowIter::Empty => None, + WindowIter::TimeIndexRange(iter) => iter.next(), + WindowIter::All(iter) => iter.next(), + } + } +} + +pub type LockedLayeredIndex<'a, T> = LayeredIndex<'a, T, LockedView<'a, Vec>>>; + +pub struct LayeredIndex<'a, T: AsTime, V: Deref>> + 'a> { + layers: LayerIds, + view: V, + marker: PhantomData<&'a Vec>>, +} + +impl<'a, T: AsTime, V: Deref>> + 'a> LayeredIndex<'a, T, V> { + pub fn new(layers: LayerIds, view: V) -> Self { + Self { + layers, + view, + marker: PhantomData, + } + } + + pub fn range_iter(&'a self, w: Range) -> Box + Send + '_> { + let iter = self + .view + .iter() + .enumerate() + .filter(|(i, _)| self.layers.contains(i)) + .map(|(_, t)| t.range_iter(w.clone()).map(|t| t.t())) + .kmerge() + .dedup(); + Box::new(iter) + } + + pub fn first(&self) -> Option { + self.view + .iter() + .enumerate() + .filter(|(i, _)| self.layers.contains(i)) + .map(|(_, t)| t.first_t()) + .min() + .flatten() + } + + pub fn active(&self, w: Range) -> bool { + self.view + .iter() + .enumerate() + .filter(|(i, _)| self.layers.contains(i)) + .any(|(_, t)| t.active(w.clone())) + } + + fn last_window(&self, w: Range) -> Option { + self.view + .iter() + .enumerate() + .filter(|(i, _)| self.layers.contains(i)) + .map(|(_, t)| t.range_iter(w.clone()).next_back().map(|t| *t.t())) + .max() + .flatten() + } +} + +impl<'a, T: AsTime, V: Deref>> + 'a> TimeIndexOps + for LayeredIndex<'a, T, V> +{ + type IterType<'b> = Box + Send + 'b> where Self: 'b; + type WindowType<'b> = LayeredTimeIndexWindow<'b, T> where Self: 'b; + type IndexType = T; + + fn active(&self, w: Range) -> bool { + self.view.iter().any(|t| t.active(w.clone())) + } + + fn range(&self, w: Range) -> LayeredTimeIndexWindow { + let timeindex = self + .view + .iter() + .enumerate() + .filter_map(|(l, t)| self.layers.contains(&l).then(|| t.range(w.clone()))) + .collect_vec(); + LayeredTimeIndexWindow { timeindex } + } + + fn first(&self) -> Option<&T> { + self.view.iter().flat_map(|t| t.first()).min() + } + + fn last(&self) -> Option<&T> { + self.view.iter().flat_map(|t| t.last()).max() + } + + fn iter_t(&self) -> Self::IterType<'_> { + let iter = self.view.iter().map(|t| t.iter_t()).kmerge().dedup(); + Box::new(iter) + } +} + +pub trait TimeIndexOps { + type IterType<'a>: Iterator + Send + 'a + where + Self: 'a; + + type WindowType<'a>: TimeIndexOps + 'a + where + Self: 'a; + type IndexType: AsTime; + + fn active(&self, w: Range) -> bool; + + fn range<'a>(&'a self, w: Range) -> Self::WindowType<'a>; + + fn first_t(&self) -> Option { + self.first().map(|ti| *ti.t()) + } + + fn first(&self) -> Option<&Self::IndexType>; + + fn last_t(&self) -> Option { + self.last().map(|ti| *ti.t()) + } + + fn last(&self) -> Option<&Self::IndexType>; + + fn iter_t(&self) -> Self::IterType<'_>; +} + +impl TimeIndexOps for TimeIndex { + type IterType<'a> = Box + Send + 'a> where T: 'a; + type WindowType<'a> = TimeIndexWindow<'a, T> where Self: 'a; + type IndexType = T; + + #[inline(always)] + fn active(&self, w: Range) -> bool { + match &self { + TimeIndex::Empty => false, + TimeIndex::One(t) => w.contains(t.t()), + TimeIndex::Set(ts) => ts.range(T::range(w)).next().is_some(), + } + } + + fn range(&self, w: Range) -> TimeIndexWindow<'_, T> { + match &self { + TimeIndex::Empty => TimeIndexWindow::Empty, + TimeIndex::One(t) => { + if w.contains(t.t()) { + TimeIndexWindow::All(self) + } else { + TimeIndexWindow::Empty + } + } + TimeIndex::Set(ts) => { + if let Some(min_val) = ts.first() { + if let Some(max_val) = ts.last() { + if min_val.t() >= &w.start && max_val.t() < &w.end { + TimeIndexWindow::All(self) + } else { + TimeIndexWindow::TimeIndexRange { + timeindex: self, + range: w, + } + } + } else { + TimeIndexWindow::Empty + } + } else { + TimeIndexWindow::Empty + } + } + } + } + + fn first(&self) -> Option<&T> { + match self { + TimeIndex::Empty => None, + TimeIndex::One(t) => Some(t), + TimeIndex::Set(ts) => ts.first(), + } + } + + fn last(&self) -> Option<&T> { + match self { + TimeIndex::Empty => None, + TimeIndex::One(t) => Some(t), + TimeIndex::Set(ts) => ts.last(), + } + } + + fn iter_t(&self) -> Box + Send + '_> { + match self { + TimeIndex::Empty => Box::new(std::iter::empty()), + TimeIndex::One(t) => Box::new(std::iter::once(t.t())), + TimeIndex::Set(ts) => Box::new(ts.iter().map(|ti| ti.t())), + } + } +} + +impl<'b, T: AsTime> TimeIndexOps for TimeIndexWindow<'b, T> +where + Self: 'b, +{ + type IterType<'a> = WindowIter<'a> where Self: 'a; + type WindowType<'a> = TimeIndexWindow<'a, T> where Self: 'a; + type IndexType = T; + + fn active(&self, w: Range) -> bool { + match self { + TimeIndexWindow::Empty => false, + TimeIndexWindow::TimeIndexRange { timeindex, range } => { + w.start < range.end + && w.end > range.start + && (timeindex.active(max(w.start, range.start)..min(w.end, range.end))) + } + TimeIndexWindow::All(timeindex) => timeindex.active(w), + } + } + + fn range(&self, w: Range) -> TimeIndexWindow { + match self { + TimeIndexWindow::Empty => TimeIndexWindow::Empty, + TimeIndexWindow::TimeIndexRange { timeindex, range } => { + let start = max(range.start, w.start); + let end = min(range.start, w.start); + if end <= start { + TimeIndexWindow::Empty + } else { + TimeIndexWindow::TimeIndexRange { + timeindex, + range: start..end, + } + } + } + TimeIndexWindow::All(timeindex) => timeindex.range(w), + } + } + + fn first(&self) -> Option<&T> { + match self { + TimeIndexWindow::Empty => None, + TimeIndexWindow::TimeIndexRange { timeindex, range } => { + timeindex.range_iter(range.clone()).next() + } + TimeIndexWindow::All(timeindex) => timeindex.first(), + } + } + + fn last(&self) -> Option<&T> { + match self { + TimeIndexWindow::Empty => None, + TimeIndexWindow::TimeIndexRange { timeindex, range } => { + timeindex.range_iter(range.clone()).next_back() + } + TimeIndexWindow::All(timeindex) => timeindex.last(), + } + } + + fn iter_t(&self) -> Self::IterType<'_> { + match self { + TimeIndexWindow::Empty => WindowIter::Empty, + TimeIndexWindow::TimeIndexRange { timeindex, range } => WindowIter::TimeIndexRange( + Box::new(timeindex.range_iter_forward(range.clone()).map(|t| t.t())), + ), + TimeIndexWindow::All(timeindex) => WindowIter::All(timeindex.iter_t()), + } + } +} + +impl<'b, T: AsTime> TimeIndexOps for LayeredTimeIndexWindow<'b, T> +where + Self: 'b, +{ + type IterType<'a> = KMerge> where Self: 'a; + type WindowType<'a> = LayeredTimeIndexWindow<'a, T> where Self: 'a; + type IndexType = T; + + fn active(&self, w: Range) -> bool { + self.timeindex.iter().any(|t| t.active(w.clone())) + } + + fn range<'a>(&'a self, w: Range) -> Self::WindowType<'a> { + let timeindex = self + .timeindex + .iter() + .map(|t| t.range(w.clone())) + .collect_vec(); + Self::WindowType { timeindex } + } + + fn first(&self) -> Option<&T> { + self.timeindex.iter().flat_map(|t| t.first()).min() + } + + fn last(&self) -> Option<&T> { + self.timeindex.iter().flat_map(|t| t.last()).max() + } + + fn iter_t(&self) -> Self::IterType<'_> { + self.timeindex.iter().map(|t| t.iter_t()).kmerge() + } +} diff --git a/raphtory/src/core/tadjset.rs b/raphtory/src/core/tadjset.rs deleted file mode 100644 index 52a4088d12..0000000000 --- a/raphtory/src/core/tadjset.rs +++ /dev/null @@ -1,224 +0,0 @@ -//! A data structure for efficiently storing and querying the temporal adjacency set of a node in a temporal graph. - -use crate::core::timeindex::TimeIndex; -use serde::{Deserialize, Serialize}; -use std::{collections::BTreeMap, hash::Hash, ops::Range}; - -const SMALL_SET: usize = 1024; - -/** - * Temporal adjacency set can track when adding edge v -> u - * does u exist already - * and if it does what is the edge metadata - * and if the edge is remote or local - * - * */ -#[derive(Debug, Default, Serialize, Deserialize, PartialEq)] -pub enum TAdjSet { - #[default] - Empty, - One(V, usize), - Small { - vs: Vec, // the neighbours - edges: Vec, // edge metadata - }, - Large { - vs: BTreeMap, // this is equiv to vs and edges - }, -} - -impl TAdjSet { - pub fn new(v: V, e: usize) -> Self { - Self::One(v, e) - } - - pub fn len(&self) -> usize { - match self { - TAdjSet::Empty => 0, - TAdjSet::One(_, _) => 1, - TAdjSet::Small { vs, .. } => vs.len(), - TAdjSet::Large { vs } => vs.len(), - } - } - - pub fn len_window(&self, timestamps: &[TimeIndex], window: &Range) -> usize { - match self { - TAdjSet::Empty => 0, - TAdjSet::One(_, e) => { - if timestamps[*e].active(window.clone()) { - 1 - } else { - 0 - } - } - - TAdjSet::Small { edges, .. } => edges - .iter() - .filter(|&&e| timestamps[e].active(window.clone())) - .count(), - TAdjSet::Large { vs } => vs - .values() - .filter(|&&e| timestamps[e].active(window.clone())) - .count(), - } - } - - pub fn push(&mut self, v: V, e: usize) { - match self { - TAdjSet::Empty => { - *self = Self::new(v, e); - } - TAdjSet::One(vv, ee) => { - if *vv < v { - *self = Self::Small { - vs: vec![*vv, v], - edges: vec![*ee, e], - } - } else if *vv > v { - *self = Self::Small { - vs: vec![v, *vv], - edges: vec![e, *ee], - } - } - } - TAdjSet::Small { vs, edges } => match vs.binary_search(&v) { - Ok(_) => {} - Err(i) => { - if vs.len() < SMALL_SET { - vs.insert(i, v); - edges.insert(i, e); - } else { - let mut map = - BTreeMap::from_iter(vs.iter().copied().zip(edges.iter().copied())); - map.insert(v, e); - *self = Self::Large { vs: map } - } - } - }, - TAdjSet::Large { vs } => { - vs.insert(v, e); - } - } - } - - pub fn iter(&self) -> Box + Send + '_> { - match self { - TAdjSet::Empty => Box::new(std::iter::empty()), - TAdjSet::One(v, e) => Box::new(std::iter::once((*v, *e))), - TAdjSet::Small { vs, edges } => Box::new(vs.iter().copied().zip(edges.iter().copied())), - TAdjSet::Large { vs } => Box::new(vs.iter().map(|(k, v)| (*k, *v))), - } - } - - pub fn vertices(&self) -> Box + Send + '_> { - match self { - TAdjSet::Empty => Box::new(std::iter::empty()), - TAdjSet::One(v, ..) => Box::new(std::iter::once(*v)), - TAdjSet::Small { vs, .. } => Box::new(vs.iter().copied()), - TAdjSet::Large { vs } => Box::new(vs.keys().copied()), - } - } - - pub fn iter_window<'a>( - &'a self, - timestamps: &'a [TimeIndex], - window: &Range, - ) -> Box + Send + 'a> { - let w = window.clone(); - Box::new( - self.iter() - .filter(move |(_, e)| timestamps[*e].active(w.clone())), - ) - } - - pub fn vertices_window<'a>( - &'a self, - timestamps: &'a [TimeIndex], - window: &Range, - ) -> Box + Send + 'a> { - let w = window.clone(); - Box::new( - self.iter() - .filter(move |(_, e)| timestamps[*e].active(w.clone())) - .map(|(v, _)| v), - ) - } - - pub fn find(&self, v: V) -> Option { - match self { - TAdjSet::Empty => None, - TAdjSet::One(vv, e) => (*vv == v).then_some(*e), - TAdjSet::Small { vs, edges } => vs.binary_search(&v).ok().map(|i| edges[i]), - TAdjSet::Large { vs } => vs.get(&v).copied(), - } - } -} - -#[cfg(test)] -mod tadjset_tests { - use super::*; - - #[quickcheck] - fn insert_fuzz(input: Vec) -> bool { - let mut ts: TAdjSet = TAdjSet::default(); - - for (e, i) in input.iter().enumerate() { - ts.push(*i, e); - } - - let res = input.iter().all(|i| ts.find(*i).is_some()); - if !res { - let ts_vec: Vec<(usize, usize)> = ts.iter().collect(); - println!("Input: {:?}", input); - println!("TAdjSet: {:?}", ts_vec); - } - res - } - - #[test] - fn insert() { - let mut ts: TAdjSet = TAdjSet::default(); - - ts.push(7, 5); - let actual = ts.iter().collect::>(); - let expected: Vec<(usize, usize)> = vec![(7, 5)]; - assert_eq!(actual, expected) - } - - #[test] - fn insert_large() { - let mut ts: TAdjSet = TAdjSet::default(); - - for i in 0..SMALL_SET + 2 { - ts.push(i, i); - } - - for i in 0..SMALL_SET + 2 { - assert_eq!(ts.find(i), Some(i)); - } - } - - #[test] - fn insert_twice() { - let mut ts: TAdjSet = TAdjSet::default(); - - ts.push(7, 9); - ts.push(7, 9); - - let actual = ts.iter().collect::>(); - let expected: Vec<(usize, usize)> = vec![(7, 9)]; - assert_eq!(actual, expected); - } - - #[test] - fn insert_two_different() { - let mut ts: TAdjSet = TAdjSet::default(); - - ts.push(1, 0); - ts.push(7, 1); - - let actual = ts.iter().collect::>(); - let expected: Vec<(usize, usize)> = vec![(1, 0), (7, 1)]; - assert_eq!(actual, expected); - } -} diff --git a/raphtory/src/core/tgraph.rs b/raphtory/src/core/tgraph.rs deleted file mode 100644 index c54d01bf3d..0000000000 --- a/raphtory/src/core/tgraph.rs +++ /dev/null @@ -1,2105 +0,0 @@ -//! A data structure for representing temporal graphs. - -use self::errors::MutateGraphError; -use crate::core::edge_layer::{EdgeLayer, VID}; -use crate::core::edge_ref::EdgeRef; -use crate::core::props::Props; -use crate::core::timeindex::TimeIndex; -use crate::core::tprop::TProp; -use crate::core::vertex::InputVertex; -use crate::core::vertex_ref::{LocalVertexRef, VertexRef}; -use crate::core::Direction; -use crate::core::{Prop, Time}; -use itertools::Itertools; -use rustc_hash::FxHashMap; -use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, ops::Range}; - -pub(crate) mod errors { - use crate::core::props::IllegalMutate; - - #[derive(thiserror::Error, Debug, PartialEq)] - pub enum MutateGraphError { - #[error("Create vertex '{vertex_id}' first before adding static properties to it")] - VertexNotFoundError { vertex_id: u64 }, - #[error("cannot change property for vertex '{vertex_id}'")] - IllegalVertexPropertyChange { - vertex_id: u64, - source: IllegalMutate, - }, - #[error("Create edge '{0}' -> '{1}' first before adding static properties to it")] - MissingEdge(u64, u64), // src, dst - #[error("cannot change property for edge '{src_id}' -> '{dst_id}'")] - IllegalEdgePropertyChange { - src_id: u64, - dst_id: u64, - source: IllegalMutate, - }, - #[error("cannot update property as is '{first_type}' and '{second_type}' given'")] - PropertyChangedType { - first_type: &'static str, - second_type: &'static str, - }, - } -} - -pub type MutateGraphResult = Result<(), MutateGraphError>; - -#[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct TemporalGraph { - id: usize, - // Maps global (logical) id to the local (physical) id which is an index to the adjacency list vector - pub(crate) logical_to_physical: FxHashMap, - - // global ids in insertion order for fast iterations, maps physical ids to logical ids - pub(crate) logical_ids: Vec, - - // Set of timestamps per vertex for fast window filtering - timestamps: Vec, - - // Properties abstraction for both vertices and edges - pub(crate) vertex_props: Props, - - // Edge layers - pub(crate) layers: Vec, - - //earliest time seen in this graph - pub(crate) earliest_time: i64, - - //latest time seen in this graph - pub(crate) latest_time: i64, -} - -impl TemporalGraph { - pub(crate) fn new(id: usize) -> Self { - Self { - id, - logical_to_physical: Default::default(), - logical_ids: Default::default(), - timestamps: Default::default(), - vertex_props: Default::default(), - layers: vec![EdgeLayer::new(0, id)], - earliest_time: i64::MAX, - latest_time: i64::MIN, - } - } -} - -// Internal helpers -impl TemporalGraph { - /// Checks if vertex ref is actually local and returns appropriate ID (either local pid or global id) - #[inline(always)] - fn local_id(&self, v: VertexRef) -> VID { - match v { - VertexRef::Local(LocalVertexRef { pid, .. }) => VID::Local(pid), - VertexRef::Remote(gid) => match self.logical_to_physical.get(&gid) { - Some(v_pid) => VID::Local(*v_pid), - None => VID::Remote(gid), - }, - } - } - - fn new_local_vertex(&self, pid: usize) -> LocalVertexRef { - LocalVertexRef { - shard_id: self.id, - pid, - } - } - - pub fn local_vertex(&self, v: VertexRef) -> Option { - match v { - VertexRef::Local(v) => { - (v.shard_id == self.id && v.pid < self.logical_ids.len()).then_some(v) - } - VertexRef::Remote(gid) => self.vertex(gid), - } - } - - pub fn local_vertex_window(&self, v: VertexRef, w: Range) -> Option { - self.local_vertex(v) - .filter(|v| self.timestamps[v.pid].active(w)) - } -} - -// Layer management: -impl TemporalGraph { - // TODO: we can completely replace this function with `layer_iter` if we are sure that doesn't - // affect performance - fn layer_iter(&self, id: Option) -> LayerIterator { - if self.layers.len() == 1 { - LayerIterator::Single(&self.layers[0]) - } else { - match id { - Some(id) => LayerIterator::Single(&self.layers[id]), - None => LayerIterator::Vector(&self.layers), - } - } - } -} - -enum LayerIterator<'a> { - Single(&'a EdgeLayer), - Vector(&'a Vec), -} - -impl TemporalGraph { - /// Global id of vertex - pub fn vertex_id(&self, v: LocalVertexRef) -> u64 { - self.logical_ids[v.pid] - } - - pub(crate) fn allocate_layer(&mut self, id: usize) { - self.layers.push(EdgeLayer::new(id, self.id)); - assert_eq!(self.layers.len(), id + 1) - } - - pub(crate) fn len(&self) -> usize { - self.logical_ids.len() - } - - pub(crate) fn len_window(&self, w: &Range) -> usize { - self.timestamps - .iter() - .filter(|&ts| ts.active(w.clone())) - .count() - } - - pub(crate) fn out_edges_len(&self, layer: Option) -> usize { - match self.layer_iter(layer) { - LayerIterator::Single(layer) => layer.out_edges_len(), - LayerIterator::Vector(_) => self - .vertices() - .map(|v| self.degree(v, Direction::OUT, None)) - .sum(), - } - } - - pub fn out_edges_len_window(&self, w: &Range

{ + pub(crate) fn new(props: P) -> Self { + Self { props } + } + pub fn keys(&self) -> Vec { + self.props.const_prop_keys().map(|v| v.clone()).collect() + } + + pub fn values(&self) -> Vec { + self.props.const_prop_values() + } + + pub fn iter(&self) -> Box + '_> { + Box::new(self.into_iter()) + } + + pub fn get(&self, key: &str) -> Option { + let id = self.props.get_const_prop_id(key)?; + self.props.get_const_prop(id) + } + + pub fn contains(&self, key: &str) -> bool { + self.get(key).is_some() + } + + pub fn as_map(&self) -> HashMap { + self.iter().collect() + } +} + +impl IntoIterator for ConstProperties

{ + type Item = (ArcStr, Prop); + type IntoIter = Zip, std::vec::IntoIter>; + + fn into_iter(self) -> Self::IntoIter { + let keys = self.keys(); + let vals = self.values(); + keys.into_iter().zip(vals) + } +} + +impl IntoIterator for &ConstProperties

{ + type Item = (ArcStr, Prop); + type IntoIter = Zip, std::vec::IntoIter>; + + fn into_iter(self) -> Self::IntoIter { + let keys = self.keys(); + let vals = self.values(); + keys.into_iter().zip(vals) + } +} + +impl PartialEq for ConstProperties

{ + fn eq(&self, other: &Self) -> bool { + self.as_map() == other.as_map() + } +} diff --git a/raphtory/src/db/api/properties/internal.rs b/raphtory/src/db/api/properties/internal.rs new file mode 100644 index 0000000000..b71e91982c --- /dev/null +++ b/raphtory/src/db/api/properties/internal.rs @@ -0,0 +1,157 @@ +use crate::{ + core::{ArcStr, Prop}, + db::api::view::internal::Base, +}; +use enum_dispatch::enum_dispatch; + +#[enum_dispatch] +pub trait TemporalPropertyViewOps { + fn temporal_value(&self, id: usize) -> Option { + self.temporal_values(id).last().cloned() + } + fn temporal_history(&self, id: usize) -> Vec; + fn temporal_values(&self, id: usize) -> Vec; + fn temporal_value_at(&self, id: usize, t: i64) -> Option { + let history = self.temporal_history(id); + match history.binary_search(&t) { + Ok(index) => Some(self.temporal_values(id)[index].clone()), + Err(index) => (index > 0).then(|| self.temporal_values(id)[index - 1].clone()), + } + } +} + +#[enum_dispatch] +pub trait ConstPropertiesOps { + /// Find id for property name (note this only checks the meta-data, not if the property actually exists for the entity) + fn get_const_prop_id(&self, name: &str) -> Option; + fn get_const_prop_name(&self, id: usize) -> ArcStr; + fn const_prop_ids(&self) -> Box + '_>; + fn const_prop_keys(&self) -> Box + '_> { + Box::new(self.const_prop_ids().map(|id| self.get_const_prop_name(id))) + } + fn const_prop_values(&self) -> Vec { + self.const_prop_ids() + .map(|k| { + self.get_const_prop(k) + .expect("ids that come from the internal iterator should exist") + }) + .collect() + } + fn get_const_prop(&self, id: usize) -> Option; +} + +#[enum_dispatch] +pub trait TemporalPropertiesOps { + fn get_temporal_prop_id(&self, name: &str) -> Option; + fn get_temporal_prop_name(&self, id: usize) -> ArcStr; + + fn temporal_prop_ids(&self) -> Box + '_>; + fn temporal_prop_keys(&self) -> Box + '_> { + Box::new( + self.temporal_prop_ids() + .map(|id| self.get_temporal_prop_name(id)), + ) + } +} + +pub trait PropertiesOps: + TemporalPropertiesOps + TemporalPropertyViewOps + ConstPropertiesOps +{ +} + +impl PropertiesOps for P {} + +pub trait InheritTemporalPropertyViewOps: Base {} +pub trait InheritTemporalPropertiesOps: Base {} +pub trait InheritStaticPropertiesOps: Base {} +pub trait InheritPropertiesOps: Base {} + +impl InheritStaticPropertiesOps for P {} +impl InheritTemporalPropertiesOps for P {} + +impl TemporalPropertyViewOps for P +where + P::Base: TemporalPropertyViewOps, +{ + #[inline] + fn temporal_value(&self, id: usize) -> Option { + self.base().temporal_value(id) + } + + #[inline] + fn temporal_history(&self, id: usize) -> Vec { + self.base().temporal_history(id) + } + + #[inline] + fn temporal_values(&self, id: usize) -> Vec { + self.base().temporal_values(id) + } + + #[inline] + fn temporal_value_at(&self, id: usize, t: i64) -> Option { + self.base().temporal_value_at(id, t) + } +} + +impl InheritTemporalPropertyViewOps for P {} + +impl TemporalPropertiesOps for P +where + P::Base: TemporalPropertiesOps, +{ + #[inline] + fn get_temporal_prop_id(&self, name: &str) -> Option { + self.base().get_temporal_prop_id(name) + } + + #[inline] + fn get_temporal_prop_name(&self, id: usize) -> ArcStr { + self.base().get_temporal_prop_name(id) + } + + #[inline] + fn temporal_prop_ids(&self) -> Box + '_> { + self.base().temporal_prop_ids() + } + + #[inline] + fn temporal_prop_keys(&self) -> Box + '_> { + self.base().temporal_prop_keys() + } +} + +impl ConstPropertiesOps for P +where + P::Base: ConstPropertiesOps, +{ + #[inline] + fn get_const_prop_id(&self, name: &str) -> Option { + self.base().get_const_prop_id(name) + } + + #[inline] + fn get_const_prop_name(&self, id: usize) -> ArcStr { + self.base().get_const_prop_name(id) + } + + #[inline] + fn const_prop_ids(&self) -> Box + '_> { + self.base().const_prop_ids() + } + + #[inline] + fn const_prop_keys(&self) -> Box + '_> { + self.base().const_prop_keys() + } + + #[inline] + fn const_prop_values(&self) -> Vec { + self.base().const_prop_values() + } + + #[inline] + fn get_const_prop(&self, id: usize) -> Option { + self.base().get_const_prop(id) + } +} diff --git a/raphtory/src/db/api/properties/mod.rs b/raphtory/src/db/api/properties/mod.rs new file mode 100644 index 0000000000..2ab4d0830f --- /dev/null +++ b/raphtory/src/db/api/properties/mod.rs @@ -0,0 +1,8 @@ +mod constant_props; +pub mod internal; +mod props; +mod temporal_props; + +pub use constant_props::*; +pub use props::*; +pub use temporal_props::*; diff --git a/raphtory/src/db/api/properties/props.rs b/raphtory/src/db/api/properties/props.rs new file mode 100644 index 0000000000..8368a2afbf --- /dev/null +++ b/raphtory/src/db/api/properties/props.rs @@ -0,0 +1,98 @@ +use crate::{ + core::{ArcStr, Prop}, + db::api::properties::{ + constant_props::ConstProperties, internal::*, temporal_props::TemporalProperties, + }, +}; +use std::collections::HashMap; + +/// View of the properties of an entity (graph|vertex|edge) +#[derive(Clone)] +pub struct Properties { + pub(crate) props: P, +} + +impl Properties

{ + pub fn new(props: P) -> Properties

{ + Self { props } + } + + /// Get property value. + /// + /// First searches temporal properties and returns latest value if it exists. + /// If not, it falls back to static properties. + pub fn get(&self, key: &str) -> Option { + self.props + .get_temporal_prop_id(key) + .and_then(|k| self.props.temporal_value(k)) + .or_else(|| { + self.props + .get_const_prop_id(key) + .and_then(|id| self.props.get_const_prop(id)) + }) + } + + /// Check if property `key` exists. + pub fn contains(&self, key: &str) -> bool { + self.get(key).is_some() + } + + /// Iterate over all property keys + pub fn keys(&self) -> impl Iterator + '_ { + self.props.temporal_prop_keys().chain( + self.props + .const_prop_keys() + .filter(|k| self.props.get_temporal_prop_id(k).is_none()), + ) + } + + /// Iterate over all property values + pub fn values(&self) -> impl Iterator + '_ { + self.keys().map(|k| self.get(&k).unwrap()) + } + + /// Iterate over all property key-value pairs + pub fn iter(&self) -> impl Iterator + '_ { + self.keys().zip(self.values()) + } + + /// Get a view of the temporal properties only. + pub fn temporal(&self) -> TemporalProperties

{ + TemporalProperties::new(self.props.clone()) + } + + /// Get a view of the constant properties (meta-data) only. + pub fn constant(&self) -> ConstProperties

{ + ConstProperties::new(self.props.clone()) + } + + /// Collect properties into vector + pub fn as_vec(&self) -> Vec<(ArcStr, Prop)> { + self.iter().map(|(k, v)| (k, v)).collect() + } + + /// Collect properties into map + pub fn as_map(&self) -> HashMap { + self.iter().map(|(k, v)| (k.clone(), v)).collect() + } +} + +impl IntoIterator for Properties

{ + type Item = (ArcStr, Prop); + type IntoIter = Box>; + + fn into_iter(self) -> Self::IntoIter { + let keys: Vec<_> = self.keys().map(|k| k.clone()).collect(); + let vals: Vec<_> = self.values().collect(); + Box::new(keys.into_iter().zip(vals)) + } +} + +impl<'a, P: PropertiesOps + Clone + 'a> IntoIterator for &'a Properties

{ + type Item = (ArcStr, Prop); + type IntoIter = Box + 'a>; + + fn into_iter(self) -> Self::IntoIter { + Box::new(self.iter()) + } +} diff --git a/raphtory/src/db/api/properties/temporal_props.rs b/raphtory/src/db/api/properties/temporal_props.rs new file mode 100644 index 0000000000..798630f5c8 --- /dev/null +++ b/raphtory/src/db/api/properties/temporal_props.rs @@ -0,0 +1,167 @@ +use crate::{ + core::{ArcStr, Prop, PropUnwrap}, + db::api::properties::internal::PropertiesOps, + prelude::Graph, +}; +use chrono::NaiveDateTime; +use std::{collections::HashMap, iter::Zip, sync::Arc}; + +pub struct TemporalPropertyView { + pub(crate) id: usize, + pub(crate) props: P, +} + +impl TemporalPropertyView

{ + pub(crate) fn new(props: P, key: usize) -> Self { + TemporalPropertyView { props, id: key } + } + pub fn history(&self) -> Vec { + self.props.temporal_history(self.id) + } + pub fn values(&self) -> Vec { + self.props.temporal_values(self.id) + } + pub fn iter(&self) -> impl Iterator { + self.into_iter() + } + pub fn at(&self, t: i64) -> Option { + self.props.temporal_value_at(self.id, t) + } + pub fn latest(&self) -> Option { + self.props.temporal_value(self.id) + } +} + +impl IntoIterator for TemporalPropertyView

{ + type Item = (i64, Prop); + type IntoIter = Zip, std::vec::IntoIter>; + + fn into_iter(self) -> Self::IntoIter { + let hist = self.history(); + let vals = self.values(); + hist.into_iter().zip(vals) + } +} + +impl IntoIterator for &TemporalPropertyView

{ + type Item = (i64, Prop); + type IntoIter = Zip, std::vec::IntoIter>; + + fn into_iter(self) -> Self::IntoIter { + let hist = self.history(); + let vals = self.values(); + hist.into_iter().zip(vals) + } +} + +pub struct TemporalProperties { + pub(crate) props: P, +} + +impl IntoIterator for TemporalProperties

{ + type Item = (ArcStr, TemporalPropertyView

); + type IntoIter = Zip, std::vec::IntoIter>>; + + fn into_iter(self) -> Self::IntoIter { + let keys: Vec<_> = self.keys().map(|k| k.clone()).collect(); + let values: Vec<_> = self.values().collect(); + keys.into_iter().zip(values) + } +} + +impl TemporalProperties

{ + pub(crate) fn new(props: P) -> Self { + Self { props } + } + pub fn keys(&self) -> impl Iterator + '_ { + self.props.temporal_prop_keys() + } + + pub fn contains(&self, key: &str) -> bool { + self.props.get_temporal_prop_id(key).is_some() + } + + pub fn values(&self) -> impl Iterator> + '_ { + self.props + .temporal_prop_ids() + .map(|k| TemporalPropertyView::new(self.props.clone(), k)) + } + + pub fn iter_latest(&self) -> impl Iterator + '_ { + self.iter().flat_map(|(k, v)| v.latest().map(|v| (k, v))) + } + + pub fn iter(&self) -> impl Iterator)> + '_ { + self.keys().zip(self.values()) + } + + pub fn get(&self, key: &str) -> Option> { + self.props + .get_temporal_prop_id(key) + .map(|k| TemporalPropertyView::new(self.props.clone(), k)) + } + + pub fn collect_properties(self) -> Vec<(ArcStr, Prop)> { + self.iter() + .flat_map(|(k, v)| v.latest().map(|v| (k.clone(), v))) + .collect() + } +} + +impl PropUnwrap for TemporalPropertyView

{ + fn into_u8(self) -> Option { + self.latest().into_u8() + } + + fn into_u16(self) -> Option { + self.latest().into_u16() + } + + fn into_str(self) -> Option { + self.latest().into_str() + } + + fn into_i32(self) -> Option { + self.latest().into_i32() + } + + fn into_i64(self) -> Option { + self.latest().into_i64() + } + + fn into_u32(self) -> Option { + self.latest().into_u32() + } + + fn into_u64(self) -> Option { + self.latest().into_u64() + } + + fn into_f32(self) -> Option { + self.latest().into_f32() + } + + fn into_f64(self) -> Option { + self.latest().into_f64() + } + + fn into_bool(self) -> Option { + self.latest().into_bool() + } + + fn into_list(self) -> Option>> { + self.latest().into_list() + } + + fn into_map(self) -> Option>> { + self.latest().into_map() + } + + fn into_dtime(self) -> Option { + self.latest().into_dtime() + } + + fn into_graph(self) -> Option { + self.latest().into_graph() + } +} diff --git a/raphtory/src/db/api/view/edge.rs b/raphtory/src/db/api/view/edge.rs new file mode 100644 index 0000000000..2336217720 --- /dev/null +++ b/raphtory/src/db/api/view/edge.rs @@ -0,0 +1,253 @@ +use crate::{ + core::{ + entities::{edges::edge_ref::EdgeRef, VID}, + storage::timeindex::{AsTime, TimeIndexEntry}, + ArcStr, + }, + db::api::{ + properties::{ + internal::{ConstPropertiesOps, TemporalPropertiesOps, TemporalPropertyViewOps}, + Properties, + }, + view::{internal::*, *}, + }, +}; + +pub trait EdgeViewInternalOps> { + fn graph(&self) -> G; + + fn eref(&self) -> EdgeRef; + + fn new_vertex(&self, v: VID) -> V; + + fn new_edge(&self, e: EdgeRef) -> Self; +} + +pub trait EdgeViewOps: + EdgeViewInternalOps + + ConstPropertiesOps + + TemporalPropertiesOps + + TemporalPropertyViewOps + + Sized + + Clone +{ + type Graph: GraphViewOps; + type Vertex: VertexViewOps; + type EList: EdgeListOps; + + /// list the activation timestamps for the edge + fn history(&self) -> Vec { + let layer_ids = self.graph().layer_ids().constrain_from_edge(self.eref()); + self.graph() + .edge_exploded(self.eref(), layer_ids) + .map(|e| *e.time().expect("exploded").t()) + .collect() + } + + /// Return a view of the properties of the edge + fn properties(&self) -> Properties { + Properties::new(self.clone()) + } + + /// Returns the source vertex of the edge. + fn src(&self) -> Self::Vertex { + let vertex = self.eref().src(); + self.new_vertex(vertex) + } + + /// Returns the destination vertex of the edge. + fn dst(&self) -> Self::Vertex { + let vertex = self.eref().dst(); + self.new_vertex(vertex) + } + + /// Check if edge is active at a given time point + fn active(&self, t: i64) -> bool { + let layer_ids = self.graph().layer_ids().constrain_from_edge(self.eref()); + match self.eref().time() { + Some(tt) => *tt.t() <= t && t <= self.latest_time().unwrap_or(*tt.t()), + None => self.graph().include_edge_window( + &self.graph().core_edge(self.eref().pid()), + t..t.saturating_add(1), + &layer_ids, + ), + } + } + + /// Returns the id of the edge. + fn id( + &self, + ) -> ( + ::ValueType, + ::ValueType, + ) { + (self.src().id(), self.dst().id()) + } + + /// Explodes an edge and returns all instances it had been updated as seperate edges + fn explode(&self) -> Self::EList; + + fn explode_layers(&self) -> Self::EList; + + /// Gets the first time an edge was seen + fn earliest_time(&self) -> Option { + let layer_ids = self.graph().layer_ids().constrain_from_edge(self.eref()); + self.graph().edge_earliest_time(self.eref(), layer_ids) + } + + /// Gets the latest time an edge was updated + fn latest_time(&self) -> Option { + let layer_ids = self.graph().layer_ids().constrain_from_edge(self.eref()); + self.graph().edge_latest_time(self.eref(), layer_ids) + } + + /// Gets the time stamp of the edge if it is exploded + fn time(&self) -> Option { + self.eref().time().map(|ti| *ti.t()) + } + + /// Gets the layer name for the edge if it is restricted to a single layer + fn layer_name(&self) -> Option { + self.eref() + .layer() + .map(|l_id| self.graph().get_layer_name(*l_id)) + } + + /// Gets the TimeIndexEntry if the edge is exploded + fn time_and_index(&self) -> Option { + self.eref().time() + } + + /// Gets the name of the layer this edge belongs to + fn layer_names(&self) -> BoxedIter { + let layer_ids = self + .graph() + .edge_layer_ids(&self.graph().core_edge(self.eref().pid())) + .constrain_from_edge(self.eref()); + self.graph().get_layer_names_from_ids(layer_ids) + } +} + +/// This trait defines the operations that can be +/// performed on a list of edges in a temporal graph view. +pub trait EdgeListOps: + IntoIterator, IntoIter = Self::IterType> + Sized +{ + type Graph: GraphViewOps; + type Vertex: VertexViewOps; + type Edge: EdgeViewOps; + type ValueType; + + /// the type of list of vertices + type VList: VertexListOps; + + /// the type of iterator + type IterType: Iterator>; + fn properties(self) -> Self::IterType>; + + /// gets the source vertices of the edges in the list + fn src(self) -> Self::VList; + + /// gets the destination vertices of the edges in the list + fn dst(self) -> Self::VList; + + fn id(self) -> Self::IterType<(u64, u64)>; + + /// returns a list of exploded edges that include an edge at each point in time + fn explode(self) -> Self::IterType; + + /// Get the timestamp for the earliest activity of the edge + fn earliest_time(self) -> Self::IterType>; + + /// Get the timestamp for the latest activity of the edge + fn latest_time(self) -> Self::IterType>; + + /// Get the timestamps of the edges if they are exploded + fn time(self) -> Self::IterType>; + + /// Get the layer name for each edge if it is restricted to a single layer + fn layer_name(self) -> Self::IterType>; +} + +#[cfg(test)] +mod test_edge_view { + use crate::prelude::*; + + #[test] + fn test_exploded_edge_properties() { + let g = Graph::new(); + let actual_prop_values = vec![0, 1, 2, 3]; + for v in actual_prop_values.iter() { + g.add_edge(0, 1, 2, [("test", *v)], None).unwrap(); + } + + let prop_values: Vec<_> = g + .edge(1, 2) + .unwrap() + .explode() + .flat_map(|e| e.properties().get("test").into_i32()) + .collect(); + assert_eq!(prop_values, actual_prop_values) + } + + #[test] + fn test_exploded_edge_multilayer() { + let g = Graph::new(); + let expected_prop_values = vec![0, 1, 2, 3]; + for v in expected_prop_values.iter() { + g.add_edge(0, 1, 2, [("test", *v)], Some((v % 2).to_string().as_str())) + .unwrap(); + } + + let prop_values: Vec<_> = g + .edge(1, 2) + .unwrap() + .explode() + .flat_map(|e| e.properties().get("test").into_i32()) + .collect(); + let actual_layers: Vec<_> = g + .edge(1, 2) + .unwrap() + .explode() + .map(|e| e.layer_names().into_iter().next().unwrap()) + .collect(); + let expected_layers: Vec<_> = expected_prop_values + .iter() + .map(|v| (v % 2).to_string()) + .collect(); + assert_eq!(prop_values, expected_prop_values); + assert_eq!(actual_layers, expected_layers); + } + + #[test] + fn test_sorting_by_secondary_index() { + let g = Graph::new(); + g.add_edge(0, 2, 3, NO_PROPS, None).unwrap(); + g.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); + g.add_edge(0, 1, 2, [("second", true)], None).unwrap(); + g.add_edge(0, 2, 3, [("second", true)], None).unwrap(); + + let mut exploded_edges: Vec<_> = g.edges().explode().collect(); + exploded_edges.sort_by_key(|a| a.time_and_index()); + + let res: Vec<_> = exploded_edges + .into_iter() + .map(|e| { + ( + e.src().id(), + e.dst().id(), + e.properties().get("second").into_bool(), + ) + }) + .collect(); + assert_eq!( + res, + vec![ + (2, 3, None), + (1, 2, None), + (1, 2, Some(true)), + (2, 3, Some(true)) + ] + ) + } +} diff --git a/raphtory/src/db/api/view/graph.rs b/raphtory/src/db/api/view/graph.rs new file mode 100644 index 0000000000..389bbe9019 --- /dev/null +++ b/raphtory/src/db/api/view/graph.rs @@ -0,0 +1,331 @@ +use crate::{ + core::{ + entities::{ + graph::tgraph::InnerTemporalGraph, vertices::vertex_ref::VertexRef, LayerIds, VID, + }, + utils::{errors::GraphError, time::IntoTime}, + ArcStr, + }, + db::{ + api::{ + mutation::{AdditionOps, PropertyAdditionOps}, + properties::Properties, + view::{internal::*, layer::LayerOps, *}, + }, + graph::{ + edge::EdgeView, + vertex::VertexView, + vertices::Vertices, + views::{ + layer_graph::LayeredGraph, vertex_subgraph::VertexSubgraph, + window_graph::WindowedGraph, + }, + }, + }, + prelude::{DeletionOps, NO_PROPS}, +}; +use rustc_hash::FxHashSet; + +/// This trait GraphViewOps defines operations for accessing +/// information about a graph. The trait has associated types +/// that are used to define the type of the vertices, edges +/// and the corresponding iterators. +pub trait GraphViewOps: BoxableGraphView + Clone + Sized { + fn subgraph, V: Into>( + &self, + vertices: I, + ) -> VertexSubgraph; + /// Return all the layer ids in the graph + fn unique_layers(&self) -> BoxedIter; + /// Timestamp of earliest activity in the graph + fn earliest_time(&self) -> Option; + /// Timestamp of latest activity in the graph + fn latest_time(&self) -> Option; + /// Return the number of vertices in the graph. + fn count_vertices(&self) -> usize; + + /// Check if the graph is empty. + fn is_empty(&self) -> bool { + self.count_vertices() == 0 + } + + /// Return the number of edges in the graph. + fn count_edges(&self) -> usize; + + // Return the number of temporal edges in the graph. + fn count_temporal_edges(&self) -> usize; + + /// Check if the graph contains a vertex `v`. + fn has_vertex>(&self, v: T) -> bool; + + /// Check if the graph contains an edge given a pair of vertices `(src, dst)`. + fn has_edge, L: Into>(&self, src: T, dst: T, layer: L) -> bool; + + /// Get a vertex `v`. + fn vertex>(&self, v: T) -> Option>; + + /// Return a View of the vertices in the Graph + fn vertices(&self) -> Vertices; + + /// Get an edge `(src, dst)`. + fn edge>(&self, src: T, dst: T) -> Option>; + + /// Return an iterator over all edges in the graph. + fn edges(&self) -> Box> + Send>; + + /// Get all property values of this graph. + /// + /// # Returns + /// + /// A view of the properties of the graph + fn properties(&self) -> Properties; + + /// Get a graph clone + /// + /// # Arguments + /// + /// # Returns + /// Graph - Returns clone of the graph + fn materialize(&self) -> Result; +} + +impl GraphViewOps for G { + fn subgraph, V: Into>( + &self, + vertices: I, + ) -> VertexSubgraph { + let filter = self.edge_filter(); + let layer_ids = self.layer_ids(); + let vertices: FxHashSet = vertices + .into_iter() + .flat_map(|v| self.internal_vertex_ref(v.into(), &layer_ids, filter)) + .collect(); + VertexSubgraph::new(self.clone(), vertices) + } + + /// Return all the layer ids in the graph + fn unique_layers(&self) -> BoxedIter { + self.get_layer_names_from_ids(self.layer_ids()) + } + + fn earliest_time(&self) -> Option { + self.earliest_time_global() + } + + fn latest_time(&self) -> Option { + self.latest_time_global() + } + + fn count_vertices(&self) -> usize { + self.vertices_len(self.layer_ids(), self.edge_filter()) + } + + fn count_temporal_edges(&self) -> usize { + self.edges().explode().count() + } + + #[inline] + fn count_edges(&self) -> usize { + self.edges_len(self.layer_ids(), self.edge_filter()) + } + + fn has_vertex>(&self, v: T) -> bool { + self.has_vertex_ref(v.into(), &self.layer_ids(), self.edge_filter()) + } + + fn has_edge, L: Into>(&self, src: T, dst: T, layer: L) -> bool { + let src_ref = src.into(); + let dst_ref = dst.into(); + let layers = self.layer_ids_from_names(layer.into()); + if let Some(src) = self.internalise_vertex(src_ref) { + if let Some(dst) = self.internalise_vertex(dst_ref) { + return self.has_edge_ref(src, dst, &layers, self.edge_filter()); + } + } + false + } + + fn vertex>(&self, v: T) -> Option> { + let v = v.into(); + self.internal_vertex_ref(v, &self.layer_ids(), self.edge_filter()) + .map(|v| VertexView::new_internal(self.clone(), v)) + } + + fn vertices(&self) -> Vertices { + let graph = self.clone(); + Vertices::new(graph) + } + + fn edge>(&self, src: T, dst: T) -> Option> { + let layer_ids = self.layer_ids(); + let edge_filter = self.edge_filter(); + if let Some(src) = self.internal_vertex_ref(src.into(), &layer_ids, edge_filter) { + if let Some(dst) = self.internal_vertex_ref(dst.into(), &layer_ids, edge_filter) { + return self + .edge_ref(src, dst, &layer_ids, edge_filter) + .map(|e| EdgeView::new(self.clone(), e)); + } + } + None + } + + fn edges(&self) -> Box> + Send> { + Box::new(self.vertices().iter().flat_map(|v| v.out_edges())) + } + + fn properties(&self) -> Properties { + Properties::new(self.clone()) + } + + fn materialize(&self) -> Result { + let g = InnerTemporalGraph::default(); + // Add edges first so we definitely have all associated vertices (important in case of persistent edges) + for e in self.edges() { + // FIXME: this needs to be verified + for ee in e.explode_layers() { + let layer_id = *ee.edge.layer().expect("exploded layers"); + let layer_ids = LayerIds::One(layer_id); + let layer_name = self.get_layer_name(layer_id); + let layer_name: Option<&str> = if layer_id == 0 { + None + } else { + Some(&layer_name) + }; + + for ee in ee.explode() { + g.add_edge( + ee.time().expect("exploded edge"), + ee.src().id(), + ee.dst().id(), + ee.properties().temporal().collect_properties(), + layer_name, + )?; + } + + if self.include_deletions() { + for t in self.edge_deletion_history(e.edge, layer_ids) { + g.delete_edge(t, e.src().id(), e.dst().id(), layer_name)?; + } + } + + g.edge(ee.src().id(), ee.dst().id()) + .expect("edge added") + .add_constant_properties(ee.properties().constant(), layer_name)?; + } + } + + for v in self.vertices().iter() { + for h in v.history() { + g.add_vertex(h, v.id(), NO_PROPS)?; + } + for (name, prop_view) in v.properties().temporal().iter() { + for (t, prop) in prop_view.iter() { + g.add_vertex(t, v.id(), [(name.clone(), prop)])?; + } + } + g.vertex(v.id()) + .expect("vertex added") + .add_constant_properties(v.properties().constant())?; + } + + g.add_constant_properties(self.properties().constant())?; + + Ok(self.new_base_graph(g)) + } +} + +impl TimeOps for G { + type WindowedViewType = WindowedGraph; + + fn start(&self) -> Option { + self.view_start() + } + + fn end(&self) -> Option { + self.view_end() + } + + fn window(&self, t_start: T, t_end: T) -> WindowedGraph { + WindowedGraph::new(self.clone(), t_start, t_end) + } +} + +impl LayerOps for G { + type LayeredViewType = LayeredGraph; + + fn default_layer(&self) -> Self::LayeredViewType { + LayeredGraph::new(self.clone(), 0.into()) + } + + fn layer>(&self, layers: L) -> Option { + let layers = layers.into(); + let ids = self.layer_ids_from_names(layers); + match ids { + LayerIds::None => None, + _ => Some(LayeredGraph::new(self.clone(), ids)), + } + } +} + +#[cfg(test)] +mod test_exploded_edges { + use crate::prelude::*; + + #[test] + fn test_exploded_edges() { + let g: Graph = Graph::new(); + g.add_edge(0, 0, 1, NO_PROPS, None).unwrap(); + g.add_edge(1, 0, 1, NO_PROPS, None).unwrap(); + g.add_edge(2, 0, 1, NO_PROPS, None).unwrap(); + g.add_edge(3, 0, 1, NO_PROPS, None).unwrap(); + + assert_eq!(g.count_temporal_edges(), 4) + } +} + +#[cfg(test)] +mod test_materialize { + use crate::prelude::*; + + #[test] + fn test_materialize() { + let g = Graph::new(); + g.add_edge(0, 1, 2, [("layer1", "1")], Some("1")).unwrap(); + g.add_edge(0, 1, 2, [("layer2", "2")], Some("2")).unwrap(); + + let gm = g.materialize().unwrap(); + assert!(!g + .layer("2") + .unwrap() + .edge(1, 2) + .unwrap() + .properties() + .temporal() + .contains("layer1")); + assert!(!gm + .into_events() + .unwrap() + .layer("2") + .unwrap() + .edge(1, 2) + .unwrap() + .properties() + .temporal() + .contains("layer1")); + } + + #[test] + fn changing_property_type_errors() { + let g = Graph::new(); + let props_0 = [("test", Prop::U64(1))]; + let props_1 = [("test", Prop::F64(0.1))]; + g.add_properties(0, props_0.clone()).unwrap(); + assert!(g.add_properties(1, props_1.clone()).is_err()); + + g.add_vertex(0, 1, props_0.clone()).unwrap(); + assert!(g.add_vertex(1, 1, props_1.clone()).is_err()); + + g.add_edge(0, 1, 2, props_0.clone(), None).unwrap(); + assert!(g.add_edge(1, 1, 2, props_1.clone(), None).is_err()); + } +} diff --git a/raphtory/src/db/api/view/internal/core_deletion_ops.rs b/raphtory/src/db/api/view/internal/core_deletion_ops.rs new file mode 100644 index 0000000000..10abe08c7e --- /dev/null +++ b/raphtory/src/db/api/view/internal/core_deletion_ops.rs @@ -0,0 +1,48 @@ +use crate::{ + core::{ + entities::{edges::edge_ref::EdgeRef, LayerIds}, + storage::timeindex::{LockedLayeredIndex, TimeIndexEntry}, + }, + db::api::view::internal::Base, +}; +use enum_dispatch::enum_dispatch; + +#[enum_dispatch] +pub trait CoreDeletionOps { + /// Get all the deletion timestamps for an edge + /// (this should always be global and not affected by windowing as deletion semantics may need information outside the current view!) + fn edge_deletions( + &self, + eref: EdgeRef, + layer_ids: LayerIds, + ) -> LockedLayeredIndex<'_, TimeIndexEntry>; +} + +pub trait InheritCoreDeletionOps: Base {} + +impl DelegateCoreDeletionOps for G +where + G::Base: CoreDeletionOps, +{ + type Internal = G::Base; + + fn graph(&self) -> &Self::Internal { + self.base() + } +} + +pub trait DelegateCoreDeletionOps { + type Internal: CoreDeletionOps + ?Sized; + + fn graph(&self) -> &Self::Internal; +} + +impl CoreDeletionOps for G { + fn edge_deletions( + &self, + eref: EdgeRef, + layer_ids: LayerIds, + ) -> LockedLayeredIndex<'_, TimeIndexEntry> { + self.graph().edge_deletions(eref, layer_ids) + } +} diff --git a/raphtory/src/db/api/view/internal/core_ops.rs b/raphtory/src/db/api/view/internal/core_ops.rs new file mode 100644 index 0000000000..94d53be15c --- /dev/null +++ b/raphtory/src/db/api/view/internal/core_ops.rs @@ -0,0 +1,376 @@ +use crate::{ + core::{ + entities::{ + edges::{edge_ref::EdgeRef, edge_store::EdgeStore}, + properties::{ + graph_props::GraphProps, + props::Meta, + tprop::{LockedLayeredTProp, TProp}, + }, + vertices::{vertex_ref::VertexRef, vertex_store::VertexStore}, + LayerIds, EID, VID, + }, + storage::{ + locked_view::LockedView, + timeindex::{LockedLayeredIndex, TimeIndex, TimeIndexEntry}, + ArcEntry, + }, + ArcStr, Prop, + }, + db::api::view::{internal::Base, BoxedIter}, +}; +use enum_dispatch::enum_dispatch; + +/// Core functions that should (almost-)always be implemented by pointing at the underlying graph. +#[enum_dispatch] +pub trait CoreGraphOps { + /// get the number of vertices in the main graph + fn unfiltered_num_vertices(&self) -> usize; + + fn vertex_meta(&self) -> &Meta; + + fn edge_meta(&self) -> &Meta; + + fn graph_meta(&self) -> &GraphProps; + + fn get_layer_name(&self, layer_id: usize) -> ArcStr; + + fn get_layer_id(&self, name: &str) -> Option; + + /// Get the layer name for a given id + fn get_layer_names_from_ids(&self, layer_ids: LayerIds) -> BoxedIter; + + /// Returns the external ID for a vertex + fn vertex_id(&self, v: VID) -> u64; + + /// Returns the string name for a vertex + fn vertex_name(&self, v: VID) -> String; + + /// Get all the addition timestamps for an edge + /// (this should always be global and not affected by windowing as deletion semantics may need information outside the current view!) + fn edge_additions( + &self, + eref: EdgeRef, + layer_ids: LayerIds, + ) -> LockedLayeredIndex<'_, TimeIndexEntry>; + + /// Get all the addition timestamps for a vertex + /// (this should always be global and not affected by windowing as deletion semantics may need information outside the current view!) + fn vertex_additions(&self, v: VID) -> LockedView>; + + /// Gets the internal reference for an external vertex reference and keeps internal references unchanged. + fn internalise_vertex(&self, v: VertexRef) -> Option; + + /// Gets the internal reference for an external vertex reference and keeps internal references unchanged. Assumes vertex exists! + fn internalise_vertex_unchecked(&self, v: VertexRef) -> VID; + + /// Gets a static graph property. + /// + /// # Arguments + /// + /// * `name` - The name of the property. + /// + /// # Returns + /// + /// Option - The property value if it exists. + fn constant_prop(&self, id: usize) -> Option; + + /// Gets a temporal graph property. + /// + /// # Arguments + /// + /// * `name` - The name of the property. + /// + /// # Returns + /// + /// Option> - The history of property values if it exists. + fn temporal_prop(&self, id: usize) -> Option>; + + /// Gets a static property of a given vertex given the name and vertex reference. + /// + /// # Arguments + /// + /// * `v` - A reference to the vertex for which the property is being queried. + /// * `name` - The name of the property. + /// + /// # Returns + /// + /// Option - The property value if it exists. + fn constant_vertex_prop(&self, v: VID, id: usize) -> Option; + + /// Gets the keys of constant properties of a given vertex + /// + /// # Arguments + /// + /// * `v` - A reference to the vertex for which the property is being queried. + /// + /// # Returns + /// + /// The keys of the constant properties. + fn constant_vertex_prop_ids(&self, v: VID) -> Box + '_>; + + /// Gets a temporal property of a given vertex given the name and vertex reference. + /// + /// # Arguments + /// + /// * `v` - A reference to the vertex for which the property is being queried. + /// * `name` - The name of the property. + /// + /// # Returns + /// + /// Option> - The history of property values if it exists. + fn temporal_vertex_prop(&self, v: VID, id: usize) -> Option>; + + /// Returns a vector of all ids of temporal properties within the given vertex + /// + /// # Arguments + /// + /// * `v` - A reference to the vertex for which to retrieve the names. + /// + /// # Returns + /// + /// the ids of the temporal properties + fn temporal_vertex_prop_ids(&self, v: VID) -> Box + '_>; + + /// Returns the static edge property with the given name for the + /// given edge reference. + /// + /// # Arguments + /// + /// * `e` - An `EdgeRef` reference to the edge of interest. + /// * `name` - A `String` containing the name of the temporal property. + /// + /// # Returns + /// + /// A property if it exists + fn get_const_edge_prop(&self, e: EdgeRef, id: usize, layer_ids: LayerIds) -> Option; + + /// Returns a vector of keys for the static properties of the given edge reference. + /// + /// # Arguments + /// + /// * `e` - An `EdgeRef` reference to the edge of interest. + /// + /// # Returns + /// + /// the keys for the constant properties of the given edge. + fn const_edge_prop_ids( + &self, + e: EdgeRef, + layer_ids: LayerIds, + ) -> Box + '_>; + + /// Returns a vector of all temporal values of the edge property with the given name for the + /// given edge reference. + /// + /// # Arguments + /// + /// * `e` - An `EdgeRef` reference to the edge of interest. + /// * `name` - A `String` containing the name of the temporal property. + /// + /// # Returns + /// + /// A property if it exists + fn temporal_edge_prop( + &self, + e: EdgeRef, + id: usize, + layer_ids: LayerIds, + ) -> Option; + + /// Returns a vector of keys for the temporal properties of the given edge reference. + /// + /// # Arguments + /// + /// * `e` - An `EdgeRef` reference to the edge of interest. + /// + /// # Returns + /// + /// * keys for the temporal properties of the given edge. + fn temporal_edge_prop_ids( + &self, + e: EdgeRef, + layer_ids: LayerIds, + ) -> Box + '_>; + + fn core_edges(&self) -> Box>>; + + fn core_edge(&self, eid: EID) -> ArcEntry; + fn core_vertices(&self) -> Box>>; + + fn core_vertex(&self, vid: VID) -> ArcEntry; +} + +pub trait InheritCoreOps: Base {} + +impl DelegateCoreOps for G +where + G::Base: CoreGraphOps, +{ + type Internal = G::Base; + + #[inline] + fn graph(&self) -> &Self::Internal { + self.base() + } +} + +pub trait DelegateCoreOps { + type Internal: CoreGraphOps + ?Sized; + + fn graph(&self) -> &Self::Internal; +} + +impl CoreGraphOps for G { + #[inline] + fn unfiltered_num_vertices(&self) -> usize { + self.graph().unfiltered_num_vertices() + } + + #[inline] + fn vertex_meta(&self) -> &Meta { + self.graph().vertex_meta() + } + + #[inline] + fn edge_meta(&self) -> &Meta { + self.graph().edge_meta() + } + + #[inline] + fn graph_meta(&self) -> &GraphProps { + self.graph().graph_meta() + } + + #[inline] + fn get_layer_name(&self, layer_id: usize) -> ArcStr { + self.graph().get_layer_name(layer_id) + } + + #[inline] + fn get_layer_id(&self, name: &str) -> Option { + self.graph().get_layer_id(name) + } + + #[inline] + fn get_layer_names_from_ids(&self, layer_ids: LayerIds) -> BoxedIter { + self.graph().get_layer_names_from_ids(layer_ids) + } + + #[inline] + fn vertex_id(&self, v: VID) -> u64 { + self.graph().vertex_id(v) + } + + #[inline] + fn vertex_name(&self, v: VID) -> String { + self.graph().vertex_name(v) + } + + #[inline] + fn edge_additions( + &self, + eref: EdgeRef, + layer_ids: LayerIds, + ) -> LockedLayeredIndex<'_, TimeIndexEntry> { + self.graph().edge_additions(eref, layer_ids) + } + + #[inline] + fn vertex_additions(&self, v: VID) -> LockedView> { + self.graph().vertex_additions(v) + } + + #[inline] + fn internalise_vertex(&self, v: VertexRef) -> Option { + self.graph().internalise_vertex(v) + } + + #[inline] + fn internalise_vertex_unchecked(&self, v: VertexRef) -> VID { + self.graph().internalise_vertex_unchecked(v) + } + + #[inline] + fn constant_prop(&self, id: usize) -> Option { + self.graph().constant_prop(id) + } + + #[inline] + fn temporal_prop(&self, id: usize) -> Option> { + self.graph().temporal_prop(id) + } + + #[inline] + fn constant_vertex_prop(&self, v: VID, id: usize) -> Option { + self.graph().constant_vertex_prop(v, id) + } + + #[inline] + fn constant_vertex_prop_ids(&self, v: VID) -> Box + '_> { + self.graph().constant_vertex_prop_ids(v) + } + + #[inline] + fn temporal_vertex_prop(&self, v: VID, id: usize) -> Option> { + self.graph().temporal_vertex_prop(v, id) + } + + #[inline] + fn temporal_vertex_prop_ids(&self, v: VID) -> Box + '_> { + self.graph().temporal_vertex_prop_ids(v) + } + + #[inline] + fn get_const_edge_prop(&self, e: EdgeRef, id: usize, layer_ids: LayerIds) -> Option { + self.graph().get_const_edge_prop(e, id, layer_ids) + } + + #[inline] + fn const_edge_prop_ids( + &self, + e: EdgeRef, + layer_ids: LayerIds, + ) -> Box + '_> { + self.graph().const_edge_prop_ids(e, layer_ids) + } + + #[inline] + fn temporal_edge_prop( + &self, + e: EdgeRef, + id: usize, + layer_ids: LayerIds, + ) -> Option { + self.graph().temporal_edge_prop(e, id, layer_ids) + } + + #[inline] + fn temporal_edge_prop_ids( + &self, + e: EdgeRef, + layer_ids: LayerIds, + ) -> Box + '_> { + self.graph().temporal_edge_prop_ids(e, layer_ids) + } + + #[inline] + fn core_edges(&self) -> Box>> { + self.graph().core_edges() + } + + #[inline] + fn core_edge(&self, eid: EID) -> ArcEntry { + self.graph().core_edge(eid) + } + + #[inline] + fn core_vertices(&self) -> Box>> { + self.graph().core_vertices() + } + + #[inline] + fn core_vertex(&self, vid: VID) -> ArcEntry { + self.graph().core_vertex(vid) + } +} diff --git a/raphtory/src/db/api/view/internal/edge_filter_ops.rs b/raphtory/src/db/api/view/internal/edge_filter_ops.rs new file mode 100644 index 0000000000..4b16688012 --- /dev/null +++ b/raphtory/src/db/api/view/internal/edge_filter_ops.rs @@ -0,0 +1,57 @@ +use crate::{ + core::entities::{edges::edge_store::EdgeStore, LayerIds}, + db::api::view::internal::Base, +}; +use enum_dispatch::enum_dispatch; +use std::sync::Arc; + +pub fn extend_filter( + old: Option, + filter: impl Fn(&EdgeStore, &LayerIds) -> bool + Send + Sync + 'static, +) -> EdgeFilter { + match old { + Some(f) => Arc::new(move |e, l| f(e, l) && filter(e, l)), + None => Arc::new(filter), + } +} + +pub type EdgeFilter = Arc bool + Send + Sync>; + +#[enum_dispatch] +pub trait EdgeFilterOps { + /// Return the optional edge filter for the graph + fn edge_filter(&self) -> Option<&EdgeFilter>; + + /// Called by the windowed graph to get the edge filter (override if it should include more/different edges than a non-windowed graph) + #[inline] + fn edge_filter_window(&self) -> Option<&EdgeFilter> { + self.edge_filter() + } +} + +pub trait InheritEdgeFilterOps: Base {} + +impl DelegateEdgeFilterOps for G +where + G::Base: EdgeFilterOps, +{ + type Internal = G::Base; + + #[inline] + fn graph(&self) -> &Self::Internal { + self.base() + } +} + +pub trait DelegateEdgeFilterOps { + type Internal: EdgeFilterOps + ?Sized; + + fn graph(&self) -> &Self::Internal; +} + +impl EdgeFilterOps for G { + #[inline] + fn edge_filter(&self) -> Option<&EdgeFilter> { + self.graph().edge_filter() + } +} diff --git a/raphtory/src/db/api/view/internal/graph_ops.rs b/raphtory/src/db/api/view/internal/graph_ops.rs new file mode 100644 index 0000000000..8ed0751626 --- /dev/null +++ b/raphtory/src/db/api/view/internal/graph_ops.rs @@ -0,0 +1,286 @@ +use crate::{ + core::{ + entities::{edges::edge_ref::EdgeRef, vertices::vertex_ref::VertexRef, LayerIds, EID, VID}, + Direction, + }, + db::api::view::internal::{Base, EdgeFilter}, +}; +use enum_dispatch::enum_dispatch; + +/// The GraphViewInternalOps trait provides a set of methods to query a directed graph +/// represented by the raphtory_core::tgraph::TGraph struct. +#[enum_dispatch] +pub trait GraphOps: Send + Sync { + /// Check if a vertex exists and returns internal reference. + fn internal_vertex_ref( + &self, + v: VertexRef, + layer_ids: &LayerIds, + filter: Option<&EdgeFilter>, + ) -> Option; + + fn find_edge_id( + &self, + e_id: EID, + layer_ids: &LayerIds, + filter: Option<&EdgeFilter>, + ) -> Option; + + /// Returns the total number of vertices in the graph. + fn vertices_len(&self, layer_ids: LayerIds, filter: Option<&EdgeFilter>) -> usize; + + /// Returns the total number of edges in the graph. + fn edges_len(&self, layers: LayerIds, filter: Option<&EdgeFilter>) -> usize; + + /// Returns true if the graph contains an edge between the source vertex + /// (src) and the destination vertex (dst). + /// # Arguments + /// + /// * `src` - The source vertex of the edge. + /// * `dst` - The destination vertex of the edge. + fn has_edge_ref( + &self, + src: VID, + dst: VID, + layers: &LayerIds, + filter: Option<&EdgeFilter>, + ) -> bool { + self.edge_ref(src, dst, layers, filter).is_some() + } + + /// Returns true if the graph contains the specified vertex (v). + /// # Arguments + /// + /// * `v` - VertexRef of the vertex to check. + fn has_vertex_ref(&self, v: VertexRef, layers: &LayerIds, filter: Option<&EdgeFilter>) -> bool { + self.internal_vertex_ref(v, layers, filter).is_some() + } + + /// Returns the number of edges that point towards or from the specified vertex + /// (v) based on the direction (d). + /// # Arguments + /// + /// * `v` - VID of the vertex to check. + /// * `d` - Direction of the edges to count. + fn degree(&self, v: VID, d: Direction, layers: &LayerIds, filter: Option<&EdgeFilter>) + -> usize; + + /// Returns the VID that corresponds to the specified vertex ID (v). + /// Returns None if the vertex ID is not present in the graph. + /// # Arguments + /// + /// * `v` - The vertex ID to lookup. + fn vertex_ref(&self, v: u64, layers: &LayerIds, filter: Option<&EdgeFilter>) -> Option { + self.internal_vertex_ref(v.into(), layers, filter) + } + + /// Returns all the vertex references in the graph. + /// # Returns + /// * `Box + Send>` - An iterator over all the vertex + /// references in the graph. + fn vertex_refs( + &self, + layers: LayerIds, + filter: Option<&EdgeFilter>, + ) -> Box + Send>; + + /// Returns the edge reference that corresponds to the specified src and dst vertex + /// # Arguments + /// + /// * `src` - The source vertex. + /// * `dst` - The destination vertex. + /// + /// # Returns + /// + /// * `Option` - The edge reference if it exists. + fn edge_ref( + &self, + src: VID, + dst: VID, + layer: &LayerIds, + filter: Option<&EdgeFilter>, + ) -> Option; + + /// Returns all the edge references in the graph. + /// + /// # Returns + /// + /// * `Box + Send>` - An iterator over all the edge references. + fn edge_refs( + &self, + layers: LayerIds, + filter: Option<&EdgeFilter>, + ) -> Box + Send>; + + /// Returns an iterator over the edges connected to a given vertex in a given direction. + /// + /// # Arguments + /// + /// * `v` - A reference to the vertex for which the edges are being queried. + /// * `d` - The direction in which to search for edges. + /// * `layer` - The optional layer to consider + /// + /// # Returns + /// + /// Box + Send> - A boxed iterator that yields references to + /// the edges connected to the vertex. + fn vertex_edges( + &self, + v: VID, + d: Direction, + layer: LayerIds, + filter: Option<&EdgeFilter>, + ) -> Box + Send>; + + /// Returns an iterator over the neighbors of a given vertex in a given direction. + /// + /// # Arguments + /// + /// * `v` - A reference to the vertex for which the neighbors are being queried. + /// * `d` - The direction in which to search for neighbors. + /// + /// # Returns + /// + /// A boxed iterator that yields references to the neighboring vertices. + fn neighbours( + &self, + v: VID, + d: Direction, + layers: LayerIds, + filter: Option<&EdgeFilter>, + ) -> Box + Send>; +} + +pub trait InheritGraphOps: Base {} + +impl DelegateGraphOps for G +where + G::Base: GraphOps, +{ + type Internal = G::Base; + + fn graph(&self) -> &Self::Internal { + self.base() + } +} + +pub trait DelegateGraphOps { + type Internal: GraphOps + ?Sized; + + fn graph(&self) -> &Self::Internal; +} + +impl GraphOps for G { + #[inline] + fn internal_vertex_ref( + &self, + v: VertexRef, + layer_ids: &LayerIds, + filter: Option<&EdgeFilter>, + ) -> Option { + self.graph().internal_vertex_ref(v, layer_ids, filter) + } + + #[inline] + fn find_edge_id( + &self, + e_id: EID, + layer_ids: &LayerIds, + filter: Option<&EdgeFilter>, + ) -> Option { + self.graph().find_edge_id(e_id, layer_ids, filter) + } + + #[inline] + fn vertices_len(&self, layer_ids: LayerIds, filter: Option<&EdgeFilter>) -> usize { + self.graph().vertices_len(layer_ids, filter) + } + + #[inline] + fn edges_len(&self, layers: LayerIds, filter: Option<&EdgeFilter>) -> usize { + self.graph().edges_len(layers, filter) + } + + #[inline] + fn has_edge_ref( + &self, + src: VID, + dst: VID, + layers: &LayerIds, + filter: Option<&EdgeFilter>, + ) -> bool { + self.graph().has_edge_ref(src, dst, layers, filter) + } + + #[inline] + fn has_vertex_ref(&self, v: VertexRef, layers: &LayerIds, filter: Option<&EdgeFilter>) -> bool { + self.graph().has_vertex_ref(v, layers, filter) + } + + #[inline] + fn degree( + &self, + v: VID, + d: Direction, + layers: &LayerIds, + filter: Option<&EdgeFilter>, + ) -> usize { + self.graph().degree(v, d, layers, filter) + } + + #[inline] + fn vertex_ref(&self, v: u64, layers: &LayerIds, filter: Option<&EdgeFilter>) -> Option { + self.graph().vertex_ref(v, layers, filter) + } + + #[inline] + fn vertex_refs( + &self, + layers: LayerIds, + filter: Option<&EdgeFilter>, + ) -> Box + Send> { + self.graph().vertex_refs(layers, filter) + } + + #[inline] + fn edge_ref( + &self, + src: VID, + dst: VID, + layer: &LayerIds, + filter: Option<&EdgeFilter>, + ) -> Option { + self.graph().edge_ref(src, dst, layer, filter) + } + + #[inline] + fn edge_refs( + &self, + layers: LayerIds, + filter: Option<&EdgeFilter>, + ) -> Box + Send> { + self.graph().edge_refs(layers, filter) + } + + #[inline] + fn vertex_edges( + &self, + v: VID, + d: Direction, + layer: LayerIds, + filter: Option<&EdgeFilter>, + ) -> Box + Send> { + self.graph().vertex_edges(v, d, layer, filter) + } + + #[inline] + fn neighbours( + &self, + v: VID, + d: Direction, + layers: LayerIds, + filter: Option<&EdgeFilter>, + ) -> Box + Send> { + self.graph().neighbours(v, d, layers, filter) + } +} diff --git a/raphtory/src/db/api/view/internal/inherit.rs b/raphtory/src/db/api/view/internal/inherit.rs new file mode 100644 index 0000000000..c758814122 --- /dev/null +++ b/raphtory/src/db/api/view/internal/inherit.rs @@ -0,0 +1,18 @@ +use std::ops::Deref; + +/// Get a base for inheriting methods +pub trait Base { + type Base: ?Sized; + + fn base(&self) -> &Self::Base; +} + +/// Deref implies Base +impl Base for T { + type Base = T::Target; + + #[inline(always)] + fn base(&self) -> &Self::Base { + self.deref() + } +} diff --git a/raphtory/src/db/api/view/internal/into_dynamic.rs b/raphtory/src/db/api/view/internal/into_dynamic.rs new file mode 100644 index 0000000000..75b6308ed5 --- /dev/null +++ b/raphtory/src/db/api/view/internal/into_dynamic.rs @@ -0,0 +1,36 @@ +use crate::db::{ + api::view::{internal::DynamicGraph, GraphViewOps}, + graph::views::{ + layer_graph::LayeredGraph, vertex_subgraph::VertexSubgraph, window_graph::WindowedGraph, + }, +}; +use enum_dispatch::enum_dispatch; + +#[enum_dispatch] +pub trait IntoDynamic { + fn into_dynamic(self) -> DynamicGraph; +} + +impl IntoDynamic for WindowedGraph { + fn into_dynamic(self) -> DynamicGraph { + DynamicGraph::new(self) + } +} + +impl IntoDynamic for LayeredGraph { + fn into_dynamic(self) -> DynamicGraph { + DynamicGraph::new(self) + } +} + +impl IntoDynamic for DynamicGraph { + fn into_dynamic(self) -> DynamicGraph { + self + } +} + +impl IntoDynamic for VertexSubgraph { + fn into_dynamic(self) -> DynamicGraph { + DynamicGraph::new(self) + } +} diff --git a/raphtory/src/db/api/view/internal/layer_ops.rs b/raphtory/src/db/api/view/internal/layer_ops.rs new file mode 100644 index 0000000000..fe38b8e85e --- /dev/null +++ b/raphtory/src/db/api/view/internal/layer_ops.rs @@ -0,0 +1,55 @@ +use crate::{ + core::entities::{edges::edge_store::EdgeStore, LayerIds}, + db::api::view::internal::Base, + prelude::Layer, +}; +use enum_dispatch::enum_dispatch; + +#[enum_dispatch] +pub trait InternalLayerOps { + /// get the layer ids for the graph view + fn layer_ids(&self) -> LayerIds; + + /// Get the layer id for the given layer name + fn layer_ids_from_names(&self, key: Layer) -> LayerIds; + + /// get the layer ids for the given edge id + fn edge_layer_ids(&self, e: &EdgeStore) -> LayerIds; +} + +pub trait InheritLayerOps: Base {} + +impl DelegateLayerOps for G +where + G::Base: InternalLayerOps, +{ + type Internal = G::Base; + + #[inline] + fn graph(&self) -> &Self::Internal { + self.base() + } +} + +pub trait DelegateLayerOps { + type Internal: InternalLayerOps + ?Sized; + + fn graph(&self) -> &Self::Internal; +} + +impl InternalLayerOps for G { + #[inline] + fn layer_ids(&self) -> LayerIds { + self.graph().layer_ids() + } + + #[inline] + fn layer_ids_from_names(&self, key: Layer) -> LayerIds { + self.graph().layer_ids_from_names(key) + } + + #[inline] + fn edge_layer_ids(&self, e: &EdgeStore) -> LayerIds { + self.graph().edge_layer_ids(e) + } +} diff --git a/raphtory/src/db/api/view/internal/materialize.rs b/raphtory/src/db/api/view/internal/materialize.rs new file mode 100644 index 0000000000..331a797541 --- /dev/null +++ b/raphtory/src/db/api/view/internal/materialize.rs @@ -0,0 +1,172 @@ +use crate::{ + core::{ + entities::{ + edges::{edge_ref::EdgeRef, edge_store::EdgeStore}, + properties::{ + graph_props::GraphProps, + props::Meta, + tprop::{LockedLayeredTProp, TProp}, + }, + vertices::{vertex_ref::VertexRef, vertex_store::VertexStore}, + LayerIds, EID, VID, + }, + storage::{ + locked_view::LockedView, + timeindex::{LockedLayeredIndex, TimeIndex, TimeIndexEntry}, + ArcEntry, + }, + utils::errors::GraphError, + ArcStr, Direction, PropType, + }, + db::{ + api::{ + mutation::internal::{InternalAdditionOps, InternalPropertyAdditionOps}, + properties::internal::{ + ConstPropertiesOps, TemporalPropertiesOps, TemporalPropertyViewOps, + }, + view::{internal::*, BoxedIter}, + }, + graph::{ + graph::{Graph, InternalGraph}, + views::deletion_graph::GraphWithDeletions, + }, + }, + prelude::{Layer, Prop}, +}; +use enum_dispatch::enum_dispatch; +use serde::{Deserialize, Serialize}; +use std::path::Path; + +#[enum_dispatch(CoreGraphOps)] +#[enum_dispatch(GraphOps)] +#[enum_dispatch(EdgeFilterOps)] +#[enum_dispatch(InternalLayerOps)] +#[enum_dispatch(IntoDynamic)] +#[enum_dispatch(TimeSemantics)] +#[enum_dispatch(InternalMaterialize)] +#[enum_dispatch(TemporalPropertiesOps)] +#[enum_dispatch(TemporalPropertyViewOps)] +#[enum_dispatch(ConstPropertiesOps)] +#[enum_dispatch(InternalAdditionOps)] +#[enum_dispatch(InternalPropertyAdditionOps)] +#[derive(Serialize, Deserialize, Clone)] +pub enum MaterializedGraph { + EventGraph(Graph), + PersistentGraph(GraphWithDeletions), +} + +impl MaterializedGraph { + pub fn into_events(self) -> Option { + match self { + MaterializedGraph::EventGraph(g) => Some(g), + MaterializedGraph::PersistentGraph(_) => None, + } + } + pub fn into_persistent(self) -> Option { + match self { + MaterializedGraph::EventGraph(_) => None, + MaterializedGraph::PersistentGraph(g) => Some(g), + } + } + + pub fn load_from_file>(path: P) -> Result { + let f = std::fs::File::open(path)?; + let mut reader = std::io::BufReader::new(f); + Ok(bincode::deserialize_from(&mut reader)?) + } + + pub fn save_to_file>(&self, path: P) -> Result<(), GraphError> { + let f = std::fs::File::create(path)?; + let mut writer = std::io::BufWriter::new(f); + Ok(bincode::serialize_into(&mut writer, self)?) + } + + pub fn bincode(&self) -> Result, GraphError> { + let encoded = bincode::serialize(self)?; + Ok(encoded) + } + + pub fn from_bincode(b: &[u8]) -> Result { + let g = bincode::deserialize(b)?; + Ok(g) + } +} + +#[enum_dispatch] +pub trait InternalMaterialize { + fn new_base_graph(&self, graph: InternalGraph) -> MaterializedGraph; + + fn include_deletions(&self) -> bool; +} + +pub trait InheritMaterialize: Base {} + +impl InternalMaterialize for G +where + G::Base: InternalMaterialize, +{ + fn new_base_graph(&self, graph: InternalGraph) -> MaterializedGraph { + self.base().new_base_graph(graph) + } + + fn include_deletions(&self) -> bool { + self.base().include_deletions() + } +} + +#[cfg(test)] +mod test_materialised_graph_dispatch { + use crate::{ + core::entities::LayerIds, + db::api::view::internal::{ + CoreGraphOps, EdgeFilterOps, GraphOps, InternalLayerOps, InternalMaterialize, + MaterializedGraph, TimeSemantics, + }, + prelude::*, + }; + + #[test] + fn materialised_graph_has_core_ops() { + let mg = MaterializedGraph::from(Graph::new()); + assert_eq!(mg.unfiltered_num_vertices(), 0); + } + + #[test] + fn materialised_graph_has_graph_ops() { + let mg = MaterializedGraph::from(Graph::new()); + assert_eq!(mg.vertices_len(mg.layer_ids(), mg.edge_filter()), 0); + } + #[test] + fn materialised_graph_has_edge_filter_ops() { + let mg = MaterializedGraph::from(Graph::new()); + assert!(mg.edge_filter().is_none()); + } + + #[test] + fn materialised_graph_has_layer_ops() { + let mg = MaterializedGraph::from(Graph::new()); + assert!(matches!(mg.layer_ids(), LayerIds::All)); + } + + #[test] + fn materialised_graph_has_time_semantics() { + let mg = MaterializedGraph::from(Graph::new()); + assert!(mg.view_start().is_none()); + } + + #[test] + fn materialised_graph_has_internal_materialise() { + let mg = MaterializedGraph::from(Graph::new()); + assert!(!mg.include_deletions()); + } + + #[test] + fn materialised_graph_can_be_used_directly() { + let g = Graph::new(); + + let mg = g.materialize().unwrap(); + + let v = mg.add_vertex(0, 1, NO_PROPS).unwrap(); + assert_eq!(v.id(), 1) + } +} diff --git a/raphtory/src/db/api/view/internal/mod.rs b/raphtory/src/db/api/view/internal/mod.rs new file mode 100644 index 0000000000..5f973d5614 --- /dev/null +++ b/raphtory/src/db/api/view/internal/mod.rs @@ -0,0 +1,153 @@ +mod core_deletion_ops; +mod core_ops; +mod edge_filter_ops; +mod graph_ops; +mod inherit; +mod into_dynamic; +mod layer_ops; +mod materialize; +pub(crate) mod time_semantics; +mod wrapped_graph; + +use crate::{ + db::api::properties::internal::{ConstPropertiesOps, InheritPropertiesOps, PropertiesOps}, + prelude::GraphViewOps, +}; +pub use core_deletion_ops::*; +pub use core_ops::*; +pub use edge_filter_ops::*; +pub use graph_ops::*; +pub use inherit::Base; +pub use into_dynamic::IntoDynamic; +pub use layer_ops::{DelegateLayerOps, InheritLayerOps, InternalLayerOps}; +pub use materialize::*; +use std::{ + fmt::{Debug, Formatter}, + sync::Arc, +}; +pub use time_semantics::*; + +/// Marker trait to indicate that an object is a valid graph view +pub trait BoxableGraphView: + CoreGraphOps + + GraphOps + + EdgeFilterOps + + InternalLayerOps + + TimeSemantics + + InternalMaterialize + + PropertiesOps + + ConstPropertiesOps + + Send + + Sync + + 'static +{ +} + +impl< + G: CoreGraphOps + + GraphOps + + EdgeFilterOps + + InternalLayerOps + + TimeSemantics + + InternalMaterialize + + PropertiesOps + + ConstPropertiesOps + + Send + + Sync + + 'static + + ?Sized, + > BoxableGraphView for G +{ +} + +pub trait InheritViewOps: Base {} + +impl InheritCoreDeletionOps for G {} +impl InheritGraphOps for G {} +impl InheritEdgeFilterOps for G {} +impl InheritLayerOps for G {} +impl InheritTimeSemantics for G {} +impl InheritCoreOps for G {} +impl InheritMaterialize for G {} +impl InheritPropertiesOps for G {} + +/// Trait for marking a struct as not dynamically dispatched. +/// Used to avoid conflicts when implementing `From` for dynamic wrappers. +pub trait Static {} + +impl From for DynamicGraph { + fn from(value: G) -> Self { + DynamicGraph(Arc::new(value)) + } +} + +impl From> for DynamicGraph { + fn from(value: Arc) -> Self { + DynamicGraph(value) + } +} + +/// Trait for marking a graph view as immutable to avoid conflicts when implementing conversions for mutable and immutable views +pub trait Immutable {} + +#[derive(Clone)] +pub struct DynamicGraph(pub(crate) Arc); + +impl Debug for DynamicGraph { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "DynamicGraph(num_vertices={}, num_edges={})", + self.count_vertices(), + self.count_edges() + ) + } +} + +impl DynamicGraph { + pub fn new(graph: G) -> Self { + Self(Arc::new(graph)) + } + + pub fn new_from_arc(graph_arc: Arc) -> Self { + Self(graph_arc) + } +} + +impl Base for DynamicGraph { + type Base = dyn BoxableGraphView; + + #[inline(always)] + fn base(&self) -> &Self::Base { + &self.0 + } +} + +impl Immutable for DynamicGraph {} + +impl InheritViewOps for DynamicGraph {} + +#[cfg(test)] +mod test { + use crate::{ + db::{ + api::{ + mutation::AdditionOps, + view::{internal::BoxableGraphView, *}, + }, + graph::graph::Graph, + }, + prelude::NO_PROPS, + }; + use itertools::Itertools; + use std::sync::Arc; + + #[test] + fn test_boxing() { + // this tests that a boxed graph actually compiles + let g = Graph::new(); + g.add_vertex(0, 1, NO_PROPS).unwrap(); + let boxed: Arc = Arc::new(g); + assert_eq!(boxed.vertices().id().collect_vec(), vec![1]) + } +} diff --git a/raphtory/src/db/api/view/internal/time_semantics.rs b/raphtory/src/db/api/view/internal/time_semantics.rs new file mode 100644 index 0000000000..501ad648f6 --- /dev/null +++ b/raphtory/src/db/api/view/internal/time_semantics.rs @@ -0,0 +1,567 @@ +use crate::{ + core::{ + entities::{ + edges::{edge_ref::EdgeRef, edge_store::EdgeStore}, + LayerIds, VID, + }, + storage::timeindex::TimeIndexOps, + Prop, + }, + db::api::view::{ + internal::{materialize::MaterializedGraph, Base, CoreGraphOps, EdgeFilter, GraphOps}, + BoxedIter, + }, +}; +use enum_dispatch::enum_dispatch; +use std::ops::Range; + +/// Methods for defining time windowing semantics for a graph +#[enum_dispatch] +pub trait TimeSemantics: GraphOps + CoreGraphOps { + /// Return the earliest time for a vertex + fn vertex_earliest_time(&self, v: VID) -> Option { + self.vertex_additions(v).first_t() + } + + /// Return the latest time for a vertex + fn vertex_latest_time(&self, v: VID) -> Option { + self.vertex_additions(v).last_t() + } + + /// Returns the default start time for perspectives over the view + #[inline] + fn view_start(&self) -> Option { + self.earliest_time_global() + } + + /// Returns the default end time for perspectives over the view + #[inline] + fn view_end(&self) -> Option { + self.latest_time_global().map(|v| v.saturating_add(1)) + } + + /// Returns the timestamp for the earliest activity + fn earliest_time_global(&self) -> Option; + /// Returns the timestamp for the latest activity + fn latest_time_global(&self) -> Option; + /// Returns the timestamp for the earliest activity in the window + fn earliest_time_window(&self, t_start: i64, t_end: i64) -> Option; + + /// Returns the timestamp for the latest activity in the window + fn latest_time_window(&self, t_start: i64, t_end: i64) -> Option; + + /// Return the earliest time for a vertex in a window + fn vertex_earliest_time_window(&self, v: VID, t_start: i64, t_end: i64) -> Option { + self.vertex_additions(v).range(t_start..t_end).first_t() + } + + /// Return the latest time for a vertex in a window + fn vertex_latest_time_window(&self, v: VID, t_start: i64, t_end: i64) -> Option { + self.vertex_additions(v).range(t_start..t_end).last_t() + } + /// check if vertex `v` should be included in window `w` + fn include_vertex_window( + &self, + v: VID, + w: Range, + layer_ids: &LayerIds, + edge_filter: Option<&EdgeFilter>, + ) -> bool; + + /// check if edge `e` should be included in window `w` + fn include_edge_window(&self, e: &EdgeStore, w: Range, layer_ids: &LayerIds) -> bool; + + /// Get the timestamps at which a vertex `v` is active (i.e has an edge addition) + fn vertex_history(&self, v: VID) -> Vec { + self.vertex_additions(v).iter_t().copied().collect() + } + + /// Get the timestamps at which a vertex `v` is active in window `w` (i.e has an edge addition) + fn vertex_history_window(&self, v: VID, w: Range) -> Vec { + self.vertex_additions(v) + .range(w) + .iter_t() + .copied() + .collect() + } + + /// Exploded edge iterator for edge `e` + fn edge_exploded(&self, e: EdgeRef, layer_ids: LayerIds) -> BoxedIter; + + /// Explode edge iterator for edge `e` for every layer + fn edge_layers(&self, e: EdgeRef, layer_ids: LayerIds) -> BoxedIter; + + /// Exploded edge iterator for edge`e` over window `w` + fn edge_window_exploded( + &self, + e: EdgeRef, + w: Range, + layer_ids: LayerIds, + ) -> BoxedIter; + + /// Exploded edge iterator for edge `e` over window `w` for every layer + fn edge_window_layers( + &self, + e: EdgeRef, + w: Range, + layer_ids: LayerIds, + ) -> BoxedIter; + + /// Get the time of the earliest activity of an edge + fn edge_earliest_time(&self, e: EdgeRef, layer_ids: LayerIds) -> Option; + + /// Get the time of the earliest activity of an edge `e` in window `w` + fn edge_earliest_time_window( + &self, + e: EdgeRef, + w: Range, + layer_ids: LayerIds, + ) -> Option; + + /// Get the time of the latest activity of an edge + fn edge_latest_time(&self, e: EdgeRef, layer_ids: LayerIds) -> Option; + + /// Get the time of the latest activity of an edge `e` in window `w` + fn edge_latest_time_window( + &self, + e: EdgeRef, + w: Range, + layer_ids: LayerIds, + ) -> Option; + + /// Get the edge deletions for use with materialize + fn edge_deletion_history(&self, e: EdgeRef, layer_ids: LayerIds) -> Vec; + + /// Get the edge deletions for use with materialize restricted to window `w` + fn edge_deletion_history_window( + &self, + e: EdgeRef, + w: Range, + layer_ids: LayerIds, + ) -> Vec; + + /// Check if graph has temporal property with the given id + /// + /// # Arguments + /// + /// * `prop_id` - The id of the property to retrieve. + fn has_temporal_prop(&self, prop_id: usize) -> bool; + + /// Returns a vector of all temporal values of the graph property with the given id + /// + /// # Arguments + /// + /// * `prop_id` - The id of the property to retrieve. + /// + /// # Returns + /// + /// A vector of tuples representing the temporal values of the property + /// that fall within the specified time window, where the first element of each tuple is the timestamp + /// and the second element is the property value. + fn temporal_prop_vec(&self, prop_id: usize) -> Vec<(i64, Prop)>; + + /// Check if graph has temporal property with the given id in the window + /// + /// # Arguments + /// + /// * `prop_id` - The id of the property to retrieve. + /// * `w` - time window + fn has_temporal_prop_window(&self, prop_id: usize, w: Range) -> bool; + + /// Returns a vector of all temporal values of the graph property with the given name + /// that fall within the specified time window. + /// + /// # Arguments + /// + /// * `name` - The name of the property to retrieve. + /// * `t_start` - The start time of the window to consider. + /// * `t_end` - The end time of the window to consider. + /// + /// # Returns + /// + /// A vector of tuples representing the temporal values of the property + /// that fall within the specified time window, where the first element of each tuple is the timestamp + /// and the second element is the property value. + fn temporal_prop_vec_window( + &self, + prop_id: usize, + t_start: i64, + t_end: i64, + ) -> Vec<(i64, Prop)>; + + /// Check if vertex has temporal property with the given id + /// + /// # Arguments + /// + /// * `v` - The id of the vertex + /// * `prop_id` - The id of the property to retrieve. + fn has_temporal_vertex_prop(&self, v: VID, prop_id: usize) -> bool; + + /// Returns a vector of all temporal values of the vertex property with the given name for the + /// given vertex + /// + /// # Arguments + /// + /// * `v` - A reference to the vertex for which to retrieve the temporal property vector. + /// * `name` - The name of the property to retrieve. + /// + /// # Returns + /// + /// A vector of tuples representing the temporal values of the property for the given vertex + /// that fall within the specified time window, where the first element of each tuple is the timestamp + /// and the second element is the property value. + fn temporal_vertex_prop_vec(&self, v: VID, id: usize) -> Vec<(i64, Prop)>; + + /// Check if vertex has temporal property with the given id in the window + /// + /// # Arguments + /// + /// * `v` - the id of the vertex + /// * `prop_id` - The id of the property to retrieve. + /// * `w` - time window + fn has_temporal_vertex_prop_window(&self, v: VID, prop_id: usize, w: Range) -> bool; + + /// Returns a vector of all temporal values of the vertex property with the given name for the given vertex + /// that fall within the specified time window. + /// + /// # Arguments + /// + /// * `v` - A reference to the vertex for which to retrieve the temporal property vector. + /// * `name` - The name of the property to retrieve. + /// * `t_start` - The start time of the window to consider. + /// * `t_end` - The end time of the window to consider. + /// + /// # Returns + /// + /// A vector of tuples representing the temporal values of the property for the given vertex + /// that fall within the specified time window, where the first element of each tuple is the timestamp + /// and the second element is the property value. + fn temporal_vertex_prop_vec_window( + &self, + v: VID, + id: usize, + t_start: i64, + t_end: i64, + ) -> Vec<(i64, Prop)>; + + /// Check if edge has temporal property with the given id in the window + /// + /// # Arguments + /// + /// * `e` - the id of the edge + /// * `prop_id` - The id of the property to retrieve. + /// * `w` - time window + fn has_temporal_edge_prop_window( + &self, + e: EdgeRef, + prop_id: usize, + w: Range, + layer_ids: LayerIds, + ) -> bool; + + /// Returns a vector of tuples containing the values of the temporal property with the given name + /// for the given edge reference within the specified time window. + /// + /// # Arguments + /// + /// * `e` - An `EdgeRef` reference to the edge of interest. + /// * `name` - A `String` containing the name of the temporal property. + /// * `t_start` - An `i64` containing the start time of the time window (inclusive). + /// * `t_end` - An `i64` containing the end time of the time window (exclusive). + /// + /// # Returns + /// + /// * A `Vec` of tuples containing the values of the temporal property with the given name for the given edge + /// within the specified time window. + /// + fn temporal_edge_prop_vec_window( + &self, + e: EdgeRef, + id: usize, + t_start: i64, + t_end: i64, + layer_ids: LayerIds, + ) -> Vec<(i64, Prop)>; + + /// Check if edge has temporal property with the given id + /// + /// # Arguments + /// + /// * `e` - The id of the edge + /// * `prop_id` - The id of the property to retrieve. + fn has_temporal_edge_prop(&self, e: EdgeRef, prop_id: usize, layer_ids: LayerIds) -> bool; + + /// Returns a vector of tuples containing the values of the temporal property with the given name + /// for the given edge reference. + /// + /// # Arguments + /// + /// * `e` - An `EdgeRef` reference to the edge of interest. + /// * `name` - A `String` containing the name of the temporal property. + /// + /// # Returns + /// + /// * A `Vec` of tuples containing the values of the temporal property with the given name for the given edge. + fn temporal_edge_prop_vec( + &self, + e: EdgeRef, + id: usize, + layer_ids: LayerIds, + ) -> Vec<(i64, Prop)>; +} + +pub trait InheritTimeSemantics: Base + GraphOps + CoreGraphOps {} + +impl DelegateTimeSemantics for G +where + ::Base: TimeSemantics, +{ + type Internal = ::Base; + + fn graph(&self) -> &Self::Internal { + self.base() + } +} + +pub trait DelegateTimeSemantics: GraphOps + CoreGraphOps { + type Internal: TimeSemantics + ?Sized; + + fn graph(&self) -> &Self::Internal; +} + +impl TimeSemantics for G { + #[inline] + fn vertex_earliest_time(&self, v: VID) -> Option { + self.graph().vertex_earliest_time(v) + } + + #[inline] + fn vertex_latest_time(&self, v: VID) -> Option { + self.graph().vertex_latest_time(v) + } + + #[inline] + fn view_start(&self) -> Option { + self.graph().view_start() + } + #[inline] + fn view_end(&self) -> Option { + self.graph().view_end() + } + #[inline] + fn earliest_time_global(&self) -> Option { + self.graph().earliest_time_global() + } + #[inline] + fn latest_time_global(&self) -> Option { + self.graph().latest_time_global() + } + #[inline] + fn earliest_time_window(&self, t_start: i64, t_end: i64) -> Option { + self.graph().earliest_time_window(t_start, t_end) + } + #[inline] + fn latest_time_window(&self, t_start: i64, t_end: i64) -> Option { + self.graph().latest_time_window(t_start, t_end) + } + #[inline] + fn vertex_earliest_time_window(&self, v: VID, t_start: i64, t_end: i64) -> Option { + self.graph().vertex_earliest_time_window(v, t_start, t_end) + } + #[inline] + fn vertex_latest_time_window(&self, v: VID, t_start: i64, t_end: i64) -> Option { + self.graph().vertex_latest_time_window(v, t_start, t_end) + } + #[inline] + fn include_vertex_window( + &self, + v: VID, + w: Range, + layer_ids: &LayerIds, + edge_filter: Option<&EdgeFilter>, + ) -> bool { + self.graph() + .include_vertex_window(v, w, layer_ids, edge_filter) + } + + #[inline] + fn include_edge_window(&self, e: &EdgeStore, w: Range, layer_ids: &LayerIds) -> bool { + self.graph().include_edge_window(e, w, layer_ids) + } + + #[inline] + fn vertex_history(&self, v: VID) -> Vec { + self.graph().vertex_history(v) + } + + #[inline] + fn vertex_history_window(&self, v: VID, w: Range) -> Vec { + self.graph().vertex_history_window(v, w) + } + + #[inline] + fn edge_exploded(&self, e: EdgeRef, layer_ids: LayerIds) -> BoxedIter { + self.graph().edge_exploded(e, layer_ids) + } + + #[inline] + fn edge_layers(&self, e: EdgeRef, layer_ids: LayerIds) -> BoxedIter { + self.graph().edge_layers(e, layer_ids) + } + + #[inline] + fn edge_window_exploded( + &self, + e: EdgeRef, + w: Range, + layer_ids: LayerIds, + ) -> BoxedIter { + self.graph().edge_window_exploded(e, w, layer_ids) + } + + #[inline] + fn edge_window_layers( + &self, + e: EdgeRef, + w: Range, + layer_ids: LayerIds, + ) -> BoxedIter { + self.graph().edge_window_layers(e, w, layer_ids) + } + + #[inline] + fn edge_earliest_time(&self, e: EdgeRef, layer_ids: LayerIds) -> Option { + self.graph().edge_earliest_time(e, layer_ids) + } + + #[inline] + fn edge_earliest_time_window( + &self, + e: EdgeRef, + w: Range, + layer_ids: LayerIds, + ) -> Option { + self.graph().edge_earliest_time_window(e, w, layer_ids) + } + + #[inline] + fn edge_latest_time(&self, e: EdgeRef, layer_ids: LayerIds) -> Option { + self.graph().edge_latest_time(e, layer_ids) + } + + #[inline] + fn edge_latest_time_window( + &self, + e: EdgeRef, + w: Range, + layer_ids: LayerIds, + ) -> Option { + self.graph().edge_latest_time_window(e, w, layer_ids) + } + + #[inline] + fn edge_deletion_history(&self, e: EdgeRef, layer_ids: LayerIds) -> Vec { + self.graph().edge_deletion_history(e, layer_ids) + } + + #[inline] + fn edge_deletion_history_window( + &self, + e: EdgeRef, + w: Range, + layer_ids: LayerIds, + ) -> Vec { + self.graph().edge_deletion_history_window(e, w, layer_ids) + } + + #[inline] + fn has_temporal_prop(&self, prop_id: usize) -> bool { + self.graph().has_temporal_prop(prop_id) + } + + #[inline] + fn temporal_prop_vec(&self, prop_id: usize) -> Vec<(i64, Prop)> { + self.graph().temporal_prop_vec(prop_id) + } + + #[inline] + fn has_temporal_prop_window(&self, prop_id: usize, w: Range) -> bool { + self.graph().has_temporal_prop_window(prop_id, w) + } + + #[inline] + fn temporal_prop_vec_window( + &self, + prop_id: usize, + t_start: i64, + t_end: i64, + ) -> Vec<(i64, Prop)> { + self.graph() + .temporal_prop_vec_window(prop_id, t_start, t_end) + } + + #[inline] + fn has_temporal_vertex_prop(&self, v: VID, prop_id: usize) -> bool { + self.graph().has_temporal_vertex_prop(v, prop_id) + } + + #[inline] + fn temporal_vertex_prop_vec(&self, v: VID, prop_id: usize) -> Vec<(i64, Prop)> { + self.graph().temporal_vertex_prop_vec(v, prop_id) + } + + #[inline] + fn has_temporal_vertex_prop_window(&self, v: VID, prop_id: usize, w: Range) -> bool { + self.graph().has_temporal_vertex_prop_window(v, prop_id, w) + } + + #[inline] + fn temporal_vertex_prop_vec_window( + &self, + v: VID, + prop_id: usize, + t_start: i64, + t_end: i64, + ) -> Vec<(i64, Prop)> { + self.graph() + .temporal_vertex_prop_vec_window(v, prop_id, t_start, t_end) + } + + fn has_temporal_edge_prop_window( + &self, + e: EdgeRef, + prop_id: usize, + w: Range, + layer_ids: LayerIds, + ) -> bool { + self.graph() + .has_temporal_edge_prop_window(e, prop_id, w, layer_ids) + } + + #[inline] + fn temporal_edge_prop_vec_window( + &self, + e: EdgeRef, + prop_id: usize, + t_start: i64, + t_end: i64, + layer_ids: LayerIds, + ) -> Vec<(i64, Prop)> { + self.graph() + .temporal_edge_prop_vec_window(e, prop_id, t_start, t_end, layer_ids) + } + + fn has_temporal_edge_prop(&self, e: EdgeRef, prop_id: usize, layer_ids: LayerIds) -> bool { + self.graph().has_temporal_edge_prop(e, prop_id, layer_ids) + } + + #[inline] + fn temporal_edge_prop_vec( + &self, + e: EdgeRef, + prop_id: usize, + layer_ids: LayerIds, + ) -> Vec<(i64, Prop)> { + self.graph().temporal_edge_prop_vec(e, prop_id, layer_ids) + } +} diff --git a/raphtory/src/db/api/view/internal/wrapped_graph.rs b/raphtory/src/db/api/view/internal/wrapped_graph.rs new file mode 100644 index 0000000000..1b49794d60 --- /dev/null +++ b/raphtory/src/db/api/view/internal/wrapped_graph.rs @@ -0,0 +1,4 @@ +use crate::db::api::view::internal::{BoxableGraphView, InheritViewOps}; +use std::sync::Arc; + +impl InheritViewOps for Arc {} diff --git a/raphtory/src/db/api/view/layer.rs b/raphtory/src/db/api/view/layer.rs new file mode 100644 index 0000000000..ed622ef30b --- /dev/null +++ b/raphtory/src/db/api/view/layer.rs @@ -0,0 +1,83 @@ +use crate::core::ArcStr; +use std::sync::Arc; + +/// Trait defining layer operations +pub trait LayerOps { + type LayeredViewType; + + /// Return a graph containing only the default edge layer + fn default_layer(&self) -> Self::LayeredViewType; + + /// Return a graph containing the layer `name` + fn layer>(&self, name: L) -> Option; +} + +#[derive(Debug)] +pub enum Layer { + All, + Default, + One(ArcStr), + Multiple(Arc<[String]>), +} + +impl<'a, T: ToOwned + ?Sized> From> for Layer { + fn from(name: Option<&'a T>) -> Self { + match name { + Some(name) => Layer::One(name.to_owned().into()), + None => Layer::All, + } + } +} + +impl From> for Layer { + fn from(value: Option) -> Self { + match value { + Some(name) => Layer::One(name.into()), + None => Layer::All, + } + } +} + +impl From for Layer { + fn from(value: ArcStr) -> Self { + Layer::One(value) + } +} + +impl From for Layer { + fn from(value: String) -> Self { + Layer::One(value.into()) + } +} + +impl<'a, T: ToOwned + ?Sized> From<&'a T> for Layer { + fn from(name: &'a T) -> Self { + Layer::One(name.to_owned().into()) + } +} + +impl<'a, T: ToOwned + ?Sized> From> for Layer { + fn from(names: Vec<&'a T>) -> Self { + match names.len() { + 0 => Layer::All, + 1 => Layer::One(names[0].to_owned().into()), + _ => Layer::Multiple( + names + .into_iter() + .map(|s| s.to_owned()) + .collect::>() + .into(), + ), + } + } +} + +impl From> for Layer { + fn from(names: Vec) -> Self { + match names.len() { + 0 => Layer::All, + 1 => Layer::One(names.into_iter().next().expect("exists").into()), + _ => Layer::Multiple(names.into()), + } + } +} diff --git a/raphtory/src/db/api/view/mod.rs b/raphtory/src/db/api/view/mod.rs new file mode 100644 index 0000000000..6be1d0f853 --- /dev/null +++ b/raphtory/src/db/api/view/mod.rs @@ -0,0 +1,16 @@ +//! Defines the `ViewApi` trait, which represents the API for querying a view of the graph. + +mod edge; +mod graph; +pub mod internal; +mod layer; +mod time; +mod vertex; + +pub use edge::*; +pub use graph::*; +pub use layer::*; +pub use time::*; +pub use vertex::*; + +pub type BoxedIter = Box + Send>; diff --git a/raphtory/src/db/view_api/time.rs b/raphtory/src/db/api/view/time.rs similarity index 80% rename from raphtory/src/db/view_api/time.rs rename to raphtory/src/db/api/view/time.rs index 6f79cf5b73..b5569dbed0 100644 --- a/raphtory/src/db/view_api/time.rs +++ b/raphtory/src/db/api/view/time.rs @@ -1,5 +1,4 @@ -use crate::core::time::error::ParseTimeError; -use crate::core::time::{Interval, IntoTime}; +use crate::core::utils::time::{error::ParseTimeError, Interval, IntoTime}; /// Trait defining time query operations pub trait TimeOps { @@ -24,11 +23,17 @@ pub trait TimeOps { /// Create a view including all events until `end` (inclusive) fn at(&self, end: T) -> Self::WindowedViewType { - self.window(i64::MIN, end.into_time().saturating_add(1)) + let end = end.into_time(); + let start = self.start().unwrap_or(end); + if start > end { + self.window(end, end.saturating_add(1)) + } else { + self.window(start, end.saturating_add(1)) + } } - /// Creates a `WindowSet` with the given `step` size and optional `start` and `end` times, - /// using an expanding window. + /// Creates a `WindowSet` with the given `step` size + /// using an expanding window. The last window may fall partially outside the range of the data/view. /// /// An expanding window is a window that grows by `step` size at each iteration. fn expanding(&self, step: I) -> Result, ParseTimeError> @@ -47,8 +52,8 @@ pub trait TimeOps { } } - /// Creates a `WindowSet` with the given `window` size and optional `step`, `start` and `end` times, - /// using a rolling window. + /// Creates a `WindowSet` with the given `window` size and optional `step` + /// using a rolling window. The last window may fall partially outside the range of the data/view. /// /// A rolling window is a window that moves forward by `step` size at each iteration. fn rolling(&self, window: I, step: Option) -> Result, ParseTimeError> @@ -90,7 +95,7 @@ impl WindowSet { // } else { // timeline_start + step - 1 // }; - let cursor_start = start + step - 1; + let cursor_start = start + step; Self { view, cursor: cursor_start, @@ -145,9 +150,12 @@ impl Iterator for TimeIndex { impl Iterator for WindowSet { type Item = T::WindowedViewType; fn next(&mut self) -> Option { - if self.cursor < self.end { - let window_end = self.cursor + 1; - let window_start = self.window.map(|w| window_end - w).unwrap_or(i64::MIN); + if self.cursor < self.end + self.step { + let window_end = self.cursor; + let window_start = self + .window + .map(|w| window_end - w) + .unwrap_or(self.view.start().unwrap_or(window_end)); let window = self.view.window(window_start, window_end); self.cursor = self.cursor + self.step; Some(window) @@ -159,18 +167,24 @@ impl Iterator for WindowSet { #[cfg(test)] mod time_tests { - use crate::core::time::TryIntoTime; - use crate::db::graph::Graph; - use crate::db::view_api::internal::GraphViewInternalOps; - use crate::db::view_api::time::WindowSet; - use crate::db::view_api::{GraphViewOps, TimeOps}; + use crate::{ + core::utils::time::TryIntoTime, + db::{ + api::{ + mutation::AdditionOps, + view::{time::WindowSet, GraphViewOps, TimeOps}, + }, + graph::graph::Graph, + }, + prelude::NO_PROPS, + }; use itertools::Itertools; // start inclusive, end exclusive fn graph_with_timeline(start: i64, end: i64) -> Graph { - let g = Graph::new(4); - g.add_vertex(start, 0, &vec![]).unwrap(); - g.add_vertex(end - 1, 0, &vec![]).unwrap(); + let g = Graph::new(); + g.add_vertex(start, 0, NO_PROPS).unwrap(); + g.add_vertex(end - 1, 0, NO_PROPS).unwrap(); assert_eq!(g.start().unwrap(), start); assert_eq!(g.end().unwrap(), end); g @@ -178,7 +192,7 @@ mod time_tests { fn assert_bounds(windows: WindowSet, expected: Vec<(i64, i64)>) where - G: GraphViewOps + GraphViewInternalOps, + G: GraphViewOps, { let window_bounds = windows .map(|w| (w.start().unwrap(), w.end().unwrap())) @@ -195,7 +209,7 @@ mod time_tests { let g = graph_with_timeline(1, 6); let windows = g.rolling(3, Some(2)).unwrap(); - let expected = vec![(0, 3), (2, 5)]; + let expected = vec![(0, 3), (2, 5), (4, 7)]; assert_bounds(windows, expected.clone()); let g = graph_with_timeline(0, 9).window(1, 6); @@ -205,15 +219,14 @@ mod time_tests { #[test] fn expanding() { - let min = i64::MIN; let g = graph_with_timeline(1, 7); let windows = g.expanding(2).unwrap(); - let expected = vec![(min, 3), (min, 5), (min, 7)]; + let expected = vec![(1, 3), (1, 5), (1, 7)]; assert_bounds(windows, expected); let g = graph_with_timeline(1, 6); let windows = g.expanding(2).unwrap(); - let expected = vec![(min, 3), (min, 5)]; + let expected = vec![(1, 3), (1, 5), (1, 7)]; assert_bounds(windows, expected.clone()); let g = graph_with_timeline(0, 9).window(1, 6); @@ -227,10 +240,16 @@ mod time_tests { let end = "2020-06-07 23:59:59.999".try_into_time().unwrap(); let g = graph_with_timeline(start, end); let windows = g.rolling("1 day", None).unwrap(); - let expected = vec![( - "2020-06-06 00:00:00".try_into_time().unwrap(), // entire 2020-06-06 - "2020-06-07 00:00:00".try_into_time().unwrap(), - )]; + let expected = vec![ + ( + "2020-06-06 00:00:00".try_into_time().unwrap(), // entire 2020-06-06 + "2020-06-07 00:00:00".try_into_time().unwrap(), + ), + ( + "2020-06-07 00:00:00".try_into_time().unwrap(), // entire 2020-06-06 + "2020-06-08 00:00:00".try_into_time().unwrap(), + ), + ]; assert_bounds(windows, expected); let start = "2020-06-06 00:00:00".try_into_time().unwrap(); @@ -269,13 +288,14 @@ mod time_tests { #[test] fn expanding_dates() { - let min = i64::MIN; - let start = "2020-06-06 00:00:00".try_into_time().unwrap(); let end = "2020-06-07 23:59:59.999".try_into_time().unwrap(); let g = graph_with_timeline(start, end); let windows = g.expanding("1 day").unwrap(); - let expected = vec![(min, "2020-06-07 00:00:00".try_into_time().unwrap())]; + let expected = vec![ + (start, "2020-06-07 00:00:00".try_into_time().unwrap()), + (start, "2020-06-08 00:00:00".try_into_time().unwrap()), + ]; assert_bounds(windows, expected); let start = "2020-06-06 00:00:00".try_into_time().unwrap(); @@ -283,8 +303,8 @@ mod time_tests { let g = graph_with_timeline(start, end); let windows = g.expanding("1 day").unwrap(); let expected = vec![ - (min, "2020-06-07 00:00:00".try_into_time().unwrap()), - (min, "2020-06-08 00:00:00".try_into_time().unwrap()), + (start, "2020-06-07 00:00:00".try_into_time().unwrap()), + (start, "2020-06-08 00:00:00".try_into_time().unwrap()), ]; assert_bounds(windows, expected); } diff --git a/raphtory/src/db/view_api/vertex.rs b/raphtory/src/db/api/view/vertex.rs similarity index 60% rename from raphtory/src/db/view_api/vertex.rs rename to raphtory/src/db/api/view/vertex.rs index a9bc641eab..98a0eb4fc1 100644 --- a/raphtory/src/db/view_api/vertex.rs +++ b/raphtory/src/db/api/view/vertex.rs @@ -1,7 +1,10 @@ -use crate::core::Prop; -use crate::db::view_api::edge::EdgeListOps; -use crate::db::view_api::{GraphViewOps, TimeOps}; -use std::collections::HashMap; +use crate::db::{ + api::{ + properties::Properties, + view::{edge::EdgeListOps, GraphViewOps, TimeOps}, + }, + graph::vertex::VertexView, +}; /// Operations defined for a vertex pub trait VertexViewOps: TimeOps { @@ -28,88 +31,15 @@ pub trait VertexViewOps: TimeOps { /// Get the timestamp for the latest activity of the vertex fn latest_time(&self) -> Self::ValueType>; - /// Gets the property value of this vertex given the name of the property. - fn property(&self, name: String, include_static: bool) -> Self::ValueType>; - /// Gets the history of the vertex (time that the vertex was added and times when changes were made to the vertex) fn history(&self) -> Self::ValueType>; - /// Get the temporal property value of this vertex. - /// - /// # Arguments - /// - /// * `name` - The name of the property to retrieve. - /// - /// # Returns - /// - /// A vector of `(i64, Prop)` tuples where the `i64` value is the timestamp of the - /// property value and `Prop` is the value itself. - fn property_history(&self, name: String) -> Self::ValueType>; - - /// Get all property values of this vertex. - /// - /// # Arguments - /// - /// * `include_static` - If `true` then static properties are included in the result. - /// - /// # Returns - /// - /// A HashMap with the names of the properties as keys and the property values as values. - fn properties(&self, include_static: bool) -> Self::ValueType>; - - /// Get all temporal property values of this vertex. - /// - /// # Returns - /// - /// A HashMap with the names of the properties as keys and a vector of `(i64, Prop)` tuples - /// as values. The `i64` value is the timestamp of the property value and `Prop` - /// is the value itself. - fn property_histories(&self) -> Self::ValueType>>; - - /// Get the names of all properties of this vertex. - /// - /// # Arguments - /// - /// * `include_static` - If `true` then static properties are included in the result. - /// - /// # Returns - /// - /// A vector of the names of the properties of this vertex. - fn property_names(&self, include_static: bool) -> Self::ValueType>; - - /// Checks if a property exists on this vertex. - /// - /// # Arguments - /// - /// * `name` - The name of the property to check for. - /// * `include_static` - If `true` then static properties are included in the result. - /// - /// # Returns - /// - /// `true` if the property exists, otherwise `false`. - fn has_property(&self, name: String, include_static: bool) -> Self::ValueType; - - /// Checks if a static property exists on this vertex. - /// - /// # Arguments - /// - /// * `name` - The name of the property to check for. - /// - /// # Returns - /// - /// `true` if the property exists, otherwise `false`. - fn has_static_property(&self, name: String) -> Self::ValueType; - - /// Get the static property value of this vertex. - /// - /// # Arguments - /// - /// * `name` - The name of the property to retrieve. + /// Get a view of the temporal properties of this vertex. /// /// # Returns /// - /// The value of the property if it exists, otherwise `None`. - fn static_property(&self, name: String) -> Self::ValueType>; + /// A view with the names of the properties as keys and the property values as values. + fn properties(&self) -> Self::ValueType>>; /// Get the degree of this vertex (i.e., the number of edges that are incident to it). /// @@ -201,9 +131,7 @@ pub trait VertexListOps: ) -> Self::IterType<::WindowedViewType>; /// Create views for the vertices including all events until `end` (inclusive) - fn at(self, end: i64) -> Self::IterType<::WindowedViewType> { - self.window(i64::MIN, end.saturating_add(1)) - } + fn at(self, end: i64) -> Self::IterType<::WindowedViewType>; /// Returns the ids of vertices in the list. /// @@ -212,31 +140,10 @@ pub trait VertexListOps: fn id(self) -> Self::IterType; fn name(self) -> Self::IterType; - fn property(self, name: String, include_static: bool) -> Self::IterType>; + /// Returns an iterator over properties of the vertices + fn properties(self) -> Self::IterType>>; - /// Returns an iterator of the values of the given property name - /// including the times when it changed - /// - /// # Arguments - /// * `name` - The name of the property. - /// - /// # Returns - /// An iterator of the values of the given property name including the times when it changed - /// as a vector of tuples of the form (time, property). - fn property_history(self, name: String) -> Self::IterType>; - fn properties(self, include_static: bool) -> Self::IterType>; fn history(self) -> Self::IterType>; - /// Returns an iterator over all vertex properties. - /// - /// # Returns - /// An iterator over all vertex properties. - fn property_histories(self) -> Self::IterType>>; - fn property_names(self, include_static: bool) -> Self::IterType>; - fn has_property(self, name: String, include_static: bool) -> Self::IterType; - - fn has_static_property(self, name: String) -> Self::IterType; - - fn static_property(self, name: String) -> Self::IterType>; /// Returns an iterator over the degree of the vertices. /// diff --git a/raphtory/src/db/edge.rs b/raphtory/src/db/edge.rs deleted file mode 100644 index 3ae42e782b..0000000000 --- a/raphtory/src/db/edge.rs +++ /dev/null @@ -1,264 +0,0 @@ -//! Defines the `Edge` struct, which represents an edge in the graph. -//! -//! Edges are used to define directed connections between verticies in the graph. -//! Edges are identified by a unique ID, can have a direction (Ingoing, Outgoing, or Both) -//! and can have properties associated with them. -//! - -use crate::core::edge_ref::EdgeRef; -use crate::core::time::IntoTime; -use crate::core::vertex_ref::VertexRef; -use crate::core::Prop; -use crate::db::graph_window::WindowedGraph; -use crate::db::vertex::VertexView; -use crate::db::view_api::edge::{EdgeViewInternalOps, EdgeViewOps}; -use crate::db::view_api::*; -use std::collections::HashMap; -use std::fmt::{Debug, Formatter}; -use std::iter; - -/// A view of an edge in the graph. -#[derive(Clone)] -pub struct EdgeView { - /// A view of an edge in the graph. - pub graph: G, - /// A reference to the edge. - pub edge: EdgeRef, -} - -impl EdgeView { - pub fn new(graph: G, edge: EdgeRef) -> Self { - Self { graph, edge } - } -} - -impl EdgeViewInternalOps> for EdgeView { - fn graph(&self) -> G { - self.graph.clone() - } - - fn eref(&self) -> EdgeRef { - self.edge - } - - fn new_vertex(&self, v: VertexRef) -> VertexView { - VertexView::new(self.graph(), v) - } - - fn new_edge(&self, e: EdgeRef) -> Self { - Self { - graph: self.graph(), - edge: e, - } - } -} - -impl EdgeViewOps for EdgeView { - type Graph = G; - type Vertex = VertexView; - type EList = BoxedIter; - - fn explode(&self) -> Self::EList { - let ev = self.clone(); - match self.edge.time() { - Some(_) => Box::new(iter::once(ev)), - None => { - let e = self.edge; - let ts = self.graph.edge_timestamps(self.edge, None); - Box::new(ts.into_iter().map(move |t| ev.new_edge(e.at(t)))) - } - } - } -} - -impl Debug for EdgeView { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!( - f, - "EdgeView({}, {})", - self.graph.vertex(self.edge.src()).unwrap().id(), - self.graph.vertex(self.edge.dst()).unwrap().id() - ) - } -} - -impl From> for EdgeRef { - fn from(value: EdgeView) -> Self { - value.edge - } -} - -impl TimeOps for EdgeView { - type WindowedViewType = EdgeView>; - - fn start(&self) -> Option { - self.graph.start() - } - - fn end(&self) -> Option { - self.graph.end() - } - - fn window(&self, t_start: T, t_end: T) -> Self::WindowedViewType { - EdgeView { - graph: self.graph.window(t_start, t_end), - edge: self.edge, - } - } -} - -/// Implement `EdgeListOps` trait for an iterator of `EdgeView` objects. -/// -/// This implementation enables the use of the `src` and `dst` methods to retrieve the vertices -/// connected to the edges inside the iterator. -impl EdgeListOps for BoxedIter> { - type Graph = G; - type Vertex = VertexView; - type Edge = EdgeView; - type ValueType = T; - - /// Specifies the associated type for an iterator over vertices. - type VList = Box> + Send>; - - /// Specifies the associated type for the iterator over edges. - type IterType = Box + Send>; - - fn has_property(self, name: String, include_static: bool) -> BoxedIter { - Box::new(self.map(move |e| e.has_property(name.clone(), include_static))) - } - - fn property(self, name: String, include_static: bool) -> BoxedIter> { - Box::new(self.map(move |e| e.property(name.clone(), include_static))) - } - - fn properties(self, include_static: bool) -> BoxedIter> { - Box::new(self.map(move |e| e.properties(include_static))) - } - - fn property_names(self, include_static: bool) -> BoxedIter> { - Box::new(self.map(move |e| e.property_names(include_static))) - } - - fn has_static_property(self, name: String) -> BoxedIter { - Box::new(self.map(move |e| e.has_static_property(name.clone()))) - } - - fn static_property(self, name: String) -> BoxedIter> { - Box::new(self.map(move |e| e.static_property(name.clone()))) - } - - fn property_history(self, name: String) -> BoxedIter> { - Box::new(self.map(move |e| e.property_history(name.clone()))) - } - - fn property_histories(self) -> BoxedIter>> { - Box::new(self.map(|e| e.property_histories())) - } - - /// Returns an iterator over the source vertices of the edges in the iterator. - fn src(self) -> Self::VList { - Box::new(self.map(|e| e.src())) - } - - /// Returns an iterator over the destination vertices of the edges in the iterator. - fn dst(self) -> Self::VList { - Box::new(self.map(|e| e.dst())) - } - - fn id(self) -> Self::IterType<(u64, u64)> { - Box::new(self.map(|e| e.id())) - } - - /// returns an iterator of exploded edges that include an edge at each point in time - fn explode(self) -> Self { - Box::new(self.flat_map(move |e| e.explode())) - } - - /// Gets the earliest times of a list of edges - fn earliest_time(self) -> Self::IterType> { - Box::new(self.map(|e| e.earliest_time())) - } - - /// Gets the latest times of a list of edges - fn latest_time(self) -> Self::IterType> { - Box::new(self.map(|e| e.latest_time())) - } -} - -impl EdgeListOps for BoxedIter>> { - type Graph = G; - type Vertex = VertexView; - type Edge = EdgeView; - type ValueType = Box + Send>; - type VList = Box> + Send>> + Send>; - type IterType = Box + Send>> + Send>; - - fn has_property(self, name: String, include_static: bool) -> BoxedIter> { - Box::new(self.map(move |it| { - let name = name.clone(); - let iter: Self::ValueType = - Box::new(it.map(move |e| e.has_property(name.clone(), include_static))); - iter - })) - } - - fn property( - self, - name: String, - include_static: bool, - ) -> BoxedIter>> { - Box::new(self.map(move |it| it.property(name.clone(), include_static))) - } - - fn properties(self, include_static: bool) -> BoxedIter>> { - Box::new(self.map(move |it| it.properties(include_static))) - } - - fn property_names(self, include_static: bool) -> BoxedIter>> { - Box::new(self.map(move |it| it.property_names(include_static))) - } - - fn has_static_property(self, name: String) -> BoxedIter> { - Box::new(self.map(move |it| it.has_static_property(name.clone()))) - } - - fn static_property(self, name: String) -> BoxedIter>> { - Box::new(self.map(move |it| it.static_property(name.clone()))) - } - - fn property_history(self, name: String) -> BoxedIter>> { - Box::new(self.map(move |it| it.property_history(name.clone()))) - } - - fn property_histories(self) -> BoxedIter>>> { - Box::new(self.map(|it| it.property_histories())) - } - - fn src(self) -> Self::VList { - Box::new(self.map(|it| it.src())) - } - - fn dst(self) -> Self::VList { - Box::new(self.map(|it| it.dst())) - } - - fn id(self) -> Self::IterType<(u64, u64)> { - Box::new(self.map(|it| it.id())) - } - - fn explode(self) -> Self { - Box::new(self.map(move |it| it.explode())) - } - - /// Gets the earliest times of a list of edges - fn earliest_time(self) -> Self::IterType> { - Box::new(self.map(|e| e.earliest_time())) - } - - /// Gets the latest times of a list of edges - fn latest_time(self) -> Self::IterType> { - Box::new(self.map(|e| e.latest_time())) - } -} - -pub type EdgeList = Box> + Send>; diff --git a/raphtory/src/db/graph.rs b/raphtory/src/db/graph.rs deleted file mode 100644 index 40a92e995d..0000000000 --- a/raphtory/src/db/graph.rs +++ /dev/null @@ -1,2178 +0,0 @@ -//! Defines the `Graph` struct, which represents a raphtory graph in memory. -//! -//! This is the base class used to create a temporal graph, add vertices and edges, -//! create windows, and query the graph with a variety of algorithms. -//! It is a wrapper around a set of shards, which are the actual graph data structures. -//! -//! # Examples -//! -//! ```rust -//! use raphtory::db::graph::InternalGraph; -//! use raphtory::db::view_api::*; -//! let graph = InternalGraph::new(2); -//! graph.add_vertex(0, "Alice", &vec![]); -//! graph.add_vertex(1, "Bob", &vec![]); -//! graph.add_edge(2, "Alice", "Bob", &vec![], None); -//! graph.num_edges(); -//! ``` -//! - -use crate::core::tgraph::TemporalGraph; -use crate::core::tgraph_shard::TGraphShard; -use crate::core::time::{IntoTimeWithFormat, TryIntoTime}; -use crate::core::{ - edge_ref::EdgeRef, tgraph_shard::errors::GraphError, utils, vertex::InputVertex, - vertex_ref::VertexRef, Direction, Prop, -}; - -use crate::core::vertex_ref::LocalVertexRef; -use crate::db::graph_immutable::ImmutableGraph; -use crate::db::view_api::internal::{GraphViewInternalOps, WrappedGraph}; -use crate::db::view_api::*; -use itertools::Itertools; -use rayon::prelude::*; -use rustc_hash::FxHashMap; -use serde::{Deserialize, Serialize}; -use std::cmp::{max, min}; -use std::fmt::{Display, Formatter}; -use std::ops::{Deref, DerefMut}; -use std::{ - collections::HashMap, - iter, - ops::Range, - path::{Path, PathBuf}, - sync::Arc, -}; - -/// A temporal graph composed of multiple shards. -/// -/// This is the public facing struct used to create a temporal graph, add vertices and edges, -/// create windows, and query the graph with a variety of algorithms. -/// It is a wrapper around a set of shards, which are the actual graph data structures. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct InternalGraph { - /// The number of shards in the graph. - pub(crate) nr_shards: usize, - /// A vector of `TGraphShard` representing the shards in the graph. - pub(crate) shards: Vec>, - /// Translates layer names to layer ids - pub(crate) layer_ids: Arc>>, -} - -#[repr(transparent)] -#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)] -pub struct Graph(Arc); - -impl Display for Graph { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl Deref for Graph { - type Target = Arc; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for Graph { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl WrappedGraph for Graph { - type Internal = InternalGraph; - - fn as_graph(&self) -> &InternalGraph { - &self.0 - } -} - -impl Graph { - /// Create a new graph with the specified number of shards - /// - /// # Arguments - /// - /// * `nr_shards` - The number of shards - /// - /// # Returns - /// - /// A raphtory graph - /// - /// # Example - /// - /// ``` - /// use raphtory::db::graph::Graph; - /// let g = Graph::new(4); - /// ``` - pub fn new(nr_shards: usize) -> Self { - Self(Arc::new(InternalGraph::new(nr_shards))) - } - - pub(crate) fn new_from_frozen( - nr_shards: usize, - shards: Vec>, - layer_ids: Arc>>, - ) -> Self { - Self(Arc::new(InternalGraph { - nr_shards, - shards, - layer_ids, - })) - } - - /// Load a graph from a directory - /// - /// # Arguments - /// - /// * `path` - The path to the directory - /// - /// # Returns - /// - /// A raphtory graph - /// - /// # Example - /// - /// ``` - /// use raphtory::db::graph::InternalGraph; - /// // let g = Graph::load_from_file("path/to/graph"); - /// ``` - pub fn load_from_file>(path: P) -> Result> { - Ok(Self(Arc::new(InternalGraph::load_from_file(path)?))) - } - - /// Freezes the current mutable graph into an immutable graph. - /// - /// This removes the internal locks, allowing the graph to be queried in - /// a read-only fashion. - /// - /// # Returns - /// - /// An `ImmutableGraph` which is an immutable copy of the current graph. - /// - /// # Example - /// ``` - /// use raphtory::db::view_api::*; - /// use raphtory::db::graph::Graph; - /// - /// let mut mutable_graph = Graph::new(1); - /// // ... add vertices and edges to the graph - /// - /// // Freeze the mutable graph into an immutable graph - /// let immutable_graph = mutable_graph.freeze(); - /// ``` - pub fn freeze(self) -> ImmutableGraph { - ImmutableGraph { - nr_shards: self.nr_shards, - shards: self.shards.iter().map(|s| s.freeze()).collect_vec(), - layer_ids: Arc::new(self.layer_ids.read().clone()), - } - } - - pub fn as_arc(&self) -> Arc { - self.0.clone() - } -} - -impl Default for InternalGraph { - fn default() -> Self { - InternalGraph::new(1) - } -} - -impl Display for InternalGraph { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!( - f, - "Graph(num_vertices={}, num_edges={})", - self.num_vertices(), - self.num_edges() - ) - } -} - -impl PartialEq for InternalGraph { - fn eq(&self, other: &G) -> bool { - if self.num_vertices() == other.num_vertices() && self.num_edges() == other.num_edges() { - self.vertices().id().all(|v| other.has_vertex(v)) && // all vertices exist in other - self.edges().explode().count() == other.edges().explode().count() && // same number of exploded edges - self.edges().explode().all(|e| { // all exploded edges exist in other - other - .edge(e.src().id(), e.dst().id(), None) - .filter(|ee| ee.active(e.time().expect("exploded"))) - .is_some() - }) - } else { - false - } - } -} - -impl GraphViewInternalOps for InternalGraph { - fn local_vertex(&self, v: VertexRef) -> Option { - self.get_shard_from_v(v).local_vertex(v) - } - - fn local_vertex_window( - &self, - v: VertexRef, - t_start: i64, - t_end: i64, - ) -> Option { - self.get_shard_from_v(v) - .local_vertex_window(v, t_start..t_end) - } - - /// Return all the layer ids, included the id of the default layer, 0 - fn get_unique_layers_internal(&self) -> Vec { - Box::new(iter::once(0).chain(self.layer_ids.read().values().copied())).collect_vec() - } - - fn get_layer_name_by_id(&self, layer_id: usize) -> String { - let layer_ids = self.layer_ids.read(); - layer_ids - .iter() - .find_map(|(name, &id)| (layer_id == id).then_some(name)) - .expect(&format!("layer id '{layer_id}' doesn't exist")) - .to_string() - } - - fn get_layer(&self, key: Option<&str>) -> Option { - match key { - None => Some(0), - Some(key) => self.layer_ids.read().get(key).copied(), - } - } - - fn view_start(&self) -> Option { - self.earliest_time_global() - } - - fn view_end(&self) -> Option { - self.latest_time_global().map(|t| t + 1) // so it is exclusive - } - - fn earliest_time_global(&self) -> Option { - let min_from_shards = self.shards.iter().map(|shard| shard.earliest_time()).min(); - min_from_shards.filter(|&min| min != i64::MAX) - } - - fn earliest_time_window(&self, t_start: i64, t_end: i64) -> Option { - //FIXME: this is not correct, should actually be the earliest activity in window - let earliest = self.earliest_time_global()?; - if earliest > t_end { - None - } else { - Some(max(earliest, t_start)) - } - } - - fn latest_time_global(&self) -> Option { - let max_from_shards = self.shards.iter().map(|shard| shard.latest_time()).max(); - max_from_shards.filter(|&max| max != i64::MIN) - } - - fn latest_time_window(&self, t_start: i64, t_end: i64) -> Option { - //FIXME: this is not correct, should actually be the latest activity in window - let latest = self.latest_time_global()?; - if latest < t_start { - None - } else { - Some(min(latest, t_end)) - } - } - - fn vertices_len(&self) -> usize { - self.shards.iter().map(|shard| shard.len()).sum() - } - - fn vertices_len_window(&self, t_start: i64, t_end: i64) -> usize { - //FIXME: This nees to be optimised ideally - self.shards - .iter() - .map(|shard| shard.vertices_window(t_start..t_end).count()) - .sum() - } - - fn edges_len(&self, layer: Option) -> usize { - let vs: Vec = self - .shards - .iter() - .map(|shard| shard.out_edges_len(layer)) - .collect(); - vs.iter().sum() - } - - fn edges_len_window(&self, t_start: i64, t_end: i64, layer: Option) -> usize { - self.shards - .iter() - .map(|shard| shard.out_edges_len_window(&(t_start..t_end), layer)) - .sum() - } - - fn has_edge_ref(&self, src: VertexRef, dst: VertexRef, layer: usize) -> bool { - let (shard, src, dst) = self.localise_edge(src, dst); - self.shards[shard].has_edge(src, dst, layer) - } - - fn has_edge_ref_window( - &self, - src: VertexRef, - dst: VertexRef, - t_start: i64, - t_end: i64, - layer: usize, - ) -> bool { - let (shard, src, dst) = self.localise_edge(src, dst); - self.shards[shard].has_edge_window(src, dst, t_start..t_end, layer) - } - - fn has_vertex_ref(&self, v: VertexRef) -> bool { - self.get_shard_from_v(v).has_vertex(v) - } - - fn has_vertex_ref_window(&self, v: VertexRef, t_start: i64, t_end: i64) -> bool { - self.get_shard_from_v(v) - .has_vertex_window(v, t_start..t_end) - } - - fn degree(&self, v: LocalVertexRef, d: Direction, layer: Option) -> usize { - self.get_shard_from_local_v(v).degree(v, d, layer) - } - - fn degree_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - d: Direction, - layer: Option, - ) -> usize { - self.get_shard_from_local_v(v) - .degree_window(v, t_start..t_end, d, layer) - } - - fn vertex_ref(&self, v: u64) -> Option { - self.get_shard_from_id(v).vertex(v) - } - - fn vertex_id(&self, v: LocalVertexRef) -> u64 { - self.shards[v.shard_id].vertex_id(v) - } - - fn vertex_ref_window(&self, v: u64, t_start: i64, t_end: i64) -> Option { - self.get_shard_from_id(v).vertex_window(v, t_start..t_end) - } - - fn vertex_earliest_time(&self, v: LocalVertexRef) -> Option { - self.get_shard_from_local_v(v).vertex_earliest_time(v) - } - - fn vertex_earliest_time_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - ) -> Option { - self.get_shard_from_local_v(v) - .vertex_earliest_time_window(v, t_start..t_end) - } - - fn vertex_latest_time(&self, v: LocalVertexRef) -> Option { - self.get_shard_from_local_v(v).vertex_latest_time(v) - } - - fn vertex_latest_time_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - ) -> Option { - self.get_shard_from_local_v(v) - .vertex_latest_time_window(v, t_start..t_end) - } - - fn vertex_refs(&self) -> Box + Send> { - let shards = self.shards.clone(); - Box::new(shards.into_iter().flat_map(|s| s.vertices())) - } - - fn vertex_refs_window( - &self, - t_start: i64, - t_end: i64, - ) -> Box + Send> { - let shards = self.shards.clone(); - Box::new( - shards - .into_iter() - .flat_map(move |s| s.vertices_window(t_start..t_end)), - ) - } - - fn vertex_refs_shard(&self, shard: usize) -> Box + Send> { - let shard = self.shards[shard].clone(); - Box::new(shard.vertices()) - } - - fn vertex_refs_window_shard( - &self, - shard: usize, - t_start: i64, - t_end: i64, - ) -> Box + Send> { - let shard = self.shards[shard].clone(); - Box::new(shard.vertices_window(t_start..t_end)) - } - - fn edge_ref(&self, src: VertexRef, dst: VertexRef, layer: usize) -> Option { - let (shard_id, src, dst) = self.localise_edge(src, dst); - self.shards[shard_id].edge(src, dst, layer) - } - - fn edge_ref_window( - &self, - src: VertexRef, - dst: VertexRef, - t_start: i64, - t_end: i64, - layer: usize, - ) -> Option { - let (shard_id, src, dst) = self.localise_edge(src, dst); - self.shards[shard_id].edge_window(src, dst, t_start..t_end, layer) - } - - fn edge_refs(&self, layer: Option) -> Box + Send> { - //FIXME: needs low-level primitive - let g = self.clone(); - match layer { - Some(layer) => Box::new( - self.vertex_refs() - .flat_map(move |v| g.vertex_edges(v, Direction::OUT, Some(layer))), - ), - None => Box::new( - self.vertex_refs() - .flat_map(move |v| g.vertex_edges(v, Direction::OUT, None)), - ), - } - } - - fn edge_refs_window( - &self, - t_start: i64, - t_end: i64, - layer: Option, - ) -> Box + Send> { - //FIXME: needs low-level primitive - let g = self.clone(); - Box::new( - self.vertex_refs() - .flat_map(move |v| g.vertex_edges_window(v, t_start, t_end, Direction::OUT, layer)), - ) - } - - fn vertex_edges( - &self, - v: LocalVertexRef, - d: Direction, - layer: Option, - ) -> Box + Send> { - Box::new(self.get_shard_from_local_v(v).vertex_edges(v, d, layer)) - } - - fn vertex_edges_t( - &self, - v: LocalVertexRef, - d: Direction, - layer: Option, - ) -> Box + Send> { - // FIXME: missing low-level implementation - Box::new(self.get_shard_from_local_v(v).vertex_edges_window_t( - v, - i64::MIN..i64::MAX, - d, - layer, - )) - } - - fn vertex_edges_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - d: Direction, - layer: Option, - ) -> Box + Send> { - Box::new( - self.get_shard_from_local_v(v) - .vertex_edges_window(v, t_start..t_end, d, layer), - ) - } - - fn vertex_edges_window_t( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - d: Direction, - layer: Option, - ) -> Box + Send> { - Box::new( - self.get_shard_from_local_v(v) - .vertex_edges_window_t(v, t_start..t_end, d, layer), - ) - } - - fn neighbours( - &self, - v: LocalVertexRef, - d: Direction, - layer: Option, - ) -> Box + Send> { - Box::new(self.get_shard_from_local_v(v).neighbours(v, d, layer)) - } - - fn neighbours_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - d: Direction, - layer: Option, - ) -> Box + Send> { - Box::new( - self.get_shard_from_local_v(v) - .neighbours_window(v, t_start..t_end, d, layer), - ) - } - - fn static_vertex_prop(&self, v: LocalVertexRef, name: String) -> Option { - self.get_shard_from_local_v(v).static_vertex_prop(v, name) - } - - fn static_vertex_prop_names(&self, v: LocalVertexRef) -> Vec { - self.get_shard_from_local_v(v).static_vertex_prop_names(v) - } - - fn temporal_vertex_prop_names(&self, v: LocalVertexRef) -> Vec { - self.get_shard_from_local_v(v).temporal_vertex_prop_names(v) - } - - fn temporal_vertex_prop_vec(&self, v: LocalVertexRef, name: String) -> Vec<(i64, Prop)> { - self.get_shard_from_local_v(v) - .temporal_vertex_prop_vec(v, name) - } - - fn vertex_timestamps(&self, v: LocalVertexRef) -> Vec { - self.get_shard_from_local_v(v).vertex_timestamps(v) - } - - fn vertex_timestamps_window(&self, v: LocalVertexRef, t_start: i64, t_end: i64) -> Vec { - self.get_shard_from_local_v(v) - .vertex_timestamps_window(v, t_start..t_end) - } - - fn temporal_vertex_prop_vec_window( - &self, - v: LocalVertexRef, - name: String, - t_start: i64, - t_end: i64, - ) -> Vec<(i64, Prop)> { - self.get_shard_from_local_v(v) - .temporal_vertex_prop_vec_window(v, name, t_start..t_end) - } - - fn temporal_vertex_props(&self, v: LocalVertexRef) -> HashMap> { - self.get_shard_from_local_v(v).temporal_vertex_props(v) - } - - fn temporal_vertex_props_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - ) -> HashMap> { - self.get_shard_from_local_v(v) - .temporal_vertex_props_window(v, t_start..t_end) - } - - fn static_edge_prop(&self, e: EdgeRef, name: String) -> Option { - self.get_shard_from_e(e).static_edge_prop(e, name) - } - - fn static_edge_prop_names(&self, e: EdgeRef) -> Vec { - self.get_shard_from_e(e).static_edge_prop_names(e) - } - - fn temporal_edge_prop_names(&self, e: EdgeRef) -> Vec { - self.get_shard_from_e(e).temporal_edge_prop_names(e) - } - - fn temporal_edge_props_vec(&self, e: EdgeRef, name: String) -> Vec<(i64, Prop)> { - self.get_shard_from_e(e).temporal_edge_prop_vec(e, name) - } - - fn temporal_edge_props_vec_window( - &self, - e: EdgeRef, - name: String, - t_start: i64, - t_end: i64, - ) -> Vec<(i64, Prop)> { - self.get_shard_from_e(e) - .temporal_edge_props_vec_window(e, name, t_start..t_end) - } - - fn edge_timestamps(&self, e: EdgeRef, window: Option>) -> Vec { - self.get_shard_from_e(e).edge_timestamps(e, window) - } - - fn temporal_edge_props(&self, e: EdgeRef) -> HashMap> { - self.get_shard_from_e(e).temporal_edge_props(e) - } - - fn temporal_edge_props_window( - &self, - e: EdgeRef, - t_start: i64, - t_end: i64, - ) -> HashMap> { - self.get_shard_from_e(e) - .temporal_edge_props_window(e, t_start..t_end) - } - - fn num_shards(&self) -> usize { - self.nr_shards - } -} - -/// The implementation of a temporal graph composed of multiple shards. -impl InternalGraph { - /// Freezes the current mutable graph into an immutable graph. - /// - /// This removes the internal locks, allowing the graph to be queried in - /// a read-only fashion. - /// - /// # Returns - /// - /// An `ImmutableGraph` which is an immutable copy of the current graph. - /// - /// # Example - /// ``` - /// use raphtory::db::view_api::*; - /// use raphtory::db::graph::Graph; - /// - /// let mut mutable_graph = Graph::new(1); - /// // ... add vertices and edges to the graph - /// - /// // Freeze the mutable graph into an immutable graph - /// let immutable_graph = mutable_graph.freeze(); - /// ``` - pub fn freeze(self) -> ImmutableGraph { - ImmutableGraph { - nr_shards: self.nr_shards, - shards: self.shards.iter().map(|s| s.freeze()).collect_vec(), - layer_ids: Arc::new(self.layer_ids.read().clone()), - } - } - - fn localise_edge(&self, src: VertexRef, dst: VertexRef) -> (usize, VertexRef, VertexRef) { - match src { - VertexRef::Local(local_src) => match dst { - VertexRef::Local(local_dst) => { - if local_src.shard_id == local_dst.shard_id { - (local_src.shard_id, src, dst) - } else { - ( - local_src.shard_id, - src, - VertexRef::Remote(self.vertex_id(local_dst)), - ) - } - } - VertexRef::Remote(_) => (local_src.shard_id, src, dst), - }, - VertexRef::Remote(gid) => match dst { - VertexRef::Local(local_dst) => (local_dst.shard_id, src, dst), - VertexRef::Remote(_) => (self.shard_id(gid), src, dst), - }, - } - } - - /// Get the shard id from a global vertex id - /// - /// # Arguments - /// - /// * `g_id` - The global vertex id - /// - /// # Returns - /// - /// The shard id - fn shard_id(&self, g_id: u64) -> usize { - utils::get_shard_id_from_global_vid(g_id, self.nr_shards) - } - - /// Get the shard from a global vertex id - /// - /// # Arguments - /// - /// * `g_id` - The global vertex id - /// - /// # Returns - /// - /// The shard reference - fn get_shard_from_id(&self, g_id: u64) -> &TGraphShard { - &self.shards[self.shard_id(g_id)] - } - - /// Get the shard from a vertex reference - /// - /// # Arguments - /// - /// * `g_id` - The global vertex id - /// - /// # Returns - /// - /// The shard reference - fn get_shard_from_v(&self, v: VertexRef) -> &TGraphShard { - match v { - VertexRef::Local(v) => self.get_shard_from_local_v(v), - VertexRef::Remote(g_id) => self.get_shard_from_id(g_id), - } - } - - #[inline(always)] - fn get_shard_from_local_v(&self, v: LocalVertexRef) -> &TGraphShard { - &self.shards[v.shard_id] - } - - /// Get the shard from an edge reference - /// - /// # Arguments - /// - /// * `e` - The edge reference - /// - /// # Returns - /// - /// The shard reference - fn get_shard_from_e(&self, e: EdgeRef) -> &TGraphShard { - &self.shards[e.shard()] - } - - /// Create a new graph with the specified number of shards - /// - /// # Arguments - /// - /// * `nr_shards` - The number of shards - /// - /// # Returns - /// - /// A raphtory graph - /// - /// # Example - /// - /// ``` - /// use raphtory::db::graph::Graph; - /// let g = Graph::new(4); - /// ``` - pub fn new(nr_shards: usize) -> Self { - InternalGraph { - nr_shards, - shards: (0..nr_shards).map(|i| TGraphShard::new(i)).collect(), - layer_ids: Default::default(), - } - } - - /// Load a graph from a directory - /// - /// # Arguments - /// - /// * `path` - The path to the directory - /// - /// # Returns - /// - /// A raphtory graph - /// - /// # Example - /// - /// ``` - /// use raphtory::db::graph::InternalGraph; - /// // let g = Graph::load_from_file("path/to/graph"); - /// ``` - pub fn load_from_file>(path: P) -> Result> { - // use BufReader for better performance - - //TODO turn to logging? - println!("loading from {:?}", path.as_ref()); - let mut p = PathBuf::from(path.as_ref()); - p.push("graphdb_nr_shards"); - - let f = std::fs::File::open(p).unwrap(); - let mut reader = std::io::BufReader::new(f); - let (nr_shards, layer_ids) = bincode::deserialize_from(&mut reader)?; - - let mut shard_paths = vec![]; - for i in 0..nr_shards { - let mut p = PathBuf::from(path.as_ref()); - p.push(format!("shard_{}", i)); - shard_paths.push((i, p)); - } - let mut shards = shard_paths - .par_iter() - .map(|(i, path)| { - let shard = TGraphShard::load_from_file(path)?; - Ok((*i, shard)) - }) - .collect::, Box>>()?; - - shards.sort_by_cached_key(|(i, _)| *i); - - let shards = shards.into_iter().map(|(_, shard)| shard).collect(); - Ok(InternalGraph { - nr_shards, - shards, - layer_ids, - }) //TODO I need to put in the actual values here - } - - /// Save a graph to a directory - /// - /// # Arguments - /// - /// * `path` - The path to the directory - /// - /// # Returns - /// - /// A raphtory graph - /// - /// # Example - /// - /// ``` - /// use raphtory::db::graph::InternalGraph; - /// use std::fs::File; - /// let g = InternalGraph::new(4); - /// g.add_vertex(1, 1, &vec![]); - /// // g.save_to_file("path_str"); - /// ``` - pub fn save_to_file>(&self, path: P) -> Result<(), Box> { - // write each shard to a different file - - // crate directory path if it doesn't exist - std::fs::create_dir_all(path.as_ref())?; - - let mut shard_paths = vec![]; - for i in 0..self.nr_shards { - let mut p = PathBuf::from(path.as_ref()); - p.push(format!("shard_{}", i)); - //TODO turn to logging? - //println!("saving shard {} to {:?}", i, p); - shard_paths.push((i, p)); - } - shard_paths - .par_iter() - .try_for_each(|(i, path)| self.shards[*i].save_to_file(path))?; - - let mut p = PathBuf::from(path.as_ref()); - p.push("graphdb_nr_shards"); - - let f = std::fs::File::create(p)?; - let writer = std::io::BufWriter::new(f); - bincode::serialize_into(writer, &(self.nr_shards, self.layer_ids.clone()))?; - Ok(()) - } - - // TODO: Probably add vector reference here like add - /// Add a vertex to the graph - /// - /// # Arguments - /// - /// * `t` - The time - /// * `v` - The vertex (can be a string or integer) - /// * `props` - The properties of the vertex - /// - /// # Returns - /// - /// A result containing the vertex id - /// - /// # Example - /// - /// ``` - /// use raphtory::db::graph::InternalGraph; - /// let g = InternalGraph::new(1); - /// let v = g.add_vertex(0, "Alice", &vec![]); - /// let v = g.add_vertex(0, 5, &vec![]); - /// ``` - pub fn add_vertex( - &self, - t: T, - v: V, - props: &Vec<(String, Prop)>, - ) -> Result<(), GraphError> { - let shard_id = utils::get_shard_id_from_global_vid(v.id(), self.nr_shards); - self.shards[shard_id].add_vertex(t.try_into_time()?, v, props) - } - - pub fn add_vertex_with_custom_time_format( - &self, - t: &str, - fmt: &str, - v: V, - props: &Vec<(String, Prop)>, - ) -> Result<(), GraphError> { - let time: i64 = t.parse_time(fmt)?; - self.add_vertex(time, v, props) - } - - /// Adds properties to the given input vertex. - /// - /// # Arguments - /// - /// * `v` - A vertex - /// * `data` - A vector of tuples containing the property name and value pairs to add to the vertex. - /// - /// # Example - /// - /// ``` - /// use raphtory::db::graph::InternalGraph; - /// use raphtory::core::Prop; - /// let graph = InternalGraph::new(1); - /// graph.add_vertex(0, "Alice", &vec![]); - /// let properties = vec![("color".to_owned(), Prop::Str("blue".to_owned())), ("weight".to_owned(), Prop::I64(11))]; - /// let result = graph.add_vertex_properties("Alice", &properties); - /// ``` - pub fn add_vertex_properties( - &self, - v: V, - data: &Vec<(String, Prop)>, - ) -> Result<(), GraphError> { - let shard_id = utils::get_shard_id_from_global_vid(v.id(), self.nr_shards); - self.shards[shard_id].add_vertex_properties(v.id(), data) - } - - // TODO: Vertex.name which gets ._id property else numba as string - /// Adds an edge between the source and destination vertices with the given timestamp and properties. - /// - /// # Arguments - /// - /// * `t` - The timestamp of the edge. - /// * `src` - An instance of `T` that implements the `InputVertex` trait representing the source vertex. - /// * `dst` - An instance of `T` that implements the `InputVertex` trait representing the destination vertex. - /// * `props` - A vector of tuples containing the property name and value pairs to add to the edge. - /// - /// # Example - /// - /// ``` - /// use raphtory::db::graph::InternalGraph; - /// - /// let graph = InternalGraph::new(1); - /// graph.add_vertex(1, "Alice", &vec![]); - /// graph.add_vertex(2, "Bob", &vec![]); - /// graph.add_edge(3, "Alice", "Bob", &vec![], None); - /// ``` - pub fn add_edge( - &self, - t: T, - src: V, - dst: V, - props: &Vec<(String, Prop)>, - layer: Option<&str>, - ) -> Result<(), GraphError> { - let time = t.try_into_time()?; - let src_shard_id = utils::get_shard_id_from_global_vid(src.id(), self.nr_shards); - let dst_shard_id = utils::get_shard_id_from_global_vid(dst.id(), self.nr_shards); - - let layer_id = self.get_or_allocate_layer(layer); - - if src_shard_id == dst_shard_id { - self.shards[src_shard_id].add_edge(time, src, dst, props, layer_id) - } else { - // FIXME these are sort of connected, we need to hold both locks for - // the src partition and dst partition to add a remote edge between both - self.shards[src_shard_id].add_edge_remote_out( - time, - src.clone(), - dst.clone(), - props, - layer_id, - )?; - self.shards[dst_shard_id].add_edge_remote_into(time, src, dst, props, layer_id)?; - Ok(()) - } - } - - pub fn add_edge_with_custom_time_format( - &self, - t: &str, - fmt: &str, - src: V, - dst: V, - props: &Vec<(String, Prop)>, - layer: Option<&str>, - ) -> Result<(), GraphError> { - let time: i64 = t.parse_time(fmt)?; - self.add_edge(time, src, dst, props, layer) - } - - /// Adds properties to an existing edge between a source and destination vertices - /// - /// # Arguments - /// - /// * `src` - An instance of `T` that implements the `InputVertex` trait representing the source vertex. - /// * `dst` - An instance of `T` that implements the `InputVertex` trait representing the destination vertex. - /// * `props` - A vector of tuples containing the property name and value pairs to add to the edge. - /// - /// # Example - /// - /// ``` - /// use raphtory::db::graph::InternalGraph; - /// use raphtory::core::Prop; - /// let graph = InternalGraph::new(1); - /// graph.add_vertex(1, "Alice", &vec![]); - /// graph.add_vertex(2, "Bob", &vec![]); - /// graph.add_edge(3, "Alice", "Bob", &vec![], None); - /// let properties = vec![("price".to_owned(), Prop::I64(100))]; - /// let result = graph.add_edge_properties("Alice", "Bob", &properties, None); - /// ``` - pub fn add_edge_properties( - &self, - src: V, - dst: V, - props: &Vec<(String, Prop)>, - layer: Option<&str>, - ) -> Result<(), GraphError> { - let layer_id = self.get_layer(layer).unwrap(); // FIXME: bubble up instead - - // TODO: we don't add properties to dst shard, but may need to depending on the plans - self.get_shard_from_id(src.id()) - .add_edge_properties(src.id(), dst.id(), props, layer_id) - } - - fn get_or_allocate_layer(&self, key: Option<&str>) -> usize { - self.get_layer(key).unwrap_or_else(|| { - let mut layer_ids = self.layer_ids.write(); - let layer_id = layer_ids.len() + 1; // default layer not included in the hashmap - layer_ids.insert(key.unwrap().to_string(), layer_id); - for shard in &self.shards { - shard.allocate_layer(layer_id).unwrap() // FIXME: bubble up error - } - layer_id - }) - } -} - -#[cfg(test)] -mod db_tests { - use super::*; - use crate::db::edge::EdgeView; - use crate::db::path::PathFromVertex; - use crate::db::view_api::edge::EdgeViewOps; - use crate::db::view_api::layer::LayerOps; - use crate::graphgen::random_attachment::random_attachment; - use itertools::Itertools; - use std::fs; - use std::sync::Arc; - use tempdir::TempDir; - use uuid::Uuid; - - #[test] - fn cloning_vec() { - let mut vs = vec![]; - for i in 0..10 { - vs.push(Arc::new(i)) - } - let should_be_10: usize = vs.iter().map(Arc::strong_count).sum(); - assert_eq!(should_be_10, 10); - - let vs2 = vs.clone(); - - let should_be_10: usize = vs2.iter().map(Arc::strong_count).sum(); - assert_eq!(should_be_10, 20) - } - - #[quickcheck] - fn add_vertex_grows_graph_len(vs: Vec<(i64, u64)>) { - let g = Graph::new(2); - - let expected_len = vs.iter().map(|(_, v)| v).sorted().dedup().count(); - for (t, v) in vs { - g.add_vertex(t, v, &vec![]) - .map_err(|err| println!("{:?}", err)) - .ok(); - } - - assert_eq!(g.num_vertices(), expected_len) - } - - #[quickcheck] - fn add_edge_grows_graph_edge_len(edges: Vec<(i64, u64, u64)>) { - let nr_shards: usize = 2; - - let g = InternalGraph::new(nr_shards); - - let unique_vertices_count = edges - .iter() - .flat_map(|(_, src, dst)| vec![src, dst]) - .sorted() - .dedup() - .count(); - - let unique_edge_count = edges - .iter() - .map(|(_, src, dst)| (src, dst)) - .unique() - .count(); - - for (t, src, dst) in edges { - g.add_edge(t, src, dst, &vec![], None).unwrap(); - } - - assert_eq!(g.num_vertices(), unique_vertices_count); - assert_eq!(g.num_edges(), unique_edge_count); - } - - #[quickcheck] - fn add_edge_works(edges: Vec<(i64, u64, u64)>) -> bool { - let g = InternalGraph::new(3); - for &(t, src, dst) in edges.iter() { - g.add_edge(t, src, dst, &vec![], None).unwrap(); - } - - edges - .iter() - .all(|&(_, src, dst)| g.has_edge(src, dst, None)) - } - - #[quickcheck] - fn get_edge_works(edges: Vec<(i64, u64, u64)>) -> bool { - let g = InternalGraph::new(100); - for &(t, src, dst) in edges.iter() { - g.add_edge(t, src, dst, &vec![], None).unwrap(); - } - - edges - .iter() - .all(|&(_, src, dst)| g.edge(src, dst, None).is_some()) - } - - #[test] - fn graph_save_to_load_from_file() { - let vs = vec![ - (1, 1, 2), - (2, 1, 3), - (-1, 2, 1), - (0, 1, 1), - (7, 3, 2), - (1, 1, 1), - ]; - - let g = InternalGraph::new(2); - - for (t, src, dst) in &vs { - g.add_edge(*t, *src, *dst, &vec![], None).unwrap(); - } - - let rand_dir = Uuid::new_v4(); - let tmp_raphtory_path: TempDir = TempDir::new("raphtory").unwrap(); - let shards_path = - format!("{:?}/{}", tmp_raphtory_path.path().display(), rand_dir).replace('\"', ""); - - println!("shards_path: {}", shards_path); - - // Save to files - let mut expected = vec![ - format!("{}/shard_1", shards_path), - format!("{}/shard_0", shards_path), - format!("{}/graphdb_nr_shards", shards_path), - ] - .iter() - .map(Path::new) - .map(PathBuf::from) - .collect::>(); - - expected.sort(); - - match g.save_to_file(&shards_path) { - Ok(()) => { - let mut actual = fs::read_dir(&shards_path) - .unwrap() - .map(|f| f.unwrap().path()) - .collect::>(); - - actual.sort(); - - assert_eq!(actual, expected); - } - Err(e) => panic!("{e}"), - } - - // Load from files - match InternalGraph::load_from_file(Path::new(&shards_path)) { - Ok(g) => { - assert!(g.has_vertex_ref(1.into())); - assert_eq!(g.nr_shards, 2); - } - Err(e) => panic!("{e}"), - } - - let _ = tmp_raphtory_path.close(); - } - - #[test] - fn has_edge() { - let g = InternalGraph::new(2); - g.add_edge(1, 7, 8, &vec![], None).unwrap(); - - assert!(!g.has_edge(8, 7, None)); - assert!(g.has_edge(7, 8, None)); - - g.add_edge(1, 7, 9, &vec![], None).unwrap(); - - assert!(!g.has_edge(9, 7, None)); - assert!(g.has_edge(7, 9, None)); - - g.add_edge(2, "haaroon", "northLondon", &vec![], None) - .unwrap(); - assert!(g.has_edge("haaroon", "northLondon", None)); - } - - #[test] - fn graph_edge() { - let g = InternalGraph::new(2); - let es = vec![ - (1, 1, 2), - (2, 1, 3), - (-1, 2, 1), - (0, 1, 1), - (7, 3, 2), - (1, 1, 1), - ]; - for (t, src, dst) in es { - g.add_edge(t, src, dst, &vec![], None).unwrap() - } - - let e = g - .edge_ref_window(1.into(), 3.into(), i64::MIN, i64::MAX, 0) - .unwrap(); - assert_eq!(g.vertex_id(g.localise_vertex_unchecked(e.src())), 1u64); - assert_eq!(g.vertex_id(g.localise_vertex_unchecked(e.dst())), 3u64); - } - - #[test] - fn graph_degree_window() { - let vs = vec![ - (1, 1, 2), - (2, 1, 3), - (-1, 2, 1), - (0, 1, 1), - (7, 3, 2), - (1, 1, 1), - ]; - - let g = InternalGraph::new(1); - - for (t, src, dst) in &vs { - g.add_edge(*t, *src, *dst, &vec![], None).unwrap(); - } - - let expected = vec![(2, 3, 1), (1, 0, 0), (1, 0, 0)]; - let actual = (1..=3) - .map(|i| { - let i = g.vertex_ref(i).unwrap(); - ( - g.degree_window(i, -1, 7, Direction::IN, None), - g.degree_window(i, 1, 7, Direction::OUT, None), - g.degree_window(i, 0, 1, Direction::BOTH, None), - ) - }) - .collect::>(); - - assert_eq!(actual, expected); - - // Check results from multiple graphs with different number of shards - let g = InternalGraph::new(3); - - for (t, src, dst) in &vs { - g.add_edge(*t, *src, *dst, &vec![], None).unwrap(); - } - - let expected = (1..=3) - .map(|i| { - let i = g.vertex_ref(i).unwrap(); - ( - g.degree_window(i, -1, 7, Direction::IN, None), - g.degree_window(i, 1, 7, Direction::OUT, None), - g.degree_window(i, 0, 1, Direction::BOTH, None), - ) - }) - .collect::>(); - - assert_eq!(actual, expected); - } - - #[test] - fn graph_edges_window() { - let vs = vec![ - (1, 1, 2), - (2, 1, 3), - (-1, 2, 1), - (0, 1, 1), - (7, 3, 2), - (1, 1, 1), - ]; - - let g = InternalGraph::new(1); - - for (t, src, dst) in &vs { - g.add_edge(*t, *src, *dst, &vec![], None).unwrap(); - } - - let expected = vec![(2, 3, 2), (1, 0, 0), (1, 0, 0)]; - let actual = (1..=3) - .map(|i| { - let i = g.vertex_ref(i).unwrap(); - ( - g.vertex_edges_window(i, -1, 7, Direction::IN, None) - .collect::>() - .len(), - g.vertex_edges_window(i, 1, 7, Direction::OUT, None) - .collect::>() - .len(), - g.vertex_edges_window(i, 0, 1, Direction::BOTH, None) - .collect::>() - .len(), - ) - }) - .collect::>(); - - assert_eq!(actual, expected); - - // Check results from multiple graphs with different number of shards - let g = InternalGraph::new(10); - - for (t, src, dst) in &vs { - g.add_edge(*t, *src, *dst, &vec![], None).unwrap(); - } - - let expected = (1..=3) - .map(|i| { - let i = g.vertex_ref(i).unwrap(); - ( - g.vertex_edges_window(i, -1, 7, Direction::IN, None) - .collect::>() - .len(), - g.vertex_edges_window(i, 1, 7, Direction::OUT, None) - .collect::>() - .len(), - g.vertex_edges_window(i, 0, 1, Direction::BOTH, None) - .collect::>() - .len(), - ) - }) - .collect::>(); - - assert_eq!(actual, expected); - } - - #[test] - fn graph_edges_window_t() { - let vs = vec![ - (1, 1, 2), - (2, 1, 3), - (-1, 2, 1), - (0, 1, 1), - (7, 3, 2), - (1, 1, 1), - ]; - - let g = InternalGraph::new(1); - - for (t, src, dst) in &vs { - g.add_edge(*t, *src, *dst, &vec![], None).unwrap(); - } - - let in_actual = (1..=3) - .map(|i| { - let i = g.vertex_ref(i).unwrap(); - g.vertex_edges_window_t(i, -1, 7, Direction::IN, None) - .map(|e| e.time().unwrap()) - .sorted() // sorted by neighbour first and then time but neighbour order can be arbitrary so normalise - .collect::>() - }) - .collect::>(); - assert_eq!(vec![vec![-1, 0, 1], vec![1], vec![2]], in_actual); - - let out_actual = (1..=3) - .map(|i| { - let i = g.vertex_ref(i).unwrap(); - g.vertex_edges_window_t(i, 1, 7, Direction::OUT, None) - .map(|e| e.time().unwrap()) - .sorted() - .collect::>() - }) - .collect::>(); - assert_eq!(vec![vec![1, 1, 2], vec![], vec![]], out_actual); - - let both_actual = (1..=3) - .map(|i| { - let i = g.vertex_ref(i).unwrap(); - g.vertex_edges_window_t(i, 0, 1, Direction::BOTH, None) - .map(|e| e.time().unwrap()) - .sorted() - .collect::>() - }) - .collect::>(); - assert_eq!(vec![vec![0, 0], vec![], vec![]], both_actual); - - // Check results from multiple graphs with different number of shards - let g = InternalGraph::new(4); - - for (src, dst, t) in &vs { - g.add_edge(*src, *dst, *t, &vec![], None).unwrap(); - } - } - - #[test] - fn time_test() { - let g = Graph::new(4); - - assert_eq!(g.latest_time(), None); - assert_eq!(g.earliest_time(), None); - - g.add_vertex(5, 1, &vec![]) - .map_err(|err| println!("{:?}", err)) - .ok(); - - assert_eq!(g.latest_time(), Some(5)); - assert_eq!(g.earliest_time(), Some(5)); - - let g = Graph::new(4); - - g.add_edge(10, 1, 2, &vec![], None).unwrap(); - assert_eq!(g.latest_time(), Some(10)); - assert_eq!(g.earliest_time(), Some(10)); - - g.add_vertex(5, 1, &vec![]) - .map_err(|err| println!("{:?}", err)) - .ok(); - assert_eq!(g.latest_time(), Some(10)); - assert_eq!(g.earliest_time(), Some(5)); - - g.add_edge(20, 3, 4, &vec![], None).unwrap(); - assert_eq!(g.latest_time(), Some(20)); - assert_eq!(g.earliest_time(), Some(5)); - - random_attachment(&g, 100, 10); - assert_eq!(g.latest_time(), Some(126)); - assert_eq!(g.earliest_time(), Some(5)); - } - - #[test] - fn static_properties() { - let g = Graph::new(100); // big enough so all edges are very likely remote - g.add_edge(0, 11, 22, &vec![], None).unwrap(); - g.add_edge( - 0, - 11, - 11, - &vec![("temp".to_string(), Prop::Bool(true))], - None, - ) - .unwrap(); - g.add_edge(0, 22, 33, &vec![], None).unwrap(); - g.add_edge(0, 33, 11, &vec![], None).unwrap(); - g.add_vertex(0, 11, &vec![("temp".to_string(), Prop::Bool(true))]) - .unwrap(); - let v11 = g.vertex_ref(11).unwrap(); - let v22 = g.vertex_ref(22).unwrap(); - let v33 = g.vertex_ref(33).unwrap(); - let edge1111 = g.edge_ref(11.into(), 11.into(), 0).unwrap(); - let edge2233 = g.edge_ref(v22.into(), v33.into(), 0).unwrap(); - let edge3311 = g.edge_ref(v33.into(), v11.into(), 0).unwrap(); - - g.add_vertex_properties( - 11, - &vec![ - ("a".to_string(), Prop::U64(11)), - ("b".to_string(), Prop::I64(11)), - ], - ) - .unwrap(); - g.add_vertex_properties(11, &vec![("c".to_string(), Prop::U32(11))]) - .unwrap(); - g.add_vertex_properties(22, &vec![("b".to_string(), Prop::U64(22))]) - .unwrap(); - g.add_edge_properties(11, 11, &vec![("d".to_string(), Prop::U64(1111))], None) - .unwrap(); - g.add_edge_properties(33, 11, &vec![("a".to_string(), Prop::U64(3311))], None) - .unwrap(); - - assert_eq!(g.static_vertex_prop_names(v11), vec!["a", "b", "c"]); - assert_eq!(g.static_vertex_prop_names(v22), vec!["b"]); - assert!(g.static_vertex_prop_names(v33).is_empty()); - assert_eq!(g.static_edge_prop_names(edge1111), vec!["d"]); - assert_eq!(g.static_edge_prop_names(edge3311), vec!["a"]); - assert!(g.static_edge_prop_names(edge2233).is_empty()); - - assert_eq!( - g.static_vertex_prop(v11, "a".to_string()), - Some(Prop::U64(11)) - ); - assert_eq!( - g.static_vertex_prop(v11, "b".to_string()), - Some(Prop::I64(11)) - ); - assert_eq!( - g.static_vertex_prop(v11, "c".to_string()), - Some(Prop::U32(11)) - ); - assert_eq!( - g.static_vertex_prop(v22, "b".to_string()), - Some(Prop::U64(22)) - ); - assert_eq!(g.static_vertex_prop(v22, "a".to_string()), None); - assert_eq!( - g.static_edge_prop(edge1111, "d".to_string()), - Some(Prop::U64(1111)) - ); - assert_eq!( - g.static_edge_prop(edge3311, "a".to_string()), - Some(Prop::U64(3311)) - ); - assert_eq!(g.static_edge_prop(edge2233, "a".to_string()), None); - } - - #[test] - #[should_panic] - fn changing_property_type_for_vertex_panics() { - let g = InternalGraph::new(4); - g.add_vertex(0, 11, &vec![("test".to_string(), Prop::Bool(true))]) - .unwrap(); - g.add_vertex_properties(11, &vec![("test".to_string(), Prop::Bool(true))]) - .unwrap(); - } - - #[test] - #[should_panic] - fn changing_property_type_for_edge_panics() { - let g = InternalGraph::new(4); - g.add_edge( - 0, - 11, - 22, - &vec![("test".to_string(), Prop::Bool(true))], - None, - ) - .unwrap(); - g.add_edge_properties(11, 22, &vec![("test".to_string(), Prop::Bool(true))], None) - .unwrap(); - } - - #[test] - fn graph_neighbours_window() { - let vs = vec![ - (1, 1, 2), - (2, 1, 3), - (-1, 2, 1), - (0, 1, 1), - (7, 3, 2), - (1, 1, 1), - ]; - - let g = InternalGraph::new(2); - - for (t, src, dst) in &vs { - g.add_edge(*t, *src, *dst, &vec![], None).unwrap(); - } - - let local_1 = VertexRef::new_local(0, 1); - let remote_2 = VertexRef::Remote(2); - let local_3 = VertexRef::new_local(1, 1); - - let expected = [ - ( - vec![local_1, remote_2], - vec![local_1, local_3, remote_2], - vec![local_1], - ), - (vec![VertexRef::Remote(1)], vec![], vec![]), - (vec![local_1], vec![], vec![]), - ]; - let actual = (1..=3) - .map(|i| { - let i = g.vertex_ref(i).unwrap(); - ( - g.neighbours_window(i, -1, 7, Direction::IN, None) - .collect::>(), - g.neighbours_window(i, 1, 7, Direction::OUT, None) - .collect::>(), - g.neighbours_window(i, 0, 1, Direction::BOTH, None) - .collect::>(), - ) - }) - .collect::>(); - - assert_eq!(actual, expected); - } - - #[test] - fn test_time_range_on_empty_graph() { - let g = InternalGraph::new(1); - - let rolling = g.rolling(1, None).unwrap().collect_vec(); - assert!(rolling.is_empty()); - - let expanding = g.expanding(1).unwrap().collect_vec(); - assert!(expanding.is_empty()); - } - - #[test] - fn test_add_vertex_with_strings() { - let g = InternalGraph::new(1); - - g.add_vertex(0, "haaroon", &vec![]).unwrap(); - g.add_vertex(1, "hamza", &vec![]).unwrap(); - g.add_vertex(1, 831, &vec![]).unwrap(); - - assert!(g.has_vertex(831)); - assert!(g.has_vertex("haaroon")); - assert!(g.has_vertex("hamza")); - - assert_eq!(g.num_vertices(), 3); - } - - #[test] - fn layers() { - let g = InternalGraph::new(4); - g.add_edge(0, 11, 22, &vec![], None).unwrap(); - g.add_edge(0, 11, 33, &vec![], None).unwrap(); - g.add_edge(0, 33, 11, &vec![], None).unwrap(); - g.add_edge(0, 11, 22, &vec![], Some("layer1")).unwrap(); - g.add_edge(0, 11, 33, &vec![], Some("layer2")).unwrap(); - g.add_edge(0, 11, 44, &vec![], Some("layer2")).unwrap(); - - assert_eq!(g.has_edge(11, 22, None), true); - assert_eq!(g.has_edge(11, 44, None), false); - assert_eq!(g.has_edge(11, 22, Some("layer2")), false); - assert_eq!(g.has_edge(11, 44, Some("layer2")), true); - - assert!(g.edge(11, 22, None).is_some()); - assert!(g.edge(11, 44, None).is_none()); - assert!(g.edge(11, 22, Some("layer2")).is_none()); - assert!(g.edge(11, 44, Some("layer2")).is_some()); - - let dft_layer = g.default_layer(); - let layer1 = g.layer("layer1").unwrap(); - let layer2 = g.layer("layer2").unwrap(); - assert!(g.layer("missing layer").is_none()); - - assert_eq!(g.num_edges(), 4); - assert_eq!(dft_layer.num_edges(), 3); - assert_eq!(layer1.num_edges(), 1); - assert_eq!(layer2.num_edges(), 2); - - let vertex = g.vertex(11).unwrap(); - let vertex_dft = dft_layer.vertex(11).unwrap(); - let vertex1 = layer1.vertex(11).unwrap(); - let vertex2 = layer2.vertex(11).unwrap(); - - assert_eq!(vertex.degree(), 3); - assert_eq!(vertex_dft.degree(), 2); - assert_eq!(vertex1.degree(), 1); - assert_eq!(vertex2.degree(), 2); - - assert_eq!(vertex.out_degree(), 3); - assert_eq!(vertex_dft.out_degree(), 2); - assert_eq!(vertex1.out_degree(), 1); - assert_eq!(vertex2.out_degree(), 2); - - assert_eq!(vertex.in_degree(), 1); - assert_eq!(vertex_dft.in_degree(), 1); - assert_eq!(vertex1.in_degree(), 0); - assert_eq!(vertex2.in_degree(), 0); - - fn to_tuples>>( - edges: I, - ) -> Vec<(u64, u64)> { - edges - .map(|e| (e.src().id(), e.dst().id())) - .sorted() - .collect_vec() - } - - assert_eq!( - to_tuples(vertex.edges()), - vec![(11, 22), (11, 22), (11, 33), (11, 33), (11, 44), (33, 11)] - ); - assert_eq!( - to_tuples(vertex_dft.edges()), - vec![(11, 22), (11, 33), (33, 11)] - ); - assert_eq!(to_tuples(vertex1.edges()), vec![(11, 22)]); - assert_eq!(to_tuples(vertex2.edges()), vec![(11, 33), (11, 44)]); - - assert_eq!(to_tuples(vertex.in_edges()), vec![(33, 11)]); - assert_eq!(to_tuples(vertex_dft.in_edges()), vec![(33, 11)]); - assert_eq!(to_tuples(vertex1.in_edges()), vec![]); - assert_eq!(to_tuples(vertex2.in_edges()), vec![]); - - assert_eq!( - to_tuples(vertex.out_edges()), - vec![(11, 22), (11, 22), (11, 33), (11, 33), (11, 44)] - ); - assert_eq!(to_tuples(vertex_dft.out_edges()), vec![(11, 22), (11, 33)]); - assert_eq!(to_tuples(vertex1.out_edges()), vec![(11, 22)]); - assert_eq!(to_tuples(vertex2.out_edges()), vec![(11, 33), (11, 44)]); - - fn to_ids(neighbours: PathFromVertex) -> Vec { - neighbours.iter().map(|n| n.id()).sorted().collect_vec() - } - - assert_eq!(to_ids(vertex.neighbours()), vec![22, 33, 44]); - assert_eq!(to_ids(vertex_dft.neighbours()), vec![22, 33]); - assert_eq!(to_ids(vertex1.neighbours()), vec![22]); - assert_eq!(to_ids(vertex2.neighbours()), vec![33, 44]); - - assert_eq!(to_ids(vertex.out_neighbours()), vec![22, 33, 44]); - assert_eq!(to_ids(vertex_dft.out_neighbours()), vec![22, 33]); - assert_eq!(to_ids(vertex1.out_neighbours()), vec![22]); - assert_eq!(to_ids(vertex2.out_neighbours()), vec![33, 44]); - - assert_eq!(to_ids(vertex.in_neighbours()), vec![33]); - assert_eq!(to_ids(vertex_dft.in_neighbours()), vec![33]); - assert!(to_ids(vertex1.in_neighbours()).is_empty()); - assert!(to_ids(vertex2.in_neighbours()).is_empty()); - } - - #[test] - fn test_exploded_edge() { - let g = InternalGraph::new(1); - g.add_edge(0, 1, 2, &vec![("weight".to_string(), Prop::I64(1))], None) - .unwrap(); - g.add_edge(1, 1, 2, &vec![("weight".to_string(), Prop::I64(2))], None) - .unwrap(); - g.add_edge(2, 1, 2, &vec![("weight".to_string(), Prop::I64(3))], None) - .unwrap(); - - let exploded = g.edge(1, 2, None).unwrap().explode(); - - let res = exploded.map(|e| e.properties(false)).collect_vec(); - - let mut expected = Vec::new(); - for i in 1..4 { - let mut map = HashMap::new(); - map.insert("weight".to_string(), Prop::I64(i)); - expected.push(map); - } - - assert_eq!(res, expected); - - let e = g - .vertex(1) - .unwrap() - .edges() - .explode() - .map(|e| e.properties(false)) - .collect_vec(); - assert_eq!(e, expected); - } - - #[test] - fn test_edge_earliest_latest() { - let g = InternalGraph::new(1); - g.add_edge(0, 1, 2, &vec![], None).unwrap(); - g.add_edge(1, 1, 2, &vec![], None).unwrap(); - g.add_edge(2, 1, 2, &vec![], None).unwrap(); - g.add_edge(0, 1, 3, &vec![], None).unwrap(); - g.add_edge(1, 1, 3, &vec![], None).unwrap(); - g.add_edge(2, 1, 3, &vec![], None).unwrap(); - - let mut res = g.edge(1, 2, None).unwrap().earliest_time().unwrap(); - assert_eq!(res, 0); - - res = g.edge(1, 2, None).unwrap().latest_time().unwrap(); - assert_eq!(res, 2); - - res = g.at(1).edge(1, 2, None).unwrap().earliest_time().unwrap(); - assert_eq!(res, 0); - - res = g.at(1).edge(1, 2, None).unwrap().latest_time().unwrap(); - assert_eq!(res, 1); - - let res_list: Vec = g - .vertex(1) - .unwrap() - .edges() - .earliest_time() - .flatten() - .collect(); - assert_eq!(res_list, vec![0, 0]); - - let res_list: Vec = g - .vertex(1) - .unwrap() - .edges() - .latest_time() - .flatten() - .collect(); - assert_eq!(res_list, vec![2, 2]); - - let res_list: Vec = g - .vertex(1) - .unwrap() - .at(1) - .edges() - .earliest_time() - .flatten() - .collect(); - assert_eq!(res_list, vec![0, 0]); - - let res_list: Vec = g - .vertex(1) - .unwrap() - .at(1) - .edges() - .latest_time() - .flatten() - .collect(); - assert_eq!(res_list, vec![1, 1]); - } - - #[test] - fn check_vertex_history() { - let g = InternalGraph::new(1); - - g.add_vertex(1, 1, &vec![]).unwrap(); - g.add_vertex(2, 1, &vec![]).unwrap(); - g.add_vertex(3, 1, &vec![]).unwrap(); - g.add_vertex(4, 1, &vec![]).unwrap(); - g.add_vertex(8, 1, &vec![]).unwrap(); - - g.add_vertex(4, "Lord Farquaad", &vec![]).unwrap(); - g.add_vertex(6, "Lord Farquaad", &vec![]).unwrap(); - g.add_vertex(7, "Lord Farquaad", &vec![]).unwrap(); - g.add_vertex(8, "Lord Farquaad", &vec![]).unwrap(); - - let times_of_one = g.vertex(1).unwrap().history(); - let times_of_farquaad = g.vertex("Lord Farquaad").unwrap().history(); - - assert_eq!(times_of_one, [1, 2, 3, 4, 8]); - assert_eq!(times_of_farquaad, [4, 6, 7, 8]); - - let view = g.window(1, 8); - - let windowed_times_of_one = view.vertex(1).unwrap().history(); - let windowed_times_of_farquaad = view.vertex("Lord Farquaad").unwrap().history(); - assert_eq!(windowed_times_of_one, [1, 2, 3, 4]); - assert_eq!(windowed_times_of_farquaad, [4, 6, 7]); - } - - #[test] - fn check_edge_history() { - let g = InternalGraph::new(1); - - g.add_edge(1, 1, 2, &vec![], None).unwrap(); - g.add_edge(2, 1, 3, &vec![], None).unwrap(); - g.add_edge(3, 1, 2, &vec![], None).unwrap(); - g.add_edge(4, 1, 4, &vec![], None).unwrap(); - - let times_of_onetwo = g.edge(1, 2, None).unwrap().history(); - let times_of_four = g.edge(1, 4, None).unwrap().window(1, 5).history(); - let view = g.window(2, 5); - let windowed_times_of_four = view.edge(1, 4, None).unwrap().window(2, 4).history(); - - assert_eq!(times_of_onetwo, [1, 3]); - assert_eq!(times_of_four, [4]); - assert!(windowed_times_of_four.is_empty()); - } - - #[test] - fn check_edge_history_on_multiple_shards() { - let g = InternalGraph::new(10); - - g.add_edge(1, 1, 2, &vec![], None).unwrap(); - g.add_edge(2, 1, 3, &vec![], None).unwrap(); - g.add_edge(3, 1, 2, &vec![], None).unwrap(); - g.add_edge(4, 1, 4, &vec![], None).unwrap(); - g.add_edge(5, 1, 4, &vec![], None).unwrap(); - g.add_edge(6, 1, 4, &vec![], None).unwrap(); - g.add_edge(7, 1, 4, &vec![], None).unwrap(); - g.add_edge(8, 1, 4, &vec![], None).unwrap(); - g.add_edge(9, 1, 4, &vec![], None).unwrap(); - g.add_edge(10, 1, 4, &vec![], None).unwrap(); - - let times_of_onetwo = g.edge(1, 2, None).unwrap().history(); - let times_of_four = g.edge(1, 4, None).unwrap().window(1, 5).history(); - let times_of_outside_window = g.edge(1, 4, None).unwrap().window(1, 4).history(); - let times_of_four_higher = g.edge(1, 4, None).unwrap().window(6, 11).history(); - - let view = g.window(1, 11); - let windowed_times_of_four = view.edge(1, 4, None).unwrap().window(2, 5).history(); - let windowed_times_of_four_higher = view.edge(1, 4, None).unwrap().window(8, 11).history(); - - assert_eq!(times_of_onetwo, [1, 3]); - assert_eq!(times_of_four, [4]); - assert_eq!(times_of_four_higher, [6, 7, 8, 9, 10]); - assert!(times_of_outside_window.is_empty()); - assert_eq!(windowed_times_of_four, [4]); - assert_eq!(windowed_times_of_four_higher, [8, 9, 10]); - } - - #[test] - fn check_vertex_history_multiple_shards() { - let g = InternalGraph::new(10); - - g.add_vertex(1, 1, &vec![]).unwrap(); - g.add_vertex(2, 1, &vec![]).unwrap(); - g.add_vertex(3, 1, &vec![]).unwrap(); - g.add_vertex(4, 1, &vec![]).unwrap(); - g.add_vertex(5, 2, &vec![]).unwrap(); - g.add_vertex(6, 2, &vec![]).unwrap(); - g.add_vertex(7, 2, &vec![]).unwrap(); - g.add_vertex(8, 1, &vec![]).unwrap(); - g.add_vertex(9, 2, &vec![]).unwrap(); - g.add_vertex(10, 2, &vec![]).unwrap(); - - g.add_vertex(4, "Lord Farquaad", &vec![]).unwrap(); - g.add_vertex(6, "Lord Farquaad", &vec![]).unwrap(); - g.add_vertex(7, "Lord Farquaad", &vec![]).unwrap(); - g.add_vertex(8, "Lord Farquaad", &vec![]).unwrap(); - - let times_of_one = g.vertex(1).unwrap().history(); - let times_of_farquaad = g.vertex("Lord Farquaad").unwrap().history(); - let times_of_upper = g.vertex(2).unwrap().history(); - - assert_eq!(times_of_one, [1, 2, 3, 4, 8]); - assert_eq!(times_of_farquaad, [4, 6, 7, 8]); - assert_eq!(times_of_upper, [5, 6, 7, 9, 10]); - - let view = g.window(1, 8); - let windowed_times_of_one = view.vertex(1).unwrap().history(); - let windowed_times_of_two = view.vertex(2).unwrap().history(); - let windowed_times_of_farquaad = view.vertex("Lord Farquaad").unwrap().history(); - - assert_eq!(windowed_times_of_one, [1, 2, 3, 4]); - assert_eq!(windowed_times_of_farquaad, [4, 6, 7]); - assert_eq!(windowed_times_of_two, [5, 6, 7]); - } - - #[test] - fn test_ingesting_timestamps() { - let earliest_time = "2022-06-06 12:34:00".try_into_time().unwrap(); - let latest_time = "2022-06-07 12:34:00".try_into_time().unwrap(); - - let g = InternalGraph::new(4); - g.add_vertex("2022-06-06T12:34:00.000", 0, &vec![]).unwrap(); - g.add_edge("2022-06-07T12:34:00", 1, 2, &vec![], None) - .unwrap(); - assert_eq!(g.earliest_time().unwrap(), earliest_time); - assert_eq!(g.latest_time().unwrap(), latest_time); - - let g = InternalGraph::new(4); - let fmt = "%Y-%m-%d %H:%M"; - g.add_vertex_with_custom_time_format("2022-06-06 12:34", fmt, 0, &vec![]) - .unwrap(); - g.add_edge_with_custom_time_format("2022-06-07 12:34", fmt, 1, 2, &vec![], None) - .unwrap(); - assert_eq!(g.earliest_time().unwrap(), earliest_time); - assert_eq!(g.latest_time().unwrap(), latest_time); - } - - #[test] - fn test_prop_display_str() { - let mut prop = Prop::Str(String::from("hello")); - assert_eq!(format!("{}", prop), "hello"); - - prop = Prop::I32(42); - assert_eq!(format!("{}", prop), "42"); - - prop = Prop::I64(9223372036854775807); - assert_eq!(format!("{}", prop), "9223372036854775807"); - - prop = Prop::U32(4294967295); - assert_eq!(format!("{}", prop), "4294967295"); - - prop = Prop::U64(18446744073709551615); - assert_eq!(format!("{}", prop), "18446744073709551615"); - - prop = Prop::F32(3.14159); - assert_eq!(format!("{}", prop), "3.14159"); - - prop = Prop::F64(3.141592653589793); - assert_eq!(format!("{}", prop), "3.141592653589793"); - - prop = Prop::Bool(true); - assert_eq!(format!("{}", prop), "true"); - } - - #[test] - fn test_temporral_edge_props_window() { - let g = Graph::new(1); - g.add_edge(1, 1, 2, &vec![("weight".to_string(), Prop::I64(1))], None) - .unwrap(); - g.add_edge(2, 1, 2, &vec![("weight".to_string(), Prop::I64(2))], None) - .unwrap(); - g.add_edge(3, 1, 2, &vec![("weight".to_string(), Prop::I64(3))], None) - .unwrap(); - - let e = g.vertex(1).unwrap().out_edges().next().unwrap(); - - let res = g.temporal_edge_props_window(EdgeRef::from(e), 1, 3); - let mut exp = HashMap::new(); - exp.insert( - "weight".to_string(), - vec![(1, Prop::I64(1)), (2, Prop::I64(2))], - ); - assert_eq!(res, exp); - } - - #[test] - fn test_vertex_early_late_times() { - let g = InternalGraph::new(1); - g.add_vertex(1, 1, &vec![]).unwrap(); - g.add_vertex(2, 1, &vec![]).unwrap(); - g.add_vertex(3, 1, &vec![]).unwrap(); - - assert_eq!(g.vertex(1).unwrap().earliest_time(), Some(1)); - assert_eq!(g.vertex(1).unwrap().latest_time(), Some(3)); - - assert_eq!(g.at(2).vertex(1).unwrap().earliest_time(), Some(1)); - assert_eq!(g.at(2).vertex(1).unwrap().latest_time(), Some(2)); - } - - #[test] - fn test_vertex_ids() { - let g = InternalGraph::new(1); - g.add_vertex(1, 1, &vec![]).unwrap(); - g.add_vertex(1, 2, &vec![]).unwrap(); - g.add_vertex(2, 3, &vec![]).unwrap(); - - assert_eq!(g.vertices().id().collect::>(), vec![1, 2, 3]); - - let g_at = g.at(1); - assert_eq!(g_at.vertices().id().collect::>(), vec![1, 2]); - } - - #[test] - fn test_edge_layer_name() -> Result<(), GraphError> { - let g = InternalGraph::new(4); - g.add_edge(0, 0, 1, &vec![], None)?; - g.add_edge(0, 0, 1, &vec![], Some("awesome name"))?; - - let layer_names = g.edges().map(|e| e.layer_name()).sorted().collect_vec(); - assert_eq!(layer_names, vec!["awesome name", "default layer"]); - Ok(()) - } - - #[test] - fn test_edge_from_single_layer() { - let g = InternalGraph::new(4); - g.add_edge(0, 1, 2, &vec![], Some("layer")).unwrap(); - - assert!(g.edge(1, 2, None).is_none()); - assert!(g.layer("layer").unwrap().edge(1, 2, None).is_some()) - } - - #[test] - fn test_unique_layers() { - let g = InternalGraph::new(4); - g.add_edge(0, 1, 2, &vec![], Some("layer1")).unwrap(); - g.add_edge(0, 1, 2, &vec![], Some("layer2")).unwrap(); - assert_eq!( - g.layer("layer2").unwrap().get_unique_layers(), - vec!["layer2"] - ) - } - - #[quickcheck] - fn vertex_from_id_is_consistent(vertices: Vec) -> bool { - let g = InternalGraph::new(1); - for v in vertices.iter() { - g.add_vertex(0, *v, &vec![]).unwrap(); - } - g.vertices() - .name() - .map(|name| g.vertex(name)) - .all(|v| v.is_some()) - } - - #[quickcheck] - fn exploded_edge_times_is_consistent(edges: Vec<(u64, u64, Vec)>, offset: i64) -> bool { - let mut correct = true; - let mut check = |condition: bool, message: String| { - if !condition { - println!("Failed: {}", message); - } - correct = correct && condition; - }; - // checks that exploded edges are preserved with correct timestamps - let mut edges: Vec<(u64, u64, Vec)> = - edges.into_iter().filter(|e| !e.2.is_empty()).collect(); - // discard edges without timestamps - for e in edges.iter_mut() { - e.2.sort(); - // FIXME: Should not have to do this, see issue https://github.com/Pometry/Raphtory/issues/973 - e.2.dedup(); // add each timestamp only once (multi-edge per timestamp currently not implemented) - } - edges.sort(); - edges.dedup_by_key(|(src, dst, _)| (*src, *dst)); - - let g = Graph::new(1); - for (src, dst, times) in edges.iter() { - for t in times.iter() { - g.add_edge(*t, *src, *dst, &vec![], None).unwrap(); - } - } - - let mut actual_edges: Vec<(u64, u64, Vec)> = g - .edges() - .map(|e| { - ( - e.src().id(), - e.dst().id(), - e.explode() - .map(|ee| { - check( - ee.earliest_time() == ee.latest_time(), - format!("times mismatched for {:?}", ee), - ); // times are the same for exploded edge - let t = ee.earliest_time().unwrap(); - check( - ee.active(t), - format!("exploded edge {:?} inactive at {}", ee, t), - ); - if t < i64::MAX { - // window is broken at MAX! - check(e.active(t), format!("edge {:?} inactive at {}", e, t)); - } - let t_test = t.saturating_add(offset); - if t_test != t && t_test < i64::MAX && t_test > i64::MIN { - check( - !ee.active(t_test), - format!("exploded edge {:?} active at {}", ee, t_test), - ); - } - t - }) - .collect(), - ) - }) - .collect(); - - for e in actual_edges.iter_mut() { - e.2.sort(); - } - actual_edges.sort(); - check( - actual_edges == edges, - format!( - "actual edges didn't match input actual: {:?}, expected: {:?}", - actual_edges, edges - ), - ); - correct - } -} diff --git a/raphtory/src/db/graph/edge.rs b/raphtory/src/db/graph/edge.rs new file mode 100644 index 0000000000..3622d68940 --- /dev/null +++ b/raphtory/src/db/graph/edge.rs @@ -0,0 +1,561 @@ +//! Defines the `Edge` struct, which represents an edge in the graph. +//! +//! Edges are used to define directed connections between verticies in the graph. +//! Edges are identified by a unique ID, can have a direction (Ingoing, Outgoing, or Both) +//! and can have properties associated with them. +//! + +use super::views::layer_graph::LayeredGraph; +use crate::{ + core::{ + entities::{edges::edge_ref::EdgeRef, LayerIds, VID}, + storage::timeindex::TimeIndexEntry, + utils::{errors::GraphError, time::IntoTime}, + ArcStr, + }, + db::{ + api::{ + mutation::{ + internal::{InternalAdditionOps, InternalDeletionOps, InternalPropertyAdditionOps}, + CollectProperties, TryIntoInputTime, + }, + properties::{ + internal::{ConstPropertiesOps, TemporalPropertiesOps, TemporalPropertyViewOps}, + Properties, + }, + view::{internal::Static, BoxedIter, EdgeViewInternalOps, LayerOps}, + }, + graph::{vertex::VertexView, views::window_graph::WindowedGraph}, + }, + prelude::*, +}; +use std::{ + fmt::{Debug, Formatter}, + iter, +}; + +/// A view of an edge in the graph. +#[derive(Clone)] +pub struct EdgeView { + /// A view of an edge in the graph. + pub graph: G, + /// A reference to the edge. + pub edge: EdgeRef, +} + +impl Static for EdgeView {} + +impl EdgeView { + pub fn new(graph: G, edge: EdgeRef) -> Self { + Self { graph, edge } + } + + pub(crate) fn layer_ids(&self) -> LayerIds { + self.graph.layer_ids().constrain_from_edge(self.edge) + } +} + +impl + EdgeView +{ + pub fn delete(&self, t: T, layer: Option<&str>) -> Result<(), GraphError> { + let t = TimeIndexEntry::from_input(&self.graph, t)?; + let layer = self.resolve_layer(layer)?; + self.graph + .internal_delete_edge(t, self.edge.src(), self.edge.dst(), layer) + } +} + +impl PartialEq for EdgeView { + fn eq(&self, other: &Self) -> bool { + self.id() == other.id() + } +} + +impl EdgeViewInternalOps> for EdgeView { + fn graph(&self) -> G { + self.graph.clone() + } + + fn eref(&self) -> EdgeRef { + self.edge + } + + fn new_vertex(&self, v: VID) -> VertexView { + VertexView::new_internal(self.graph(), v) + } + + fn new_edge(&self, e: EdgeRef) -> Self { + Self { + graph: self.graph(), + edge: e, + } + } +} + +impl EdgeView { + fn resolve_layer(&self, layer: Option<&str>) -> Result { + match layer { + Some(name) => match self.edge.layer() { + Some(l_id) => self + .graph + .get_layer_id(name) + .filter(|id| id == l_id) + .ok_or_else(|| GraphError::InvalidLayer(name.to_owned())), + None => Ok(self.graph.resolve_layer(layer)), + }, + None => Ok(self.edge.layer().copied().unwrap_or(0)), + } + } + + /// Add constant properties for the edge + /// + /// Returns a person with the name given them + /// + /// # Arguments + /// + /// * `props` - Property key-value pairs to add + /// * `layer` - The layer to which properties should be added. If the edge view is restricted to a + /// single layer, 'None' will add the properties to that layer and 'Some("name")' + /// fails unless the layer matches the edge view. If the edge view is not restricted + /// to a single layer, 'None' sets the properties on the default layer and 'Some("name")' + /// sets the properties on layer '"name"' and fails if that layer doesn't exist. + pub fn add_constant_properties( + &self, + props: C, + layer: Option<&str>, + ) -> Result<(), GraphError> { + let properties: Vec<(usize, Prop)> = props.collect_properties( + |name, dtype| self.graph.resolve_edge_property(name, dtype, true), + |prop| self.graph.process_prop_value(prop), + )?; + let input_layer_id = self.resolve_layer(layer)?; + + self.graph.internal_add_constant_edge_properties( + self.edge.pid(), + input_layer_id, + properties, + ) + } + + pub fn add_updates( + &self, + time: T, + props: C, + layer: Option<&str>, + ) -> Result<(), GraphError> { + let t = TimeIndexEntry::from_input(&self.graph, time)?; + let layer_id = self.resolve_layer(layer)?; + let properties: Vec<(usize, Prop)> = props.collect_properties( + |name, dtype| self.graph.resolve_edge_property(name, dtype, false), + |prop| self.graph.process_prop_value(prop), + )?; + + self.graph + .internal_add_edge(t, self.edge.src(), self.edge.dst(), properties, layer_id)?; + Ok(()) + } +} + +impl ConstPropertiesOps for EdgeView { + fn get_const_prop_id(&self, name: &str) -> Option { + self.graph.edge_meta().const_prop_meta().get_id(name) + } + + fn get_const_prop_name(&self, id: usize) -> ArcStr { + self.graph.edge_meta().const_prop_meta().get_name(id) + } + + fn const_prop_ids(&self) -> Box + '_> { + self.graph + .const_edge_prop_ids(self.edge, self.graph.layer_ids()) + } + + fn const_prop_keys(&self) -> Box + '_> { + let reverse_map = self.graph.edge_meta().const_prop_meta().get_keys(); + Box::new(self.const_prop_ids().map(move |id| reverse_map[id].clone())) + } + + fn get_const_prop(&self, id: usize) -> Option { + self.graph + .get_const_edge_prop(self.edge, id, self.graph.layer_ids()) + } +} + +impl TemporalPropertyViewOps for EdgeView { + fn temporal_history(&self, id: usize) -> Vec { + self.graph + .temporal_edge_prop_vec(self.edge, id, self.graph.layer_ids()) + .into_iter() + .map(|(t, _)| t) + .collect() + } + + fn temporal_values(&self, id: usize) -> Vec { + let layer_ids = self.graph.layer_ids().constrain_from_edge(self.edge); + self.graph + .temporal_edge_prop_vec(self.edge, id, layer_ids) + .into_iter() + .map(|(_, v)| v) + .collect() + } +} + +impl TemporalPropertiesOps for EdgeView { + fn get_temporal_prop_id(&self, name: &str) -> Option { + self.graph + .edge_meta() + .temporal_prop_meta() + .get_id(name) + .filter(|id| { + self.graph + .has_temporal_edge_prop(self.edge, *id, self.layer_ids()) + }) + } + + fn get_temporal_prop_name(&self, id: usize) -> ArcStr { + self.graph.edge_meta().temporal_prop_meta().get_name(id) + } + + fn temporal_prop_ids(&self) -> Box + '_> { + Box::new( + self.graph + .temporal_edge_prop_ids(self.edge, self.layer_ids()) + .filter(|id| { + self.graph + .has_temporal_edge_prop(self.edge, *id, self.layer_ids()) + }), + ) + } + + fn temporal_prop_keys(&self) -> Box + '_> { + let reverse_map = self.graph.edge_meta().temporal_prop_meta().get_keys(); + Box::new( + self.temporal_prop_ids() + .map(move |id| reverse_map[id].clone()), + ) + } +} + +impl EdgeViewOps for EdgeView { + type Graph = G; + type Vertex = VertexView; + type EList = BoxedIter; + + fn explode(&self) -> Self::EList { + let ev = self.clone(); + match self.edge.time() { + Some(_) => Box::new(iter::once(ev)), + None => { + let layer_ids = self.graph.layer_ids().constrain_from_edge(self.edge); + let e = self.edge; + let ex_iter = self.graph.edge_exploded(e, layer_ids); + // FIXME: use duration + Box::new(ex_iter.map(move |ex| ev.new_edge(ex))) + } + } + } + + fn explode_layers(&self) -> Self::EList { + let ev = self.clone(); + match self.edge.layer() { + Some(_) => Box::new(iter::once(ev)), + None => { + let e = self.edge; + let ex_iter = self.graph.edge_layers(e, self.graph.layer_ids()); + Box::new(ex_iter.map(move |ex| ev.new_edge(ex))) + } + } + } +} + +impl Debug for EdgeView { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "EdgeView({}, {})", + self.graph.vertex(self.edge.src()).unwrap().id(), + self.graph.vertex(self.edge.dst()).unwrap().id() + ) + } +} + +impl From> for EdgeRef { + fn from(value: EdgeView) -> Self { + value.edge + } +} + +impl TimeOps for EdgeView { + type WindowedViewType = EdgeView>; + + fn start(&self) -> Option { + self.graph.start() + } + + fn end(&self) -> Option { + self.graph.end() + } + + fn window(&self, t_start: T, t_end: T) -> Self::WindowedViewType { + EdgeView { + graph: self.graph.window(t_start, t_end), + edge: self.edge, + } + } +} + +impl LayerOps for EdgeView { + type LayeredViewType = EdgeView>; + + fn default_layer(&self) -> Self::LayeredViewType { + EdgeView { + graph: self.graph.default_layer(), + edge: self.edge, + } + } + + fn layer>(&self, name: L) -> Option { + let layer_ids = self + .graph + .layer_ids_from_names(name.into()) + .constrain_from_edge(self.edge); + self.graph + .has_edge_ref( + self.edge.src(), + self.edge.dst(), + &layer_ids, + self.graph.edge_filter(), + ) + .then(|| EdgeView { + graph: LayeredGraph::new(self.graph.clone(), layer_ids), + edge: self.edge, + }) + } +} + +/// Implement `EdgeListOps` trait for an iterator of `EdgeView` objects. +/// +/// This implementation enables the use of the `src` and `dst` methods to retrieve the vertices +/// connected to the edges inside the iterator. +impl EdgeListOps for BoxedIter> { + type Graph = G; + type Vertex = VertexView; + type Edge = EdgeView; + type ValueType = T; + + /// Specifies the associated type for an iterator over vertices. + type VList = Box> + Send>; + + /// Specifies the associated type for the iterator over edges. + type IterType = Box + Send>; + + fn properties(self) -> Self::IterType> { + Box::new(self.map(move |e| e.properties())) + } + + /// Returns an iterator over the source vertices of the edges in the iterator. + fn src(self) -> Self::VList { + Box::new(self.map(|e| e.src())) + } + + /// Returns an iterator over the destination vertices of the edges in the iterator. + fn dst(self) -> Self::VList { + Box::new(self.map(|e| e.dst())) + } + + fn id(self) -> Self::IterType<(u64, u64)> { + Box::new(self.map(|e| e.id())) + } + + /// returns an iterator of exploded edges that include an edge at each point in time + fn explode(self) -> Self { + Box::new(self.flat_map(move |e| e.explode())) + } + + /// Gets the earliest times of a list of edges + fn earliest_time(self) -> Self::IterType> { + Box::new(self.map(|e| e.earliest_time())) + } + + /// Gets the latest times of a list of edges + fn latest_time(self) -> Self::IterType> { + Box::new(self.map(|e| e.latest_time())) + } + + fn time(self) -> Self::IterType> { + Box::new(self.map(|e| e.time())) + } + + fn layer_name(self) -> Self::IterType> { + Box::new(self.map(|e| e.layer_name().map(|v| v.clone()))) + } +} + +impl EdgeListOps for BoxedIter>> { + type Graph = G; + type Vertex = VertexView; + type Edge = EdgeView; + type ValueType = Box + Send>; + type VList = Box> + Send>> + Send>; + type IterType = Box + Send>> + Send>; + + fn properties(self) -> Self::IterType> { + Box::new(self.map(move |it| it.properties())) + } + + fn src(self) -> Self::VList { + Box::new(self.map(|it| it.src())) + } + + fn dst(self) -> Self::VList { + Box::new(self.map(|it| it.dst())) + } + + fn id(self) -> Self::IterType<(u64, u64)> { + Box::new(self.map(|it| it.id())) + } + + fn explode(self) -> Self { + Box::new(self.map(move |it| it.explode())) + } + + /// Gets the earliest times of a list of edges + fn earliest_time(self) -> Self::IterType> { + Box::new(self.map(|e| e.earliest_time())) + } + + /// Gets the latest times of a list of edges + fn latest_time(self) -> Self::IterType> { + Box::new(self.map(|e| e.latest_time())) + } + + fn time(self) -> Self::IterType> { + Box::new(self.map(|it| it.time())) + } + + fn layer_name(self) -> Self::IterType> { + Box::new(self.map(|it| it.layer_name())) + } +} + +pub type EdgeList = Box> + Send>; + +#[cfg(test)] +mod test_edge { + use crate::{ + core::{ArcStr, IntoPropMap}, + prelude::*, + }; + use itertools::Itertools; + use std::collections::HashMap; + + #[test] + fn test_properties() { + let g = Graph::new(); + let props = [(ArcStr::from("test"), "test".into_prop())]; + g.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); + g.add_edge(2, 1, 2, props.clone(), None).unwrap(); + + let e1 = g.edge(1, 2).unwrap(); + let e1_w = g.window(0, 1).edge(1, 2).unwrap(); + assert_eq!(HashMap::from_iter(e1.properties().as_vec()), props.into()); + assert!(e1_w.properties().as_vec().is_empty()) + } + + #[test] + fn test_constant_properties() { + let g = Graph::new(); + g.add_edge(1, 1, 2, NO_PROPS, Some("layer 1")) + .unwrap() + .add_constant_properties([("test_prop", "test_val")], Some("layer 1")) + .unwrap(); + g.add_edge(1, 2, 3, NO_PROPS, Some("layer 2")) + .unwrap() + .add_constant_properties([("test_prop", "test_val")], Some("layer 2")) + .unwrap(); + + assert_eq!( + g.edge(1, 2) + .unwrap() + .properties() + .constant() + .get("test_prop"), + Some([("layer 1", "test_val")].into_prop_map()) + ); + assert_eq!( + g.edge(2, 3) + .unwrap() + .properties() + .constant() + .get("test_prop"), + Some([("layer 2", "test_val")].into_prop_map()) + ); + for e in g.edges() { + for ee in e.explode() { + assert_eq!( + ee.properties().constant().get("test_prop"), + Some("test_val".into()) + ) + } + } + } + + #[test] + fn test_property_additions() { + let g = Graph::new(); + let props = [("test", "test")]; + let e1 = g.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); + e1.add_updates(2, props, None).unwrap(); // same layer works + assert!(e1.add_updates(2, props, Some("test2")).is_err()); // different layer is error + let e = g.edge(1, 2).unwrap(); + e.add_updates(2, props, Some("test2")).unwrap(); // non-restricted edge view can create new layers + let layered_views = e.explode_layers().collect_vec(); + for ev in layered_views { + let layer = ev.layer_name().unwrap(); + assert!(ev.add_updates(1, props, Some("test")).is_err()); // restricted edge view cannot create updates in different layer + ev.add_updates(1, [("test2", layer)], None).unwrap() // this will add an update to the same layer as the view (not the default layer) + } + let e1_w = e1.window(0, 1); + assert_eq!( + e1.properties().as_map(), + props + .into_iter() + .map(|(k, v)| (ArcStr::from(k), v.into_prop())) + .chain([(ArcStr::from("test2"), "_default".into_prop())]) + .collect() + ); + assert_eq!( + e.layer("test2").unwrap().properties().as_map(), + props + .into_iter() + .map(|(k, v)| (ArcStr::from(k), v.into_prop())) + .chain([(ArcStr::from("test2"), "test2".into_prop())]) + .collect() + ); + assert_eq!(e1_w.properties().as_map(), HashMap::default()) + } + + #[test] + fn test_constant_property_additions() { + let g = Graph::new(); + let e = g.add_edge(0, 1, 2, NO_PROPS, Some("test")).unwrap(); + assert!(e + .add_constant_properties([("test1", "test1")], None) + .is_ok()); // adds properties to layer `"test"` + assert!(e + .add_constant_properties([("test", "test")], Some("test2")) + .is_err()); // cannot add properties to a different layer + e.add_constant_properties([("test", "test")], Some("test")) + .unwrap(); // layer is consistent + assert_eq!(e.properties().get("test"), Some("test".into())); + assert_eq!(e.properties().get("test1"), Some("test1".into())); + } + + #[test] + fn test_layers_earliest_time() { + let g = Graph::new(); + let e = g.add_edge(1, 1, 2, NO_PROPS, Some("test")).unwrap(); + assert_eq!(e.earliest_time(), Some(1)); + } +} diff --git a/raphtory/src/db/graph/graph.rs b/raphtory/src/db/graph/graph.rs new file mode 100644 index 0000000000..332a5ec18a --- /dev/null +++ b/raphtory/src/db/graph/graph.rs @@ -0,0 +1,1577 @@ +//! Defines the `Graph` struct, which represents a raphtory graph in memory. +//! +//! This is the base class used to create a temporal graph, add vertices and edges, +//! create windows, and query the graph with a variety of algorithms. +//! It is a wrapper around a set of shards, which are the actual graph data structures. +//! +//! # Examples +//! +//! ```rust +//! use raphtory::prelude::*; +//! let graph = Graph::new(); +//! graph.add_vertex(0, "Alice", NO_PROPS).unwrap(); +//! graph.add_vertex(1, "Bob", NO_PROPS).unwrap(); +//! graph.add_edge(2, "Alice", "Bob", NO_PROPS, None).unwrap(); +//! graph.count_edges(); +//! ``` +//! + +use crate::{ + core::{entities::graph::tgraph::InnerTemporalGraph, utils::errors::GraphError}, + db::api::{ + mutation::internal::{InheritAdditionOps, InheritPropertyAdditionOps}, + view::internal::{Base, DynamicGraph, InheritViewOps, IntoDynamic, MaterializedGraph}, + }, + prelude::*, +}; +use serde::{Deserialize, Serialize}; +use std::{ + fmt::{Display, Formatter}, + path::Path, + sync::Arc, +}; + +const SEG: usize = 16; +pub(crate) type InternalGraph = InnerTemporalGraph; + +#[repr(transparent)] +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct Graph(pub Arc); + +pub fn graph_equal(g1: &G1, g2: &G2) -> bool { + if g1.count_vertices() == g2.count_vertices() && g1.count_edges() == g2.count_edges() { + g1.vertices().id().all(|v| g2.has_vertex(v)) && // all vertices exist in other + g1.edges().explode().count() == g2.edges().explode().count() && // same number of exploded edges + g1.edges().explode().all(|e| { // all exploded edges exist in other + g2 + .edge(e.src().id(), e.dst().id()) + .filter(|ee| ee.active(e.time().expect("exploded"))) + .is_some() + }) + } else { + false + } +} + +impl Display for Graph { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for Graph { + fn from(value: InternalGraph) -> Self { + Self(Arc::new(value)) + } +} + +impl PartialEq for Graph { + fn eq(&self, other: &G) -> bool { + graph_equal(self, other) + } +} + +impl Base for Graph { + type Base = InternalGraph; + + #[inline(always)] + fn base(&self) -> &InternalGraph { + &self.0 + } +} + +impl InheritAdditionOps for Graph {} +impl InheritPropertyAdditionOps for Graph {} +impl InheritViewOps for Graph {} + +impl Graph { + /// Create a new graph with the specified number of shards + /// + /// # Returns + /// + /// A raphtory graph + /// + /// # Example + /// + /// ``` + /// use raphtory::prelude::Graph; + /// let g = Graph::new(); + /// ``` + pub fn new() -> Self { + Self(Arc::new(InternalGraph::default())) + } + + pub(crate) fn new_from_inner(inner: Arc) -> Self { + Self(inner) + } + + /// Load a graph from a directory + /// + /// # Arguments + /// + /// * `path` - The path to the directory + /// + /// # Returns + /// + /// A raphtory graph + /// + /// # Example + /// + /// ```no_run + /// use raphtory::prelude::Graph; + /// let g = Graph::load_from_file("path/to/graph"); + /// ``` + pub fn load_from_file>(path: P) -> Result { + let g = MaterializedGraph::load_from_file(path)?; + g.into_events().ok_or(GraphError::GraphLoadError) + } + + /// Save a graph to a directory + pub fn save_to_file>(&self, path: P) -> Result<(), GraphError> { + MaterializedGraph::from(self.clone()).save_to_file(path) + } + + pub fn as_arc(&self) -> Arc { + self.0.clone() + } +} + +impl IntoDynamic for Graph { + fn into_dynamic(self) -> DynamicGraph { + DynamicGraph::new(self) + } +} + +#[cfg(test)] +mod db_tests { + use super::*; + use crate::{ + core::{ + utils::time::{error::ParseTimeError, TryIntoTime}, + ArcStr, Prop, + }, + db::{ + api::view::{ + EdgeListOps, EdgeViewOps, GraphViewOps, Layer, LayerOps, TimeOps, VertexViewOps, + }, + graph::{edge::EdgeView, path::PathFromVertex}, + }, + graphgen::random_attachment::random_attachment, + prelude::{AdditionOps, PropertyAdditionOps}, + }; + use chrono::NaiveDateTime; + use itertools::Itertools; + use quickcheck::Arbitrary; + use rayon::prelude::*; + use std::collections::{HashMap, HashSet}; + use tempdir::TempDir; + + #[quickcheck] + fn test_multithreaded_add_edge(edges: Vec<(u64, u64)>) -> bool { + let g = Graph::new(); + edges.par_iter().enumerate().for_each(|(t, (i, j))| { + g.add_edge(t as i64, *i, *j, NO_PROPS, None).unwrap(); + }); + edges + .iter() + .all(|(i, j)| g.has_edge(*i, *j, Layer::Default)) + && g.count_temporal_edges() == edges.len() + } + + #[quickcheck] + fn add_vertex_grows_graph_len(vs: Vec<(i64, u64)>) { + let g = Graph::new(); + + let expected_len = vs.iter().map(|(_, v)| v).sorted().dedup().count(); + for (t, v) in vs { + g.add_vertex(t, v, NO_PROPS) + .map_err(|err| println!("{:?}", err)) + .ok(); + } + + assert_eq!(g.count_vertices(), expected_len) + } + + #[quickcheck] + fn add_vertex_gets_names(vs: Vec) -> bool { + let g = Graph::new(); + + let expected_len = vs.iter().sorted().dedup().count(); + for (t, name) in vs.iter().enumerate() { + g.add_vertex(t as i64, name.clone(), NO_PROPS) + .map_err(|err| println!("{:?}", err)) + .ok(); + } + + assert_eq!(g.count_vertices(), expected_len); + + vs.iter().all(|name| { + let v = g.vertex(name.clone()).unwrap(); + v.name() == name.clone() + }) + } + + #[quickcheck] + fn add_edge_grows_graph_edge_len(edges: Vec<(i64, u64, u64)>) { + let g = Graph::new(); + + let unique_vertices_count = edges + .iter() + .flat_map(|(_, src, dst)| vec![src, dst]) + .sorted() + .dedup() + .count(); + + let unique_edge_count = edges + .iter() + .map(|(_, src, dst)| (src, dst)) + .unique() + .count(); + + for (t, src, dst) in edges { + g.add_edge(t, src, dst, NO_PROPS, None).unwrap(); + } + + assert_eq!(g.count_vertices(), unique_vertices_count); + assert_eq!(g.count_edges(), unique_edge_count); + } + + #[quickcheck] + fn add_edge_works(edges: Vec<(i64, u64, u64)>) -> bool { + let g = Graph::new(); + for &(t, src, dst) in edges.iter() { + g.add_edge(t, src, dst, NO_PROPS, None).unwrap(); + } + + edges + .iter() + .all(|&(_, src, dst)| g.has_edge(src, dst, Layer::All)) + } + + #[quickcheck] + fn get_edge_works(edges: Vec<(i64, u64, u64)>) -> bool { + let g = Graph::new(); + for &(t, src, dst) in edges.iter() { + g.add_edge(t, src, dst, NO_PROPS, None).unwrap(); + } + + edges + .iter() + .all(|&(_, src, dst)| g.edge(src, dst).is_some()) + } + + #[test] + fn graph_save_to_load_from_file() { + let vs = vec![ + (1, 1, 2), + (2, 1, 3), + (-1, 2, 1), + (0, 1, 1), + (7, 3, 2), + (1, 1, 1), + ]; + + let g = Graph::new(); + + for (t, src, dst) in &vs { + g.add_edge(*t, *src, *dst, NO_PROPS, None).unwrap(); + } + + let tmp_raphtory_path: TempDir = + TempDir::new("raphtory").expect("Failed to create tempdir"); + + let graph_path = format!("{}/graph.bin", tmp_raphtory_path.path().display()); + g.save_to_file(&graph_path).expect("Failed to save graph"); + + // Load from files + let g2 = Graph::load_from_file(&graph_path).expect("Failed to load graph"); + + assert_eq!(g, g2); + + let _ = tmp_raphtory_path.close(); + } + + #[test] + fn has_edge() { + let g = Graph::new(); + g.add_edge(1, 7, 8, NO_PROPS, None).unwrap(); + + assert!(!g.has_edge(8, 7, Layer::All)); + assert!(g.has_edge(7, 8, Layer::All)); + + g.add_edge(1, 7, 9, NO_PROPS, None).unwrap(); + + assert!(!g.has_edge(9, 7, Layer::All)); + assert!(g.has_edge(7, 9, Layer::All)); + + g.add_edge(2, "haaroon", "northLondon", NO_PROPS, None) + .unwrap(); + assert!(g.has_edge("haaroon", "northLondon", Layer::All)); + } + + #[test] + fn graph_edge() { + let g = Graph::new(); + let es = vec![ + (1, 1, 2), + (2, 1, 3), + (-1, 2, 1), + (0, 1, 1), + (7, 3, 2), + (1, 1, 1), + ]; + for (t, src, dst) in es { + g.add_edge(t, src, dst, NO_PROPS, None).unwrap(); + } + + let e = g + .window(i64::MIN, i64::MAX) + .layer(Layer::Default) + .unwrap() + .edge(1, 3) + .unwrap(); + assert_eq!(e.src().id(), 1u64); + assert_eq!(e.dst().id(), 3u64); + } + + #[test] + fn graph_degree_window() { + let vs = vec![ + (1, 1, 2), + (2, 1, 3), + (-1, 2, 1), + (0, 1, 1), + (7, 3, 2), + (1, 1, 1), + ]; + + let g = Graph::new(); + + for (t, src, dst) in &vs { + g.add_edge(*t, *src, *dst, NO_PROPS, None).unwrap(); + } + + let expected = vec![(2, 3, 1), (1, 0, 0), (1, 0, 0)]; + let actual = (1..=3) + .map(|i| { + let v = g.vertex(i).unwrap(); + ( + v.window(-1, 7).in_degree(), + v.window(1, 7).out_degree(), + v.window(0, 1).degree(), + ) + }) + .collect::>(); + + assert_eq!(actual, expected); + } + + #[test] + fn graph_edges_window() { + let vs = vec![ + (1, 1, 2), + (2, 1, 3), + (-1, 2, 1), + (0, 1, 1), + (7, 3, 2), + (1, 1, 1), + ]; + + let g = Graph::new(); + + for (t, src, dst) in &vs { + g.add_edge(*t, *src, *dst, NO_PROPS, None).unwrap(); + } + + let expected = vec![(2, 3, 2), (1, 0, 0), (1, 0, 0)]; + let actual = (1..=3) + .map(|i| { + let v = g.vertex(i).unwrap(); + ( + v.window(-1, 7).in_edges().collect::>().len(), + v.window(1, 7).out_edges().collect::>().len(), + v.window(0, 1).edges().collect::>().len(), + ) + }) + .collect::>(); + + assert_eq!(actual, expected); + } + + #[test] + fn time_test() { + let g = Graph::new(); + + assert_eq!(g.latest_time(), None); + assert_eq!(g.earliest_time(), None); + + g.add_vertex(5, 1, NO_PROPS) + .map_err(|err| println!("{:?}", err)) + .ok(); + + assert_eq!(g.latest_time(), Some(5)); + assert_eq!(g.earliest_time(), Some(5)); + + let g = Graph::new(); + + g.add_edge(10, 1, 2, NO_PROPS, None).unwrap(); + assert_eq!(g.latest_time(), Some(10)); + assert_eq!(g.earliest_time(), Some(10)); + + g.add_vertex(5, 1, NO_PROPS) + .map_err(|err| println!("{:?}", err)) + .ok(); + assert_eq!(g.latest_time(), Some(10)); + assert_eq!(g.earliest_time(), Some(5)); + + g.add_edge(20, 3, 4, NO_PROPS, None).unwrap(); + assert_eq!(g.latest_time(), Some(20)); + assert_eq!(g.earliest_time(), Some(5)); + + random_attachment(&g, 100, 10); + assert_eq!(g.latest_time(), Some(126)); + assert_eq!(g.earliest_time(), Some(5)); + } + + #[test] + fn static_properties() { + let g = Graph::new(); + g.add_edge(0, 11, 22, NO_PROPS, None).unwrap(); + g.add_edge( + 0, + 11, + 11, + vec![("temp".to_string(), Prop::Bool(true))], + None, + ) + .unwrap(); + g.add_edge(0, 22, 33, NO_PROPS, None).unwrap(); + g.add_edge(0, 33, 11, NO_PROPS, None).unwrap(); + g.add_vertex(0, 11, vec![("temp".to_string(), Prop::Bool(true))]) + .unwrap(); + g.add_edge(0, 44, 55, NO_PROPS, None).unwrap(); + let v11 = g.vertex(11).unwrap(); + let v22 = g.vertex(22).unwrap(); + let v33 = g.vertex(33).unwrap(); + let v44 = g.vertex(44).unwrap(); + let v55 = g.vertex(55).unwrap(); + let edge1111 = g.edge(&v11, &v11).unwrap(); + let edge2233 = g.edge(&v22, &v33).unwrap(); + let edge3311 = g.edge(&v33, &v11).unwrap(); + + v11.add_constant_properties(vec![("a", Prop::U64(11)), ("b", Prop::I64(11))]) + .unwrap(); + v11.add_constant_properties(vec![("c", Prop::U32(11))]) + .unwrap(); + + v44.add_constant_properties(vec![("e", Prop::U8(1))]) + .unwrap(); + v55.add_constant_properties(vec![("f", Prop::U16(1))]) + .unwrap(); + edge1111 + .add_constant_properties(vec![("d", Prop::U64(1111))], None) + .unwrap(); + edge3311 + .add_constant_properties(vec![("a", Prop::U64(3311))], None) + .unwrap(); + + // cannot change property type + assert!(v22 + .add_constant_properties(vec![("b", Prop::U64(22))]) + .is_err()); + + assert_eq!(v11.properties().constant().keys(), vec!["a", "b", "c"]); + assert!(v22.properties().constant().keys().is_empty()); + assert!(v33.properties().constant().keys().is_empty()); + assert_eq!(v44.properties().constant().keys(), vec!["e"]); + assert_eq!(v55.properties().constant().keys(), vec!["f"]); + assert_eq!(edge1111.properties().constant().keys(), vec!["d"]); + assert_eq!(edge3311.properties().constant().keys(), vec!["a"]); + assert!(edge2233.properties().constant().keys().is_empty()); + + assert_eq!(v11.properties().constant().get("a"), Some(Prop::U64(11))); + assert_eq!(v11.properties().constant().get("b"), Some(Prop::I64(11))); + assert_eq!(v11.properties().constant().get("c"), Some(Prop::U32(11))); + assert_eq!(v22.properties().constant().get("b"), None); + assert_eq!(v44.properties().constant().get("e"), Some(Prop::U8(1))); + assert_eq!(v55.properties().constant().get("f"), Some(Prop::U16(1))); + assert_eq!(v22.properties().constant().get("a"), None); + assert_eq!( + edge1111.properties().constant().get("d"), + Some(Prop::U64(1111)) + ); + assert_eq!( + edge3311.properties().constant().get("a"), + Some(Prop::U64(3311)) + ); + assert_eq!(edge2233.properties().constant().get("a"), None); + } + + #[test] + fn temporal_props_vertex() { + let g = Graph::new(); + + g.add_vertex(0, 1, [("cool".to_string(), Prop::Bool(true))]) + .unwrap(); + + let v = g.vertex(1).unwrap(); + + let actual = v.properties().get("cool"); + assert_eq!(actual, Some(Prop::Bool(true))); + + // we flip cool from true to false after t 3 + g.add_vertex(3, 1, [("cool".to_string(), Prop::Bool(false))]) + .unwrap(); + + let wg = g.window(3, 15); + let v = wg.vertex(1).unwrap(); + + let actual = v.properties().get("cool"); + assert_eq!(actual, Some(Prop::Bool(false))); + + let hist: Vec<_> = v + .properties() + .temporal() + .get("cool") + .unwrap() + .iter() + .collect(); + assert_eq!(hist, vec![(3, Prop::Bool(false))]); + + let v = g.vertex(1).unwrap(); + + let hist: Vec<_> = v + .properties() + .temporal() + .get("cool") + .unwrap() + .iter() + .collect(); + assert_eq!(hist, vec![(0, Prop::Bool(true)), (3, Prop::Bool(false))]); + } + + #[test] + fn temporal_props_edge() { + let g = Graph::new(); + + g.add_edge(1, 0, 1, vec![("distance".to_string(), Prop::U32(5))], None) + .expect("add edge"); + + let e = g.edge(0, 1).unwrap(); + + let prop = e.properties().get("distance").unwrap(); + assert_eq!(prop, Prop::U32(5)); + } + + #[test] + fn graph_neighbours_window() { + let vs = vec![ + (1, 1, 2), + (2, 1, 3), + (-1, 2, 1), + (0, 1, 1), + (7, 3, 2), + (1, 1, 1), + ]; + + let g = Graph::new(); + + for (t, src, dst) in &vs { + g.add_edge(*t, *src, *dst, NO_PROPS, None).unwrap(); + } + + let expected = vec![ + (vec![1, 2], vec![1, 2, 3], vec![1]), + (vec![1], vec![], vec![]), + (vec![1], vec![], vec![]), + ]; + let actual = (1..=3) + .map(|i| { + let v = g.vertex(i).unwrap(); + ( + v.window(-1, 7).in_neighbours().id().collect::>(), + v.window(1, 7).out_neighbours().id().collect::>(), + v.window(0, 1).neighbours().id().collect::>(), + ) + }) + .collect::>(); + + assert_eq!(actual, expected); + } + + #[test] + fn test_time_range_on_empty_graph() { + let g = Graph::new(); + + let rolling = g.rolling(1, None).unwrap().collect_vec(); + assert!(rolling.is_empty()); + + let expanding = g.expanding(1).unwrap().collect_vec(); + assert!(expanding.is_empty()); + } + + #[test] + fn test_add_vertex_with_strings() { + let g = Graph::new(); + + g.add_vertex(0, "haaroon", NO_PROPS).unwrap(); + g.add_vertex(1, "hamza", NO_PROPS).unwrap(); + g.add_vertex(1, 831, NO_PROPS).unwrap(); + + assert!(g.has_vertex(831)); + assert!(g.has_vertex("haaroon")); + assert!(g.has_vertex("hamza")); + + assert_eq!(g.count_vertices(), 3); + } + + #[test] + fn layers() -> Result<(), GraphError> { + let g = Graph::new(); + g.add_edge(0, 11, 22, NO_PROPS, None)?; + g.add_edge(0, 11, 33, NO_PROPS, None)?; + g.add_edge(0, 33, 11, NO_PROPS, None)?; + g.add_edge(0, 11, 22, NO_PROPS, Some("layer1"))?; + g.add_edge(0, 11, 33, NO_PROPS, Some("layer2"))?; + g.add_edge(0, 11, 44, NO_PROPS, Some("layer2"))?; + + assert!(g.has_edge(11, 22, Layer::All)); + assert!(g.has_edge(11, 22, Layer::Default)); + assert!(!g.has_edge(11, 44, Layer::Default)); + assert!(!g.has_edge(11, 22, "layer2")); + assert!(g.has_edge(11, 44, "layer2")); + + assert!(g.edge(11, 22).is_some()); + assert!(g.layer(Layer::Default).unwrap().edge(11, 44).is_none()); + assert!(g.edge(11, 22).unwrap().layer("layer2").is_none()); + assert!(g.edge(11, 44).unwrap().layer("layer2").is_some()); + + let dft_layer = g.default_layer(); + let layer1 = g.layer("layer1").expect("layer1"); + let layer2 = g.layer("layer2").expect("layer2"); + assert!(g.layer("missing layer").is_none()); + + assert_eq!(g.count_vertices(), 4); + assert_eq!(g.count_edges(), 4); + assert_eq!(dft_layer.count_edges(), 3); + assert_eq!(layer1.count_edges(), 1); + assert_eq!(layer2.count_edges(), 2); + + let vertex = g.vertex(11).unwrap(); + let vertex_dft = dft_layer.vertex(11).unwrap(); + let vertex1 = layer1.vertex(11).unwrap(); + let vertex2 = layer2.vertex(11).unwrap(); + + assert_eq!(vertex.degree(), 3); + assert_eq!(vertex_dft.degree(), 2); + assert_eq!(vertex1.degree(), 1); + assert_eq!(vertex2.degree(), 2); + + assert_eq!(vertex.out_degree(), 3); + assert_eq!(vertex_dft.out_degree(), 2); + assert_eq!(vertex1.out_degree(), 1); + assert_eq!(vertex2.out_degree(), 2); + + assert_eq!(vertex.in_degree(), 1); + assert_eq!(vertex_dft.in_degree(), 1); + assert_eq!(vertex1.in_degree(), 0); + assert_eq!(vertex2.in_degree(), 0); + + fn to_tuples>>( + edges: I, + ) -> Vec<(u64, u64)> { + edges + .map(|e| (e.src().id(), e.dst().id())) + .sorted() + .collect_vec() + } + + assert_eq!( + to_tuples(vertex.edges()), + vec![(11, 22), (11, 33), (11, 44), (33, 11)] + ); + assert_eq!( + to_tuples(vertex_dft.edges()), + vec![(11, 22), (11, 33), (33, 11)] + ); + assert_eq!(to_tuples(vertex1.edges()), vec![(11, 22)]); + assert_eq!(to_tuples(vertex2.edges()), vec![(11, 33), (11, 44)]); + + assert_eq!(to_tuples(vertex.in_edges()), vec![(33, 11)]); + assert_eq!(to_tuples(vertex_dft.in_edges()), vec![(33, 11)]); + assert_eq!(to_tuples(vertex1.in_edges()), vec![]); + assert_eq!(to_tuples(vertex2.in_edges()), vec![]); + + assert_eq!( + to_tuples(vertex.out_edges()), + vec![(11, 22), (11, 33), (11, 44)] + ); + assert_eq!(to_tuples(vertex_dft.out_edges()), vec![(11, 22), (11, 33)]); + assert_eq!(to_tuples(vertex1.out_edges()), vec![(11, 22)]); + assert_eq!(to_tuples(vertex2.out_edges()), vec![(11, 33), (11, 44)]); + + fn to_ids(neighbours: PathFromVertex) -> Vec { + neighbours.iter().map(|n| n.id()).sorted().collect_vec() + } + + assert_eq!(to_ids(vertex.neighbours()), vec![22, 33, 44]); + assert_eq!(to_ids(vertex_dft.neighbours()), vec![22, 33]); + assert_eq!(to_ids(vertex1.neighbours()), vec![22]); + assert_eq!(to_ids(vertex2.neighbours()), vec![33, 44]); + + assert_eq!(to_ids(vertex.out_neighbours()), vec![22, 33, 44]); + assert_eq!(to_ids(vertex_dft.out_neighbours()), vec![22, 33]); + assert_eq!(to_ids(vertex1.out_neighbours()), vec![22]); + assert_eq!(to_ids(vertex2.out_neighbours()), vec![33, 44]); + + assert_eq!(to_ids(vertex.in_neighbours()), vec![33]); + assert_eq!(to_ids(vertex_dft.in_neighbours()), vec![33]); + assert!(to_ids(vertex1.in_neighbours()).is_empty()); + assert!(to_ids(vertex2.in_neighbours()).is_empty()); + Ok(()) + } + + #[test] + fn test_exploded_edge() { + let g = Graph::new(); + g.add_edge(0, 1, 2, [("weight", Prop::I64(1))], None) + .unwrap(); + g.add_edge(1, 1, 2, [("weight", Prop::I64(2))], None) + .unwrap(); + g.add_edge(2, 1, 2, [("weight", Prop::I64(3))], None) + .unwrap(); + + let exploded = g.edge(1, 2).unwrap().explode(); + + let res = exploded.map(|e| e.properties().as_vec()).collect_vec(); + + let mut expected = Vec::new(); + for i in 1..4 { + expected.push(vec![("weight".into(), Prop::I64(i))]); + } + + assert_eq!(res, expected); + + let e = g + .vertex(1) + .unwrap() + .edges() + .explode() + .map(|e| e.properties().as_vec()) + .collect_vec(); + assert_eq!(e, expected); + } + + #[test] + fn test_edge_earliest_latest() { + let g = Graph::new(); + g.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); + g.add_edge(1, 1, 2, NO_PROPS, None).unwrap(); + g.add_edge(2, 1, 2, NO_PROPS, None).unwrap(); + g.add_edge(0, 1, 3, NO_PROPS, None).unwrap(); + g.add_edge(1, 1, 3, NO_PROPS, None).unwrap(); + g.add_edge(2, 1, 3, NO_PROPS, None).unwrap(); + + let mut res = g.edge(1, 2).unwrap().earliest_time().unwrap(); + assert_eq!(res, 0); + + res = g.edge(1, 2).unwrap().latest_time().unwrap(); + assert_eq!(res, 2); + + res = g.at(1).edge(1, 2).unwrap().earliest_time().unwrap(); + assert_eq!(res, 0); + + res = g.at(1).edge(1, 2).unwrap().latest_time().unwrap(); + assert_eq!(res, 1); + + let res_list: Vec = g + .vertex(1) + .unwrap() + .edges() + .earliest_time() + .flatten() + .collect(); + assert_eq!(res_list, vec![0, 0]); + + let res_list: Vec = g + .vertex(1) + .unwrap() + .edges() + .latest_time() + .flatten() + .collect(); + assert_eq!(res_list, vec![2, 2]); + + let res_list: Vec = g + .vertex(1) + .unwrap() + .at(1) + .edges() + .earliest_time() + .flatten() + .collect(); + assert_eq!(res_list, vec![0, 0]); + + let res_list: Vec = g + .vertex(1) + .unwrap() + .at(1) + .edges() + .latest_time() + .flatten() + .collect(); + assert_eq!(res_list, vec![1, 1]); + } + + #[test] + fn check_vertex_history() { + let g = Graph::new(); + + g.add_vertex(1, 1, NO_PROPS).unwrap(); + g.add_vertex(2, 1, NO_PROPS).unwrap(); + g.add_vertex(3, 1, NO_PROPS).unwrap(); + g.add_vertex(4, 1, NO_PROPS).unwrap(); + g.add_vertex(8, 1, NO_PROPS).unwrap(); + + g.add_vertex(4, "Lord Farquaad", NO_PROPS).unwrap(); + g.add_vertex(6, "Lord Farquaad", NO_PROPS).unwrap(); + g.add_vertex(7, "Lord Farquaad", NO_PROPS).unwrap(); + g.add_vertex(8, "Lord Farquaad", NO_PROPS).unwrap(); + + let times_of_one = g.vertex(1).unwrap().history(); + let times_of_farquaad = g.vertex("Lord Farquaad").unwrap().history(); + + assert_eq!(times_of_one, [1, 2, 3, 4, 8]); + assert_eq!(times_of_farquaad, [4, 6, 7, 8]); + + let view = g.window(1, 8); + + let windowed_times_of_one = view.vertex(1).unwrap().history(); + let windowed_times_of_farquaad = view.vertex("Lord Farquaad").unwrap().history(); + assert_eq!(windowed_times_of_one, [1, 2, 3, 4]); + assert_eq!(windowed_times_of_farquaad, [4, 6, 7]); + } + + #[test] + fn check_edge_history() { + let g = Graph::new(); + + g.add_edge(1, 1, 2, NO_PROPS, None).unwrap(); + g.add_edge(2, 1, 3, NO_PROPS, None).unwrap(); + g.add_edge(3, 1, 2, NO_PROPS, None).unwrap(); + g.add_edge(4, 1, 4, NO_PROPS, None).unwrap(); + + let times_of_onetwo = g.edge(1, 2).unwrap().history(); + let times_of_four = g.edge(1, 4).unwrap().window(1, 5).history(); + let view = g.window(2, 5); + let windowed_times_of_four = view.edge(1, 4).unwrap().window(2, 4).history(); + + assert_eq!(times_of_onetwo, [1, 3]); + assert_eq!(times_of_four, [4]); + assert!(windowed_times_of_four.is_empty()); + } + + #[test] + fn check_edge_history_on_multiple_shards() { + let g = Graph::new(); + + g.add_edge(1, 1, 2, NO_PROPS, None).unwrap(); + g.add_edge(2, 1, 3, NO_PROPS, None).unwrap(); + g.add_edge(3, 1, 2, NO_PROPS, None).unwrap(); + g.add_edge(4, 1, 4, NO_PROPS, None).unwrap(); + g.add_edge(5, 1, 4, NO_PROPS, None).unwrap(); + g.add_edge(6, 1, 4, NO_PROPS, None).unwrap(); + g.add_edge(7, 1, 4, NO_PROPS, None).unwrap(); + g.add_edge(8, 1, 4, NO_PROPS, None).unwrap(); + g.add_edge(9, 1, 4, NO_PROPS, None).unwrap(); + g.add_edge(10, 1, 4, NO_PROPS, None).unwrap(); + + let times_of_onetwo = g.edge(1, 2).unwrap().history(); + let times_of_four = g.edge(1, 4).unwrap().window(1, 5).history(); + let times_of_outside_window = g.edge(1, 4).unwrap().window(1, 4).history(); + let times_of_four_higher = g.edge(1, 4).unwrap().window(6, 11).history(); + + let view = g.window(1, 11); + let windowed_times_of_four = view.edge(1, 4).unwrap().window(2, 5).history(); + let windowed_times_of_four_higher = view.edge(1, 4).unwrap().window(8, 11).history(); + + assert_eq!(times_of_onetwo, [1, 3]); + assert_eq!(times_of_four, [4]); + assert_eq!(times_of_four_higher, [6, 7, 8, 9, 10]); + assert!(times_of_outside_window.is_empty()); + assert_eq!(windowed_times_of_four, [4]); + assert_eq!(windowed_times_of_four_higher, [8, 9, 10]); + } + + #[test] + fn check_vertex_history_multiple_shards() { + let g = Graph::new(); + + g.add_vertex(1, 1, NO_PROPS).unwrap(); + g.add_vertex(2, 1, NO_PROPS).unwrap(); + g.add_vertex(3, 1, NO_PROPS).unwrap(); + g.add_vertex(4, 1, NO_PROPS).unwrap(); + g.add_vertex(5, 2, NO_PROPS).unwrap(); + g.add_vertex(6, 2, NO_PROPS).unwrap(); + g.add_vertex(7, 2, NO_PROPS).unwrap(); + g.add_vertex(8, 1, NO_PROPS).unwrap(); + g.add_vertex(9, 2, NO_PROPS).unwrap(); + g.add_vertex(10, 2, NO_PROPS).unwrap(); + + g.add_vertex(4, "Lord Farquaad", NO_PROPS).unwrap(); + g.add_vertex(6, "Lord Farquaad", NO_PROPS).unwrap(); + g.add_vertex(7, "Lord Farquaad", NO_PROPS).unwrap(); + g.add_vertex(8, "Lord Farquaad", NO_PROPS).unwrap(); + + let times_of_one = g.vertex(1).unwrap().history(); + let times_of_farquaad = g.vertex("Lord Farquaad").unwrap().history(); + let times_of_upper = g.vertex(2).unwrap().history(); + + assert_eq!(times_of_one, [1, 2, 3, 4, 8]); + assert_eq!(times_of_farquaad, [4, 6, 7, 8]); + assert_eq!(times_of_upper, [5, 6, 7, 9, 10]); + + let view = g.window(1, 8); + let windowed_times_of_one = view.vertex(1).unwrap().history(); + let windowed_times_of_two = view.vertex(2).unwrap().history(); + let windowed_times_of_farquaad = view.vertex("Lord Farquaad").unwrap().history(); + + assert_eq!(windowed_times_of_one, [1, 2, 3, 4]); + assert_eq!(windowed_times_of_farquaad, [4, 6, 7]); + assert_eq!(windowed_times_of_two, [5, 6, 7]); + } + + #[derive(Debug)] + struct CustomTime<'a>(&'a str, &'a str); + + impl<'a> TryIntoTime for CustomTime<'a> { + fn try_into_time(self) -> Result { + let CustomTime(time, fmt) = self; + let time = NaiveDateTime::parse_from_str(time, fmt)?; + let time = time.timestamp_millis(); + Ok(time) + } + } + + #[test] + fn test_ingesting_timestamps() { + let earliest_time = "2022-06-06 12:34:00".try_into_time().unwrap(); + let latest_time = "2022-06-07 12:34:00".try_into_time().unwrap(); + + let g = Graph::new(); + g.add_vertex("2022-06-06T12:34:00.000", 0, NO_PROPS) + .unwrap(); + g.add_edge("2022-06-07T12:34:00", 1, 2, NO_PROPS, None) + .unwrap(); + assert_eq!(g.earliest_time().unwrap(), earliest_time); + assert_eq!(g.latest_time().unwrap(), latest_time); + + let g = Graph::new(); + let fmt = "%Y-%m-%d %H:%M"; + + g.add_vertex(CustomTime("2022-06-06 12:34", fmt), 0, NO_PROPS) + .unwrap(); + g.add_edge(CustomTime("2022-06-07 12:34", fmt), 1, 2, NO_PROPS, None) + .unwrap(); + assert_eq!(g.earliest_time().unwrap(), earliest_time); + assert_eq!(g.latest_time().unwrap(), latest_time); + } + + #[test] + fn test_prop_display_str() { + let mut prop = Prop::Str("hello".into()); + assert_eq!(format!("{}", prop), "hello"); + + prop = Prop::I32(42); + assert_eq!(format!("{}", prop), "42"); + + prop = Prop::I64(9223372036854775807); + assert_eq!(format!("{}", prop), "9223372036854775807"); + + prop = Prop::U32(4294967295); + assert_eq!(format!("{}", prop), "4294967295"); + + prop = Prop::U64(18446744073709551615); + assert_eq!(format!("{}", prop), "18446744073709551615"); + + prop = Prop::U8(255); + assert_eq!(format!("{}", prop), "255"); + + prop = Prop::U16(65535); + assert_eq!(format!("{}", prop), "65535"); + + prop = Prop::F32(3.14159); + assert_eq!(format!("{}", prop), "3.14159"); + + prop = Prop::F64(3.141592653589793); + assert_eq!(format!("{}", prop), "3.141592653589793"); + + prop = Prop::Bool(true); + assert_eq!(format!("{}", prop), "true"); + } + + #[quickcheck] + fn test_graph_constant_props(u64_props: HashMap) -> bool { + let g = Graph::new(); + + let as_props = u64_props + .into_iter() + .map(|(name, value)| (name, Prop::U64(value))) + .collect::>(); + + g.add_constant_properties(as_props.clone()).unwrap(); + + let props_map = as_props.into_iter().collect::>(); + + props_map + .into_iter() + .all(|(name, value)| g.properties().constant().get(&name).unwrap() == value) + } + + #[quickcheck] + fn test_graph_constant_props_names(u64_props: HashMap) -> bool { + let g = Graph::new(); + + let as_props = u64_props + .into_iter() + .map(|(name, value)| (name.into(), Prop::U64(value))) + .collect::>(); + + g.add_constant_properties(as_props.clone()).unwrap(); + + let props_names = as_props + .into_iter() + .map(|(name, _)| name) + .collect::>(); + + g.properties() + .constant() + .keys() + .into_iter() + .collect::>() + == props_names + } + + #[quickcheck] + fn test_graph_temporal_props(str_props: HashMap) -> bool { + let g = Graph::new(); + + let (t0, t1) = (1, 2); + + let (t0_props, t1_props): (Vec<_>, Vec<_>) = str_props + .iter() + .enumerate() + .map(|(i, props)| { + let (name, value) = props; + let value = Prop::from(value); + (name.as_str().into(), value, i % 2) + }) + .partition(|(_, _, i)| *i == 0); + + let t0_props: HashMap = t0_props + .into_iter() + .map(|(name, value, _)| (name, value)) + .collect(); + + let t1_props: HashMap = t1_props + .into_iter() + .map(|(name, value, _)| (name, value)) + .collect(); + + g.add_properties(t0, t0_props.clone()).unwrap(); + g.add_properties(t1, t1_props.clone()).unwrap(); + + let check = t0_props.iter().all(|(name, value)| { + g.properties().temporal().get(name).unwrap().at(t0) == Some(value.clone()) + }) && t1_props.iter().all(|(name, value)| { + g.properties().temporal().get(name).unwrap().at(t1) == Some(value.clone()) + }); + if !check { + println!("failed time-specific comparison for {:?}", str_props); + return false; + } + let check = check + && g.at(t0) + .properties() + .temporal() + .iter_latest() + .map(|(k, v)| (k.clone(), v)) + .collect::>() + == t0_props; + if !check { + println!("failed latest value comparison for {:?} at t0", str_props); + return false; + } + let check = check + && t1_props.iter().all(|(k, ve)| { + g.at(t1) + .properties() + .temporal() + .get(k) + .and_then(|v| v.latest()) + == Some(ve.clone()) + }); + if !check { + println!("failed latest value comparison for {:?} at t1", str_props); + return false; + } + check + } + + #[test] + fn test_temporral_edge_props_window() { + let g = Graph::new(); + g.add_edge(1, 1, 2, vec![("weight".to_string(), Prop::I64(1))], None) + .unwrap(); + g.add_edge(2, 1, 2, vec![("weight".to_string(), Prop::I64(2))], None) + .unwrap(); + g.add_edge(3, 1, 2, vec![("weight".to_string(), Prop::I64(3))], None) + .unwrap(); + + let e = g.vertex(1).unwrap().out_edges().next().unwrap(); + let res: HashMap> = e + .window(1, 3) + .properties() + .temporal() + .iter() + .map(|(k, v)| (k.clone(), v.iter().collect())) + .collect(); + + let mut exp = HashMap::new(); + exp.insert( + ArcStr::from("weight"), + vec![(1, Prop::I64(1)), (2, Prop::I64(2))], + ); + assert_eq!(res, exp); + } + + #[test] + fn test_vertex_early_late_times() { + let g = Graph::new(); + g.add_vertex(1, 1, NO_PROPS).unwrap(); + g.add_vertex(2, 1, NO_PROPS).unwrap(); + g.add_vertex(3, 1, NO_PROPS).unwrap(); + + assert_eq!(g.vertex(1).unwrap().earliest_time(), Some(1)); + assert_eq!(g.vertex(1).unwrap().latest_time(), Some(3)); + + assert_eq!(g.at(2).vertex(1).unwrap().earliest_time(), Some(1)); + assert_eq!(g.at(2).vertex(1).unwrap().latest_time(), Some(2)); + } + + #[test] + fn test_vertex_ids() { + let g = Graph::new(); + g.add_vertex(1, 1, NO_PROPS).unwrap(); + g.add_vertex(1, 2, NO_PROPS).unwrap(); + g.add_vertex(2, 3, NO_PROPS).unwrap(); + + assert_eq!(g.vertices().id().collect::>(), vec![1, 2, 3]); + + let g_at = g.at(1); + assert_eq!(g_at.vertices().id().collect::>(), vec![1, 2]); + } + + #[test] + fn test_edge_layer_name() -> Result<(), GraphError> { + let g = Graph::new(); + g.add_edge(0, 0, 1, NO_PROPS, None)?; + g.add_edge(0, 0, 1, NO_PROPS, Some("awesome name"))?; + + let what = g + .edges() + .map(|e| (e.src().id(), e.dst().id())) + .collect_vec(); + assert_eq!(what, vec![(0, 1)]); + + let layer_names = g + .edges() + .flat_map(|e| e.layer_names()) + .sorted() + .collect_vec(); + assert_eq!(layer_names, vec!["_default", "awesome name"]); + Ok(()) + } + + #[test] + fn test_edge_from_single_layer() { + let g = Graph::new(); + g.add_edge(0, 1, 2, NO_PROPS, Some("layer")).unwrap(); + + assert!(g.edge(1, 2).is_some()); + assert!(g.layer("layer").unwrap().edge(1, 2).is_some()) + } + + #[test] + fn test_edge_layer_intersect_layer() { + let g = Graph::new(); + + g.add_edge(1, 1, 2, NO_PROPS, Some("layer1")) + .expect("add edge"); + g.add_edge(1, 1, 3, NO_PROPS, Some("layer3")) + .expect("add edge"); + g.add_edge(1, 1, 4, NO_PROPS, None).expect("add edge"); + + let g_layers = g.layer(vec!["layer1", "layer3"]).expect("layer"); + + assert!(g_layers.edge(1, 2).unwrap().layer("layer1").is_some()); + assert!(g_layers.edge(1, 3).unwrap().layer("layer3").is_some()); + assert!(g_layers.edge(1, 2).is_some()); + assert!(g_layers.edge(1, 3).is_some()); + + assert!(g_layers.edge(1, 4).is_none()); + + let one = g_layers.vertex(1).expect("vertex"); + let ns = one.neighbours().iter().map(|v| v.id()).collect::>(); + assert_eq!(ns, vec![2, 3]); + + let g_layers2 = g_layers.layer(vec!["layer1"]).expect("layer"); + + assert!(g_layers2.edge(1, 2).unwrap().layer("layer1").is_some()); + assert!(g_layers2.edge(1, 2).is_some()); + + assert!(g_layers2.edge(1, 3).is_none()); + + assert!(g_layers2.edge(1, 4).is_none()); + + let one = g_layers2.vertex(1).expect("vertex"); + let ns = one.neighbours().iter().map(|v| v.id()).collect::>(); + assert_eq!(ns, vec![2]); + } + + #[test] + fn simple_triangle() { + let g = Graph::new(); + + let vs = vec![(1, 1, 2), (2, 1, 3), (3, 2, 1), (4, 3, 2)]; + + for (t, src, dst) in &vs { + g.add_edge(*t, *src, *dst, NO_PROPS, None).unwrap(); + } + + let windowed_graph = g.window(0, 5); + let one = windowed_graph.vertex(1).expect("vertex"); + let ns_win = one.neighbours().id().collect::>(); + + let one = g.vertex(1).expect("vertex"); + let ns = one.neighbours().id().collect::>(); + assert_eq!(ns, vec![2, 3]); + assert_eq!(ns_win, ns); + } + + #[test] + fn test_layer_explode() { + let g = Graph::new(); + g.add_edge(0, 1, 2, NO_PROPS, Some("layer1")).unwrap(); + g.add_edge(1, 1, 2, NO_PROPS, Some("layer2")).unwrap(); + g.add_edge(2, 1, 2, NO_PROPS, Some("layer1")).unwrap(); + g.add_edge(3, 1, 2, NO_PROPS, None).unwrap(); + + let e = g.edge(1, 2).expect("edge"); + + let layer_exploded = e + .explode_layers() + .filter_map(|e| { + e.edge + .layer() + .copied() + .map(|layer| (e.src().id(), e.dst().id(), layer)) + }) + .collect::>(); + + assert_eq!(layer_exploded, vec![(1, 2, 0), (1, 2, 1), (1, 2, 2),]); + } + + #[test] + fn test_layer_explode_window() { + let g = Graph::new(); + g.add_edge(0, 1, 2, NO_PROPS, Some("layer1")).unwrap(); + g.add_edge(1, 1, 2, NO_PROPS, Some("layer2")).unwrap(); + g.add_edge(2, 1, 2, NO_PROPS, Some("layer1")).unwrap(); + g.add_edge(3, 1, 2, NO_PROPS, None).unwrap(); + + let g = g.window(0, 3); + let e = g.edge(1, 2).expect("edge"); + + let layer_exploded = e + .explode_layers() + .filter_map(|e| { + e.edge + .layer() + .copied() + .map(|layer| (e.src().id(), e.dst().id(), layer)) + }) + .collect::>(); + + assert_eq!(layer_exploded, vec![(1, 2, 1), (1, 2, 2),]); + } + + #[test] + fn test_layer_explode_stacking() { + let g = Graph::new(); + g.add_edge(0, 1, 2, NO_PROPS, Some("layer1")).unwrap(); + g.add_edge(1, 1, 2, NO_PROPS, Some("layer2")).unwrap(); + g.add_edge(2, 1, 2, NO_PROPS, Some("layer1")).unwrap(); + g.add_edge(3, 1, 2, NO_PROPS, None).unwrap(); + + let e = g.edge(1, 2).expect("edge"); + + let layer_exploded = e + .explode_layers() + .flat_map(|e| { + e.explode().filter_map(|e| { + e.edge + .layer() + .zip(e.time()) + .map(|(layer, t)| (t, e.src().id(), e.dst().id(), *layer)) + }) + }) + .collect::>(); + + assert_eq!( + layer_exploded, + vec![(3, 1, 2, 0), (0, 1, 2, 1), (2, 1, 2, 1), (1, 1, 2, 2),] + ); + } + + #[test] + fn test_layer_explode_stacking_window() { + let g = Graph::new(); + g.add_edge(0, 1, 2, NO_PROPS, Some("layer1")).unwrap(); + g.add_edge(1, 1, 2, NO_PROPS, Some("layer2")).unwrap(); + g.add_edge(2, 1, 2, NO_PROPS, Some("layer1")).unwrap(); + g.add_edge(3, 1, 2, NO_PROPS, None).unwrap(); + + let g = g.window(0, 3); + let e = g.edge(1, 2).expect("edge"); + + let layer_exploded = e + .explode_layers() + .flat_map(|e| { + e.explode().filter_map(|e| { + e.edge + .layer() + .zip(e.time()) + .map(|(layer, t)| (t, e.src().id(), e.dst().id(), *layer)) + }) + }) + .collect::>(); + + assert_eq!( + layer_exploded, + vec![(0, 1, 2, 1), (2, 1, 2, 1), (1, 1, 2, 2),] + ); + } + + #[test] + fn test_multiple_layers_fundamentals() { + let g = Graph::new(); + + g.add_edge(1, 1, 2, [("tx_sent", 10u64)], "btc".into()) + .expect("failed"); + g.add_edge(1, 1, 2, [("tx_sent", 20u64)], "eth".into()) + .expect("failed"); + g.add_edge(1, 1, 2, [("tx_sent", 70u64)], "tether".into()) + .expect("failed"); + + let e = g.edge(1, 2).expect("failed to get edge"); + let sum: u64 = e + .properties() + .temporal() + .get("tx_sent") + .unwrap() + .iter() + .filter_map(|(_, prop)| prop.into_u64()) + .sum(); + + assert_eq!(sum, 100); + + let lg = g.layer(vec!["eth", "btc"]).expect("failed to layer graph"); + + let e = lg.edge(1, 2).expect("failed to get edge"); + + let sum_eth_btc: u64 = e + .properties() + .temporal() + .get("tx_sent") + .unwrap() + .iter() + .filter_map(|(_, prop)| prop.into_u64()) + .sum(); + + assert_eq!(sum_eth_btc, 30); + + assert_eq!(lg.count_edges(), 1); + + let e = g.edge(1, 2).expect("failed to get edge"); + + let e_btc = e.layer("btc").expect("failed to get btc layer"); + let e_eth = e.layer("eth").expect("failed to get eth layer"); + + let edge_btc_sum = e_btc + .properties() + .temporal() + .get("tx_sent") + .unwrap() + .iter() + .filter_map(|(_, prop)| prop.into_u64()) + .sum::(); + + let edge_eth_sum = e_eth + .properties() + .temporal() + .get("tx_sent") + .unwrap() + .iter() + .filter_map(|(_, prop)| prop.into_u64()) + .sum::(); + + assert!(edge_btc_sum < edge_eth_sum); + + let e_eth = e_eth + .layer(vec!["eth", "btc"]) + .expect("failed to get eth,btc layers"); + + let eth_sum = e_eth + .properties() + .temporal() + .get("tx_sent") + .unwrap() + .iter() + .filter_map(|(_, prop)| prop.into_u64()) + .sum::(); + + // layer does not have a way to reset yet! + assert_eq!(eth_sum, 20); + } + + #[test] + fn test_unique_layers() { + let g = Graph::new(); + g.add_edge(0, 1, 2, NO_PROPS, Some("layer1")).unwrap(); + g.add_edge(0, 1, 2, NO_PROPS, Some("layer2")).unwrap(); + assert_eq!( + g.layer("layer2").unwrap().unique_layers().collect_vec(), + vec!["layer2"] + ) + } + + #[quickcheck] + fn vertex_from_id_is_consistent(vertices: Vec) -> bool { + let g = Graph::new(); + for v in vertices.iter() { + g.add_vertex(0, *v, NO_PROPS).unwrap(); + } + g.vertices() + .name() + .map(|name| g.vertex(name)) + .all(|v| v.is_some()) + } + + #[quickcheck] + fn exploded_edge_times_is_consistent(edges: Vec<(u64, u64, Vec)>, offset: i64) -> bool { + check_exploded_edge_times_is_consistent(edges, offset) + } + + #[test] + fn exploded_edge_times_is_consistent_1() { + let edges = vec![(0, 0, vec![0, 1])]; + assert!(check_exploded_edge_times_is_consistent(edges, 0)); + } + + fn check_exploded_edge_times_is_consistent( + edges: Vec<(u64, u64, Vec)>, + offset: i64, + ) -> bool { + let mut correct = true; + let mut check = |condition: bool, message: String| { + if !condition { + println!("Failed: {}", message); + } + correct = correct && condition; + }; + // checks that exploded edges are preserved with correct timestamps + let mut edges: Vec<(u64, u64, Vec)> = + edges.into_iter().filter(|e| !e.2.is_empty()).collect(); + // discard edges without timestamps + for e in edges.iter_mut() { + e.2.sort(); + // FIXME: Should not have to do this, see issue https://github.com/Pometry/Raphtory/issues/973 + e.2.dedup(); // add each timestamp only once (multi-edge per timestamp currently not implemented) + } + edges.sort(); + edges.dedup_by_key(|(src, dst, _)| (*src, *dst)); + + let g = Graph::new(); + for (src, dst, times) in edges.iter() { + for t in times.iter() { + g.add_edge(*t, *src, *dst, NO_PROPS, None).unwrap(); + } + } + + let mut actual_edges: Vec<(u64, u64, Vec)> = g + .edges() + .map(|e| { + ( + e.src().id(), + e.dst().id(), + e.explode() + .map(|ee| { + check( + ee.earliest_time() == ee.latest_time(), + format!("times mismatched for {:?}", ee), + ); // times are the same for exploded edge + let t = ee.earliest_time().unwrap(); + check( + ee.active(t), + format!("exploded edge {:?} inactive at {}", ee, t), + ); + if t < i64::MAX { + // window is broken at MAX! + check(e.active(t), format!("edge {:?} inactive at {}", e, t)); + } + let t_test = t.saturating_add(offset); + if t_test != t && t_test < i64::MAX && t_test > i64::MIN { + check( + !ee.active(t_test), + format!("exploded edge {:?} active at {}", ee, t_test), + ); + } + t + }) + .collect(), + ) + }) + .collect(); + + for e in actual_edges.iter_mut() { + e.2.sort(); + } + actual_edges.sort(); + check( + actual_edges == edges, + format!( + "actual edges didn't match input actual: {:?}, expected: {:?}", + actual_edges, edges + ), + ); + correct + } + + // non overlaping time intervals + #[derive(Clone, Debug)] + struct Intervals(Vec<(i64, i64)>); + + impl Arbitrary for Intervals { + fn arbitrary(g: &mut quickcheck::Gen) -> Self { + let mut some_nums = Vec::::arbitrary(g); + some_nums.sort(); + let intervals = some_nums + .into_iter() + .tuple_windows() + .filter(|(a, b)| a != b) + .collect_vec(); + Intervals(intervals) + } + } +} diff --git a/raphtory/src/db/graph/mod.rs b/raphtory/src/db/graph/mod.rs new file mode 100644 index 0000000000..0c529d4c55 --- /dev/null +++ b/raphtory/src/db/graph/mod.rs @@ -0,0 +1,6 @@ +pub mod edge; +pub mod graph; +pub mod path; +pub mod vertex; +pub mod vertices; +pub mod views; diff --git a/raphtory/src/db/path.rs b/raphtory/src/db/graph/path.rs similarity index 64% rename from raphtory/src/db/path.rs rename to raphtory/src/db/graph/path.rs index 8953c2f0d0..5fcb4e80e3 100644 --- a/raphtory/src/db/path.rs +++ b/raphtory/src/db/graph/path.rs @@ -1,16 +1,23 @@ -use crate::core::time::IntoTime; -use crate::core::vertex_ref::{LocalVertexRef, VertexRef}; -use crate::core::{Direction, Prop}; -use crate::db::edge::EdgeView; -use crate::db::graph_layer::LayeredGraph; -use crate::db::graph_window::WindowedGraph; -use crate::db::vertex::VertexView; -use crate::db::view_api::layer::LayerOps; -use crate::db::view_api::BoxedIter; -use crate::db::view_api::*; -use std::collections::HashMap; -use std::iter; -use std::sync::Arc; +use crate::{ + core::{ + entities::{vertices::vertex_ref::VertexRef, VID}, + utils::time::IntoTime, + Direction, + }, + db::{ + api::{ + properties::Properties, + view::{internal::extend_filter, BoxedIter, Layer, LayerOps}, + }, + graph::{ + edge::EdgeView, + vertex::VertexView, + views::{layer_graph::LayeredGraph, window_graph::WindowedGraph}, + }, + }, + prelude::*, +}; +use std::{iter, sync::Arc}; #[derive(Copy, Clone)] pub enum Operations { @@ -28,25 +35,27 @@ impl Operations { fn op( self, graph: G, - iter: Box + Send>, - ) -> Box + Send> { + iter: Box + Send>, + ) -> Box + Send> { + let layer_ids = graph.layer_ids(); + let edge_filter = graph.edge_filter().cloned(); match self { Operations::Neighbours { dir } => Box::new(iter.flat_map(move |v| { - graph.neighbours(graph.localise_vertex_unchecked(v), dir, None) + graph.neighbours(v, dir, layer_ids.clone(), edge_filter.as_ref()) })), Operations::NeighboursWindow { dir, t_start, t_end, - } => Box::new(iter.flat_map(move |v| { - graph.neighbours_window( - graph.localise_vertex_unchecked(v), - t_start, - t_end, - dir, - None, - ) - })), + } => { + let graph1 = graph.clone(); + let filter = Some(extend_filter(edge_filter, move |e, l| { + graph1.include_edge_window(e, t_start..t_end, l) + })); + Box::new(iter.flat_map(move |v| { + graph.neighbours(v, dir, layer_ids.clone(), filter.as_ref()) + })) + } } } } @@ -58,7 +67,7 @@ pub struct PathFromGraph { } impl PathFromGraph { - pub(crate) fn new(graph: G, operation: Operations) -> PathFromGraph { + pub fn new(graph: G, operation: Operations) -> PathFromGraph { PathFromGraph { graph, operations: Arc::new(vec![operation]), @@ -68,11 +77,14 @@ impl PathFromGraph { pub fn iter(&self) -> Box> + Send> { let g = self.graph.clone(); let ops = self.operations.clone(); - Box::new(g.vertex_refs().map(move |v| PathFromVertex { - graph: g.clone(), - vertex: v, - operations: ops.clone(), - })) + Box::new( + g.vertex_refs(g.layer_ids(), g.edge_filter()) + .map(move |v| PathFromVertex { + graph: g.clone(), + vertex: v, + operations: ops.clone(), + }), + ) } } @@ -98,24 +110,6 @@ impl VertexViewOps for PathFromGraph { Box::new(self.iter().map(|it| it.latest_time())) } - fn property( - &self, - name: String, - include_static: bool, - ) -> Box> + Send>> + Send> { - Box::new( - self.iter() - .map(move |it| it.property(name.clone(), include_static.clone())), - ) - } - - fn property_history( - &self, - name: String, - ) -> Box> + Send>> + Send> { - Box::new(self.iter().map(move |it| it.property_history(name.clone()))) - } - fn history( &self, ) -> Box> + Send>> + Send> { @@ -124,54 +118,9 @@ impl VertexViewOps for PathFromGraph { fn properties( &self, - include_static: bool, - ) -> Box> + Send>> + Send> + ) -> Box>> + Send>> + Send> { - Box::new(self.iter().map(move |it| it.properties(include_static))) - } - - fn property_histories( - &self, - ) -> Box< - dyn Iterator>> + Send>> - + Send, - > { - Box::new(self.iter().map(|it| it.property_histories())) - } - - fn property_names( - &self, - include_static: bool, - ) -> Box> + Send>> + Send> { - Box::new(self.iter().map(move |it| it.property_names(include_static))) - } - - fn has_property( - &self, - name: String, - include_static: bool, - ) -> Box + Send>> + Send> { - Box::new( - self.iter() - .map(move |it| it.has_property(name.clone(), include_static)), - ) - } - - fn has_static_property( - &self, - name: String, - ) -> Box + Send>> + Send> { - Box::new( - self.iter() - .map(move |it| it.has_static_property(name.clone())), - ) - } - - fn static_property( - &self, - name: String, - ) -> Box> + Send>> + Send> { - Box::new(self.iter().map(move |it| it.static_property(name.clone()))) + Box::new(self.iter().map(move |it| it.properties())) } fn degree(&self) -> Box + Send>> + Send> { @@ -264,7 +213,7 @@ impl LayerOps for PathFromGraph { } } - fn layer(&self, name: &str) -> Option { + fn layer>(&self, name: L) -> Option { Some(PathFromGraph { graph: self.graph.layer(name)?, operations: self.operations.clone(), @@ -275,15 +224,13 @@ impl LayerOps for PathFromGraph { #[derive(Clone)] pub struct PathFromVertex { pub graph: G, - pub vertex: LocalVertexRef, + pub vertex: VID, pub operations: Arc>, } impl PathFromVertex { - - pub fn iter_refs(&self) -> Box + Send> { - let init: Box + Send> = - Box::new(iter::once(VertexRef::Local(self.vertex))); + pub fn iter_refs(&self) -> Box + Send> { + let init: Box + Send> = Box::new(iter::once(self.vertex)); let g = self.graph.clone(); let ops = self.operations.clone(); let iter = ops @@ -294,17 +241,18 @@ impl PathFromVertex { pub fn iter(&self) -> Box> + Send> { let g = self.graph.clone(); - let iter = self.iter_refs() - .map(move |v| VertexView::new(g.clone(), v)); + let iter = self + .iter_refs() + .map(move |v| VertexView::new_internal(g.clone(), v)); Box::new(iter) } - pub(crate) fn new>( + pub fn new>( graph: G, vertex: V, operation: Operations, ) -> PathFromVertex { - let v = graph.localise_vertex_unchecked(vertex.into()); + let v = graph.internalise_vertex_unchecked(vertex.into()); PathFromVertex { graph, vertex: v, @@ -312,9 +260,13 @@ impl PathFromVertex { } } - pub(crate) fn neighbours_window(&self, dir:Direction, t_start: i64, t_end:i64) -> Self { + pub fn neighbours_window(&self, dir: Direction, t_start: i64, t_end: i64) -> Self { let mut new_ops = (*self.operations).clone(); - new_ops.push(Operations::NeighboursWindow { dir, t_start, t_end }); + new_ops.push(Operations::NeighboursWindow { + dir, + t_start, + t_end, + }); Self { graph: self.graph.clone(), vertex: self.vertex, @@ -345,40 +297,12 @@ impl VertexViewOps for PathFromVertex { self.iter().latest_time() } - fn property(&self, name: String, include_static: bool) -> Self::ValueType> { - self.iter().property(name, include_static) - } - - fn property_history(&self, name: String) -> Self::ValueType> { - self.iter().property_history(name) - } - fn history(&self) -> Self::ValueType> { self.iter().history() } - fn properties(&self, include_static: bool) -> Self::ValueType> { - self.iter().properties(include_static) - } - - fn property_histories(&self) -> Self::ValueType>> { - self.iter().property_histories() - } - - fn property_names(&self, include_static: bool) -> Self::ValueType> { - self.iter().property_names(include_static) - } - - fn has_property(&self, name: String, include_static: bool) -> Self::ValueType { - self.iter().has_property(name, include_static) - } - - fn has_static_property(&self, name: String) -> Self::ValueType { - self.iter().has_static_property(name) - } - - fn static_property(&self, name: String) -> Self::ValueType> { - self.iter().static_property(name) + fn properties(&self) -> Self::ValueType>> { + self.iter().properties() } fn degree(&self) -> Self::ValueType { @@ -416,7 +340,6 @@ impl VertexViewOps for PathFromVertex { } } - fn in_neighbours(&self) -> Self { let mut new_ops = (*self.operations).clone(); let dir = Direction::IN; @@ -471,7 +394,7 @@ impl LayerOps for PathFromVertex { } } - fn layer(&self, name: &str) -> Option { + fn layer>(&self, name: L) -> Option { Some(PathFromVertex { graph: self.graph.layer(name)?, vertex: self.vertex, diff --git a/raphtory/src/db/graph/vertex.rs b/raphtory/src/db/graph/vertex.rs new file mode 100644 index 0000000000..25c8177d53 --- /dev/null +++ b/raphtory/src/db/graph/vertex.rs @@ -0,0 +1,567 @@ +//! Defines the `Vertex` struct, which represents a vertex in the graph. + +use crate::{ + core::{ + entities::{vertices::vertex_ref::VertexRef, VID}, + storage::timeindex::TimeIndexEntry, + utils::{errors::GraphError, time::IntoTime}, + ArcStr, Direction, + }, + db::{ + api::{ + mutation::{ + internal::{InternalAdditionOps, InternalPropertyAdditionOps}, + CollectProperties, TryIntoInputTime, + }, + properties::{ + internal::{ConstPropertiesOps, TemporalPropertiesOps, TemporalPropertyViewOps}, + Properties, + }, + view::{internal::Static, BoxedIter, Layer, LayerOps}, + }, + graph::{ + edge::{EdgeList, EdgeView}, + path::{Operations, PathFromVertex}, + views::{layer_graph::LayeredGraph, window_graph::WindowedGraph}, + }, + }, + prelude::*, +}; + +#[derive(Debug, Clone)] +pub struct VertexView { + pub graph: G, + pub vertex: VID, +} + +impl PartialEq> for VertexView { + fn eq(&self, other: &VertexView) -> bool { + self.id() == other.id() + } +} + +impl From> for VertexRef { + fn from(value: VertexView) -> Self { + VertexRef::Internal(value.vertex) + } +} + +impl From<&VertexView> for VertexRef { + fn from(value: &VertexView) -> Self { + VertexRef::Internal(value.vertex) + } +} + +impl VertexView { + /// Creates a new `VertexView` wrapping an internal vertex reference and a graph, internalising any global vertex ids. + pub fn new(graph: G, vertex: VertexRef) -> VertexView { + match vertex { + VertexRef::Internal(local) => Self::new_internal(graph, local), + _ => { + let v = graph.internalise_vertex_unchecked(vertex); + VertexView { graph, vertex: v } + } + } + } + + /// Creates a new `VertexView` wrapping an internal vertex reference and a graph + pub fn new_internal(graph: G, vertex: VID) -> VertexView { + VertexView { graph, vertex } + } +} + +impl TemporalPropertiesOps for VertexView { + fn get_temporal_prop_id(&self, name: &str) -> Option { + self.graph + .vertex_meta() + .temporal_prop_meta() + .get_id(name) + .filter(|id| self.graph.has_temporal_vertex_prop(self.vertex, *id)) + } + + fn get_temporal_prop_name(&self, id: usize) -> ArcStr { + self.graph.vertex_meta().temporal_prop_meta().get_name(id) + } + + fn temporal_prop_ids(&self) -> Box + '_> { + Box::new( + self.graph + .temporal_vertex_prop_ids(self.vertex) + .filter(|id| self.graph.has_temporal_vertex_prop(self.vertex, *id)), + ) + } +} + +impl TemporalPropertyViewOps for VertexView { + fn temporal_value(&self, id: usize) -> Option { + self.graph + .temporal_vertex_prop_vec(self.vertex, id) + .last() + .map(|(_, v)| v.to_owned()) + } + + fn temporal_history(&self, id: usize) -> Vec { + self.graph + .temporal_vertex_prop_vec(self.vertex, id) + .into_iter() + .map(|(t, _)| t) + .collect() + } + + fn temporal_values(&self, id: usize) -> Vec { + self.graph + .temporal_vertex_prop_vec(self.vertex, id) + .into_iter() + .map(|(_, v)| v) + .collect() + } + + fn temporal_value_at(&self, id: usize, t: i64) -> Option { + let history = self.temporal_history(id); + match history.binary_search(&t) { + Ok(index) => Some(self.temporal_values(id)[index].clone()), + Err(index) => (index > 0).then(|| self.temporal_values(id)[index - 1].clone()), + } + } +} + +impl ConstPropertiesOps for VertexView { + fn get_const_prop_id(&self, name: &str) -> Option { + self.graph.vertex_meta().const_prop_meta().get_id(name) + } + + fn get_const_prop_name(&self, id: usize) -> ArcStr { + self.graph.vertex_meta().const_prop_meta().get_name(id) + } + + fn const_prop_ids(&self) -> Box + '_> { + self.graph.constant_vertex_prop_ids(self.vertex) + } + + fn get_const_prop(&self, id: usize) -> Option { + self.graph.constant_vertex_prop(self.vertex, id) + } +} + +impl Static for VertexView {} + +/// View of a Vertex in a Graph +impl VertexViewOps for VertexView { + type Graph = G; + type ValueType = T; + type PathType<'a> = PathFromVertex where Self: 'a; + type EList = BoxedIter>; + + fn id(&self) -> u64 { + self.graph.vertex_id(self.vertex) + } + + fn name(&self) -> String { + self.graph.vertex_name(self.vertex) + } + + fn earliest_time(&self) -> Option { + self.graph.vertex_earliest_time(self.vertex) + } + + fn latest_time(&self) -> Option { + self.graph.vertex_latest_time(self.vertex) + } + + fn history(&self) -> Vec { + self.graph.vertex_history(self.vertex) + } + + fn properties(&self) -> Properties { + Properties::new(self.clone()) + } + + fn degree(&self) -> usize { + let dir = Direction::BOTH; + self.graph.degree( + self.vertex, + dir, + &self.graph.layer_ids(), + self.graph.edge_filter(), + ) + } + + fn in_degree(&self) -> usize { + let dir = Direction::IN; + self.graph.degree( + self.vertex, + dir, + &self.graph.layer_ids(), + self.graph.edge_filter(), + ) + } + + fn out_degree(&self) -> usize { + let dir = Direction::OUT; + self.graph.degree( + self.vertex, + dir, + &self.graph.layer_ids(), + self.graph.edge_filter(), + ) + } + + fn edges(&self) -> EdgeList { + let g = self.graph.clone(); + let dir = Direction::BOTH; + Box::new( + g.vertex_edges( + self.vertex, + dir, + self.graph.layer_ids(), + self.graph.edge_filter(), + ) + .map(move |e| EdgeView::new(g.clone(), e)), + ) + } + + fn in_edges(&self) -> EdgeList { + let g = self.graph.clone(); + let dir = Direction::IN; + Box::new( + g.vertex_edges( + self.vertex, + dir, + self.graph.layer_ids(), + self.graph.edge_filter(), + ) + .map(move |e| EdgeView::new(g.clone(), e)), + ) + } + + fn out_edges(&self) -> EdgeList { + let g = self.graph.clone(); + let dir = Direction::OUT; + Box::new( + g.vertex_edges( + self.vertex, + dir, + self.graph.layer_ids(), + self.graph.edge_filter(), + ) + .map(move |e| EdgeView::new(g.clone(), e)), + ) + } + + fn neighbours(&self) -> PathFromVertex { + let g = self.graph.clone(); + let dir = Direction::BOTH; + PathFromVertex::new(g, self, Operations::Neighbours { dir }) + } + + fn in_neighbours(&self) -> PathFromVertex { + let g = self.graph.clone(); + let dir = Direction::IN; + PathFromVertex::new(g, self, Operations::Neighbours { dir }) + } + + fn out_neighbours(&self) -> PathFromVertex { + let g = self.graph.clone(); + let dir = Direction::OUT; + PathFromVertex::new(g, self, Operations::Neighbours { dir }) + } +} + +impl TimeOps for VertexView { + type WindowedViewType = VertexView>; + + fn start(&self) -> Option { + self.graph.start() + } + + fn end(&self) -> Option { + self.graph.end() + } + + fn window(&self, t_start: T, t_end: T) -> Self::WindowedViewType { + VertexView { + graph: self.graph.window(t_start, t_end), + vertex: self.vertex, + } + } +} + +impl LayerOps for VertexView { + type LayeredViewType = VertexView>; + + fn default_layer(&self) -> Self::LayeredViewType { + VertexView { + graph: self.graph.default_layer(), + vertex: self.vertex, + } + } + + fn layer>(&self, name: L) -> Option { + Some(VertexView { + graph: self.graph.layer(name)?, + vertex: self.vertex, + }) + } +} + +impl VertexView { + pub fn add_constant_properties( + &self, + props: C, + ) -> Result<(), GraphError> { + let properties: Vec<(usize, Prop)> = props.collect_properties( + |name, dtype| self.graph.resolve_vertex_property(name, dtype, true), + |prop| self.graph.process_prop_value(prop), + )?; + self.graph + .internal_add_constant_vertex_properties(self.vertex, properties) + } + + pub fn add_updates( + &self, + time: T, + props: C, + ) -> Result<(), GraphError> { + let t = TimeIndexEntry::from_input(&self.graph, time)?; + let properties: Vec<(usize, Prop)> = props.collect_properties( + |name, dtype| self.graph.resolve_vertex_property(name, dtype, false), + |prop| self.graph.process_prop_value(prop), + )?; + self.graph.internal_add_vertex(t, self.vertex, properties) + } +} + +/// Implementation of the VertexListOps trait for an iterator of VertexView objects. +/// +impl VertexListOps for Box> + Send> { + type Graph = G; + type Vertex = VertexView; + type IterType = Box + Send>; + type EList = Box> + Send>; + type ValueType = T; + + fn earliest_time(self) -> BoxedIter> { + Box::new(self.map(|v| v.start())) + } + + fn latest_time(self) -> BoxedIter> { + Box::new(self.map(|v| v.end().map(|t| t - 1))) + } + + fn window(self, t_start: i64, t_end: i64) -> BoxedIter>> { + Box::new(self.map(move |v| v.window(t_start, t_end))) + } + + fn at(self, end: i64) -> Self::IterType<::WindowedViewType> { + Box::new(self.map(move |v| v.at(end))) + } + + fn id(self) -> BoxedIter { + Box::new(self.map(|v| v.id())) + } + + fn name(self) -> BoxedIter { + Box::new(self.map(|v| v.name())) + } + + fn properties(self) -> BoxedIter>> { + Box::new(self.map(move |v| v.properties())) + } + + fn history(self) -> BoxedIter> { + Box::new(self.map(|v| v.history())) + } + + fn degree(self) -> BoxedIter { + Box::new(self.map(|v| v.degree())) + } + + fn in_degree(self) -> BoxedIter { + Box::new(self.map(|v| v.in_degree())) + } + + fn out_degree(self) -> BoxedIter { + Box::new(self.map(|v| v.out_degree())) + } + + fn edges(self) -> Self::EList { + Box::new(self.flat_map(|v| v.edges())) + } + + fn in_edges(self) -> Self::EList { + Box::new(self.flat_map(|v| v.in_edges())) + } + + fn out_edges(self) -> Self::EList { + Box::new(self.flat_map(|v| v.out_edges())) + } + + fn neighbours(self) -> Self { + Box::new(self.flat_map(|v| v.neighbours())) + } + + fn in_neighbours(self) -> Self { + Box::new(self.flat_map(|v| v.in_neighbours())) + } + + fn out_neighbours(self) -> Self { + Box::new(self.flat_map(|v| v.out_neighbours())) + } +} + +impl VertexListOps for BoxedIter>> { + type Graph = G; + type Vertex = VertexView; + type IterType = BoxedIter>; + type EList = BoxedIter>>; + type ValueType = BoxedIter; + + fn earliest_time(self) -> BoxedIter>> { + Box::new(self.map(|it| it.earliest_time())) + } + + fn latest_time(self) -> BoxedIter>> { + Box::new(self.map(|it| it.latest_time())) + } + + fn window( + self, + t_start: i64, + t_end: i64, + ) -> BoxedIter>>> { + Box::new(self.map(move |it| it.window(t_start, t_end))) + } + + fn at(self, end: i64) -> Self::IterType<::WindowedViewType> { + Box::new(self.map(move |v| v.at(end))) + } + + fn id(self) -> BoxedIter> { + Box::new(self.map(|it| it.id())) + } + + fn name(self) -> BoxedIter> { + Box::new(self.map(|it| it.name())) + } + + fn properties(self) -> BoxedIter>>> { + Box::new(self.map(move |it| it.properties())) + } + + fn history(self) -> BoxedIter>> { + Box::new(self.map(move |it| it.history())) + } + + fn degree(self) -> BoxedIter> { + Box::new(self.map(|it| it.degree())) + } + + fn in_degree(self) -> BoxedIter> { + Box::new(self.map(|it| it.in_degree())) + } + + fn out_degree(self) -> BoxedIter> { + Box::new(self.map(|it| it.out_degree())) + } + + fn edges(self) -> Self::EList { + Box::new(self.map(|it| it.edges())) + } + + fn in_edges(self) -> Self::EList { + Box::new(self.map(|it| it.in_edges())) + } + + fn out_edges(self) -> Self::EList { + Box::new(self.map(|it| it.out_edges())) + } + + fn neighbours(self) -> Self { + Box::new(self.map(|it| it.neighbours())) + } + + fn in_neighbours(self) -> Self { + Box::new(self.map(|it| it.in_neighbours())) + } + + fn out_neighbours(self) -> Self { + Box::new(self.map(|it| it.out_neighbours())) + } +} + +#[cfg(test)] +mod vertex_test { + use crate::prelude::*; + use std::collections::HashMap; + + #[test] + fn test_earliest_time() { + let g = Graph::new(); + g.add_vertex(0, 1, NO_PROPS).unwrap(); + g.add_vertex(1, 1, NO_PROPS).unwrap(); + g.add_vertex(2, 1, NO_PROPS).unwrap(); + let mut view = g.at(1); + assert_eq!(view.vertex(1).expect("v").earliest_time().unwrap(), 0); + assert_eq!(view.vertex(1).expect("v").latest_time().unwrap(), 1); + + view = g.at(3); + assert_eq!(view.vertex(1).expect("v").earliest_time().unwrap(), 0); + assert_eq!(view.vertex(1).expect("v").latest_time().unwrap(), 2); + } + + #[test] + fn test_properties() { + let g = Graph::new(); + let props = [("test", "test")]; + g.add_vertex(0, 1, NO_PROPS).unwrap(); + g.add_vertex(2, 1, props).unwrap(); + + let v1 = g.vertex(1).unwrap(); + let v1_w = g.window(0, 1).vertex(1).unwrap(); + assert_eq!( + v1.properties().as_map(), + props + .into_iter() + .map(|(k, v)| (k.into(), v.into_prop())) + .collect() + ); + assert_eq!(v1_w.properties().as_map(), HashMap::default()) + } + + #[test] + fn test_property_additions() { + let g = Graph::new(); + let props = [("test", "test")]; + let v1 = g.add_vertex(0, 1, NO_PROPS).unwrap(); + v1.add_updates(2, props).unwrap(); + let v1_w = v1.window(0, 1); + assert_eq!( + v1.properties().as_map(), + props + .into_iter() + .map(|(k, v)| (k.into(), v.into_prop())) + .collect() + ); + assert_eq!(v1_w.properties().as_map(), HashMap::default()) + } + + #[test] + fn test_constant_property_additions() { + let g = Graph::new(); + let v1 = g.add_vertex(0, 1, NO_PROPS).unwrap(); + v1.add_constant_properties([("test", "test")]).unwrap(); + assert_eq!(v1.properties().get("test"), Some("test".into())) + } + + #[test] + fn test_string_deduplication() { + let g = Graph::new(); + let v1 = g + .add_vertex(0, 1, [("test1", "test"), ("test2", "test")]) + .unwrap(); + let s1 = v1.properties().get("test1").unwrap_str(); + let s2 = v1.properties().get("test2").unwrap_str(); + + assert_eq!(s1.as_ptr(), s2.as_ptr()) + } +} diff --git a/raphtory/src/db/vertices.rs b/raphtory/src/db/graph/vertices.rs similarity index 53% rename from raphtory/src/db/vertices.rs rename to raphtory/src/db/graph/vertices.rs index 95eea7201c..b44646565d 100644 --- a/raphtory/src/db/vertices.rs +++ b/raphtory/src/db/graph/vertices.rs @@ -1,15 +1,19 @@ -use crate::core::time::IntoTime; -use crate::core::vertex_ref::VertexRef; -use crate::core::{Direction, Prop}; -use crate::db::edge::EdgeView; -use crate::db::graph_layer::LayeredGraph; -use crate::db::graph_window::WindowedGraph; -use crate::db::path::{Operations, PathFromGraph}; -use crate::db::vertex::VertexView; -use crate::db::view_api::layer::LayerOps; -use crate::db::view_api::BoxedIter; -use crate::db::view_api::*; -use std::collections::HashMap; +use crate::{ + core::{entities::vertices::vertex_ref::VertexRef, utils::time::IntoTime, Direction}, + db::{ + api::{ + properties::Properties, + view::{BoxedIter, Layer, LayerOps}, + }, + graph::{ + edge::EdgeView, + path::{Operations, PathFromGraph}, + vertex::VertexView, + views::{layer_graph::LayeredGraph, window_graph::WindowedGraph}, + }, + }, + prelude::*, +}; #[derive(Clone)] pub struct Vertices { @@ -24,15 +28,17 @@ impl Vertices { pub fn iter(&self) -> Box> + Send> { let g = self.graph.clone(); Box::new( - g.vertex_refs() - .map(move |v| VertexView::new_local(g.clone(), v)), + g.vertex_refs(g.layer_ids(), g.edge_filter()) + .map(move |v| VertexView::new_internal(g.clone(), v)), ) } + /// Returns the number of vertices in the graph. pub fn len(&self) -> usize { - self.graph.num_vertices() + self.graph.count_vertices() } + /// Returns true if the graph contains no vertices. pub fn is_empty(&self) -> bool { self.graph.is_empty() } @@ -48,92 +54,115 @@ impl VertexViewOps for Vertices { type PathType<'a> = PathFromGraph; type EList = BoxedIter>>; + /// Returns an iterator over the vertices' id fn id(&self) -> Self::ValueType { self.iter().id() } + /// Returns an iterator over the vertices' name fn name(&self) -> Self::ValueType { self.iter().name() } + /// Returns an iterator over the vertices' earliest time fn earliest_time(&self) -> Self::ValueType> { self.iter().earliest_time() } + /// Returns an iterator over the vertices' latest time fn latest_time(&self) -> Self::ValueType> { self.iter().latest_time() } - fn property(&self, name: String, include_static: bool) -> Self::ValueType> { - self.iter().property(name, include_static) - } - - fn property_history(&self, name: String) -> Self::ValueType> { - self.iter().property_history(name) - } - + /// Returns an iterator over the vertices' histories fn history(&self) -> Self::ValueType> { self.iter().history() } - fn properties(&self, include_static: bool) -> Self::ValueType> { - self.iter().properties(include_static) - } - - fn property_histories(&self) -> Self::ValueType>> { - self.iter().property_histories() - } - - fn property_names(&self, include_static: bool) -> Self::ValueType> { - self.iter().property_names(include_static) - } - - fn has_property(&self, name: String, include_static: bool) -> Self::ValueType { - self.iter().has_property(name, include_static) - } - - fn has_static_property(&self, name: String) -> Self::ValueType { - self.iter().has_static_property(name) - } - - fn static_property(&self, name: String) -> Self::ValueType> { - self.iter().static_property(name) + /// Returns an iterator over the vertices' properties + fn properties(&self) -> Self::ValueType>> { + self.iter().properties() } + /// Returns the number of edges of the vertices + /// + /// # Returns + /// + /// An iterator of the number of edges of the vertices fn degree(&self) -> Self::ValueType { self.iter().degree() } + /// Returns the number of in edges of the vertices + /// + /// # Returns + /// + /// An iterator of the number of in edges of the vertices fn in_degree(&self) -> Self::ValueType { self.iter().in_degree() } + /// Returns the number of out edges of the vertices + /// + /// # Returns + /// + /// An iterator of the number of out edges of the vertices fn out_degree(&self) -> Self::ValueType { self.iter().out_degree() } + /// Returns the edges of the vertices + /// + /// # Returns + /// + /// An iterator of edges of the vertices fn edges(&self) -> Self::EList { Box::new(self.iter().map(|v| v.edges())) } + /// Returns the in edges of the vertices + /// + /// # Returns + /// + /// An iterator of in edges of the vertices fn in_edges(&self) -> Self::EList { Box::new(self.iter().map(|v| v.in_edges())) } + /// Returns the out edges of the vertices + /// + /// # Returns + /// + /// An iterator of out edges of the vertices fn out_edges(&self) -> Self::EList { Box::new(self.iter().map(|v| v.out_edges())) } + /// Get the neighbours of the vertices + /// + /// # Returns + /// + /// An iterator of the neighbours of the vertices fn neighbours(&self) -> PathFromGraph { let dir = Direction::BOTH; PathFromGraph::new(self.graph.clone(), Operations::Neighbours { dir }) } + /// Get the in neighbours of the vertices + /// + /// # Returns + /// + /// An iterator of the in neighbours of the vertices fn in_neighbours(&self) -> PathFromGraph { let dir = Direction::IN; PathFromGraph::new(self.graph.clone(), Operations::Neighbours { dir }) } + /// Get the out neighbours of the vertices + /// + /// # Returns + /// + /// An iterator of the out neighbours of the vertices fn out_neighbours(&self) -> PathFromGraph { let dir = Direction::OUT; PathFromGraph::new(self.graph.clone(), Operations::Neighbours { dir }) @@ -161,13 +190,27 @@ impl TimeOps for Vertices { impl LayerOps for Vertices { type LayeredViewType = Vertices>; + /// Create a view including all the vertices in the default layer + /// + /// # Returns + /// + /// A view including all the vertices in the default layer fn default_layer(&self) -> Self::LayeredViewType { Vertices { graph: self.graph.default_layer(), } } - fn layer(&self, name: &str) -> Option { + /// Create a view including all the vertices in the given layer + /// + /// # Arguments + /// + /// * `name` - The name of the layer + /// + /// # Returns + /// + /// A view including all the vertices in the given layer + fn layer>(&self, name: L) -> Option { Some(Vertices { graph: self.graph.layer(name)?, }) diff --git a/raphtory/src/db/graph/views/deletion_graph.rs b/raphtory/src/db/graph/views/deletion_graph.rs new file mode 100644 index 0000000000..c6126ecb36 --- /dev/null +++ b/raphtory/src/db/graph/views/deletion_graph.rs @@ -0,0 +1,673 @@ +use crate::{ + core::{ + entities::{ + edges::{edge_ref::EdgeRef, edge_store::EdgeStore}, + vertices::vertex_store::VertexStore, + LayerIds, VID, + }, + storage::timeindex::{AsTime, TimeIndexOps}, + utils::errors::GraphError, + Direction, Prop, + }, + db::{ + api::{ + mutation::internal::InheritMutationOps, + properties::internal::InheritPropertiesOps, + view::{internal::*, BoxedIter}, + }, + graph::graph::{graph_equal, InternalGraph}, + }, + prelude::*, +}; +use serde::{Deserialize, Serialize}; +use std::{ + cmp::min, + fmt::{Display, Formatter}, + iter, + ops::Range, + path::Path, + sync::Arc, +}; + +/// A graph view where an edge remains active from the time it is added until it is explicitly marked as deleted. +/// +/// Note that the graph will give you access to all edges that were added at any point in time, even those that are marked as deleted. +/// The deletion only has an effect on the exploded edge view that are returned. An edge is included in a windowed view of the graph if +/// it is considered active at any point in the window. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct GraphWithDeletions { + graph: Arc, +} + +impl From for GraphWithDeletions { + fn from(value: InternalGraph) -> Self { + Self { + graph: Arc::new(value), + } + } +} + +impl IntoDynamic for GraphWithDeletions { + fn into_dynamic(self) -> DynamicGraph { + DynamicGraph::new(self) + } +} + +impl Display for GraphWithDeletions { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Display::fmt(&self.graph, f) + } +} + +impl GraphWithDeletions { + fn edge_alive_at(&self, e: &EdgeStore, t: i64, layer_ids: &LayerIds) -> bool { + // FIXME: assumes additions are before deletions if at the same timestamp (need to have strict ordering/secondary index) + let ( + first_addition, + first_deletion, + last_addition_before_start, + last_deletion_before_start, + ) = match layer_ids { + LayerIds::None => return false, + LayerIds::All => ( + e.additions().iter().flat_map(|v| v.first_t()).min(), + e.deletions().iter().flat_map(|v| v.first_t()).min(), + e.additions() + .iter() + .flat_map(|v| v.range(i64::MIN..t.saturating_add(1)).last_t()) + .max(), + e.deletions() + .iter() + .flat_map(|v| v.range(i64::MIN..t).last_t()) + .max(), + ), + LayerIds::One(l_id) => ( + e.additions().get(*l_id).and_then(|v| v.first_t()), + e.deletions().get(*l_id).and_then(|v| v.first_t()), + e.additions() + .get(*l_id) + .and_then(|v| v.range(i64::MIN..t.saturating_add(1)).last_t()), + e.deletions() + .get(*l_id) + .and_then(|v| v.range(i64::MIN..t).last_t()), + ), + LayerIds::Multiple(ids) => ( + ids.iter() + .flat_map(|l_id| e.additions().get(*l_id).and_then(|v| v.first_t())) + .min(), + ids.iter() + .flat_map(|l_id| e.deletions().get(*l_id).and_then(|v| v.first_t())) + .min(), + ids.iter() + .flat_map(|l_id| { + e.additions() + .get(*l_id) + .and_then(|v| v.range(i64::MIN..t.saturating_add(1)).last_t()) + }) + .max(), + ids.iter() + .flat_map(|l_id| { + e.deletions() + .get(*l_id) + .and_then(|v| v.range(i64::MIN..t).last_t()) + }) + .max(), + ), + }; + + // None is less than any value (see test below) + (first_deletion < first_addition && first_deletion.filter(|v| *v >= t).is_some()) + || last_addition_before_start > last_deletion_before_start + } + + fn vertex_alive_at( + &self, + v: &VertexStore, + t: i64, + layers: &LayerIds, + edge_filter: Option<&EdgeFilter>, + ) -> bool { + let edges = self.graph.inner().storage.edges.read_lock(); + v.edge_tuples(layers, Direction::BOTH) + .map(|eref| edges.get(eref.pid().into())) + .filter(|e| { + edge_filter.map(|f| f(e, layers)).unwrap_or(true) + && self.edge_alive_at(e, t, layers) + }) + .next() + .is_some() + } + + pub fn new() -> Self { + Self { + graph: Arc::new(InternalGraph::default()), + } + } + + /// Save a graph to a directory + /// + /// # Arguments + /// + /// * `path` - The path to the directory + /// + /// # Returns + /// + /// A raphtory graph + /// + /// # Example + /// + /// ```no_run + /// use std::fs::File; + /// use raphtory::prelude::*; + /// let g = Graph::new(); + /// g.add_vertex(1, 1, NO_PROPS).unwrap(); + /// g.save_to_file("path_str").expect("failed to save file"); + /// ``` + pub fn save_to_file>(&self, path: P) -> Result<(), GraphError> { + MaterializedGraph::from(self.clone()).save_to_file(path) + } + + /// Load a graph from a directory + /// + /// # Arguments + /// + /// * `path` - The path to the directory + /// + /// # Returns + /// + /// A raphtory graph + /// + /// # Example + /// + /// ```no_run + /// use raphtory::prelude::*; + /// let g = Graph::load_from_file("path/to/graph"); + /// ``` + pub fn load_from_file>(path: P) -> Result { + let g = MaterializedGraph::load_from_file(path)?; + g.into_persistent().ok_or(GraphError::GraphLoadError) + } +} + +impl PartialEq for GraphWithDeletions { + fn eq(&self, other: &G) -> bool { + graph_equal(self, other) + } +} + +impl Base for GraphWithDeletions { + type Base = InternalGraph; + #[inline(always)] + fn base(&self) -> &Self::Base { + &self.graph + } +} + +impl InternalMaterialize for GraphWithDeletions { + fn new_base_graph(&self, graph: InternalGraph) -> MaterializedGraph { + MaterializedGraph::PersistentGraph(GraphWithDeletions { + graph: Arc::new(graph), + }) + } + + fn include_deletions(&self) -> bool { + true + } +} + +impl InheritMutationOps for GraphWithDeletions {} + +impl InheritCoreOps for GraphWithDeletions {} + +impl InheritCoreDeletionOps for GraphWithDeletions {} + +impl InheritGraphOps for GraphWithDeletions {} + +impl InheritPropertiesOps for GraphWithDeletions {} + +impl InheritLayerOps for GraphWithDeletions {} + +impl InheritEdgeFilterOps for GraphWithDeletions {} + +impl TimeSemantics for GraphWithDeletions { + fn vertex_earliest_time(&self, v: VID) -> Option { + self.graph.vertex_earliest_time(v) + } + + fn view_start(&self) -> Option { + self.graph.view_start() + } + + fn view_end(&self) -> Option { + self.graph.view_end() + } + + fn earliest_time_global(&self) -> Option { + self.graph.earliest_time_global() + } + + fn latest_time_global(&self) -> Option { + self.graph.latest_time_global() + } + + fn earliest_time_window(&self, t_start: i64, t_end: i64) -> Option { + self.graph.earliest_time_window(t_start, t_end) + } + + fn latest_time_window(&self, t_start: i64, t_end: i64) -> Option { + self.graph.latest_time_window(t_start, t_end) + } + + fn include_vertex_window( + &self, + v: VID, + w: Range, + layer_ids: &LayerIds, + edge_filter: Option<&EdgeFilter>, + ) -> bool { + let v = self.graph.inner().storage.get_node(v); + v.active(w.clone()) || self.vertex_alive_at(&v, w.start, layer_ids, edge_filter) + } + + fn include_edge_window(&self, e: &EdgeStore, w: Range, layer_ids: &LayerIds) -> bool { + // includes edge if it is alive at the start of the window or added during the window + e.active(layer_ids, w.clone()) || self.edge_alive_at(e, w.start, layer_ids) + } + + fn vertex_history(&self, v: VID) -> Vec { + self.graph.vertex_history(v) + } + + fn vertex_history_window(&self, v: VID, w: Range) -> Vec { + self.graph.vertex_history_window(v, w) + } + + fn edge_exploded(&self, e: EdgeRef, layer_ids: LayerIds) -> BoxedIter { + //Fixme: Need support for duration on exploded edges + if self.edge_alive_at(&self.core_edge(e.pid()), i64::MIN, &layer_ids) { + Box::new( + iter::once(e.at(i64::MIN.into())).chain(self.graph.edge_window_exploded( + e, + (i64::MIN + 1)..i64::MAX, + layer_ids, + )), + ) + } else { + self.graph.edge_exploded(e, layer_ids) + } + } + + fn edge_layers(&self, e: EdgeRef, layer_ids: LayerIds) -> BoxedIter { + self.graph.edge_layers(e, layer_ids) + } + + fn edge_window_exploded( + &self, + e: EdgeRef, + w: Range, + layer_ids: LayerIds, + ) -> BoxedIter { + // FIXME: Need better iterators on LockedView that capture the guard + let entry = self.core_edge(e.pid()); + if self.edge_alive_at(&entry, w.start, &layer_ids) { + Box::new( + iter::once(e.at(w.start.into())).chain(self.graph.edge_window_exploded( + e, + w.start.saturating_add(1)..w.end, + layer_ids, + )), + ) + } else { + self.graph.edge_window_exploded(e, w, layer_ids) + } + } + + fn edge_window_layers( + &self, + e: EdgeRef, + w: Range, + layer_ids: LayerIds, + ) -> BoxedIter { + let g = self.clone(); + Box::new( + self.graph + .edge_layers(e, layer_ids.clone()) + .filter(move |&e| { + let entry = g.core_edge(e.pid()); + g.include_edge_window( + &entry, + w.clone(), + &layer_ids.clone().constrain_from_edge(e), + ) + }), + ) + } + + fn edge_earliest_time(&self, e: EdgeRef, layer_ids: LayerIds) -> Option { + e.time().map(|ti| *ti.t()).or_else(|| { + let entry = self.core_edge(e.pid()); + if self.edge_alive_at(&entry, i64::MIN, &layer_ids.clone()) { + Some(i64::MIN) + } else { + self.edge_additions(e, layer_ids).first().map(|ti| *ti.t()) + } + }) + } + + fn edge_earliest_time_window( + &self, + e: EdgeRef, + w: Range, + layer_ids: LayerIds, + ) -> Option { + let entry = self.core_edge(e.pid()); + if self.edge_alive_at(&entry, w.start, &layer_ids) { + Some(w.start) + } else { + self.edge_additions(e, layer_ids).range(w).first_t() + } + } + + fn edge_latest_time(&self, e: EdgeRef, layer_ids: LayerIds) -> Option { + match e.time().map(|ti| *ti.t()) { + Some(t) => Some(min( + self.edge_additions(e, layer_ids.clone()) + .range(t.saturating_add(1)..i64::MAX) + .first_t() + .unwrap_or(i64::MAX), + self.edge_deletions(e, layer_ids) + .range(t.saturating_add(1)..i64::MAX) + .first_t() + .unwrap_or(i64::MAX), + )), + None => { + let entry = self.core_edge(e.pid()); + if self.edge_alive_at(&entry, i64::MAX, &layer_ids) { + Some(i64::MAX) + } else { + self.edge_deletions(e, layer_ids).last_t() + } + } + } + } + + fn edge_latest_time_window( + &self, + e: EdgeRef, + w: Range, + layer_ids: LayerIds, + ) -> Option { + match e.time().map(|ti| *ti.t()) { + Some(t) => Some(min( + self.edge_additions(e, layer_ids.clone()) + .range(t.saturating_add(1)..w.end) + .first_t() + .unwrap_or(w.end - 1), + self.edge_deletions(e, layer_ids) + .range(t.saturating_add(1)..w.end) + .first_t() + .unwrap_or(w.end - 1), + )), + None => { + let entry = self.core_edge(e.pid()); + if self.edge_alive_at(&entry, w.end - 1, &layer_ids) { + Some(w.end - 1) + } else { + self.edge_deletions(e, layer_ids).range(w).last_t() + } + } + } + } + + fn edge_deletion_history(&self, e: EdgeRef, layer_ids: LayerIds) -> Vec { + self.edge_deletions(e, layer_ids) + .iter_t() + .copied() + .collect() + } + + fn edge_deletion_history_window( + &self, + e: EdgeRef, + w: Range, + layer_ids: LayerIds, + ) -> Vec { + self.edge_deletions(e, layer_ids) + .range(w) + .iter_t() + .copied() + .collect() + } + + #[inline] + fn has_temporal_prop(&self, prop_id: usize) -> bool { + self.graph.has_temporal_prop(prop_id) + } + + fn temporal_prop_vec(&self, prop_id: usize) -> Vec<(i64, Prop)> { + self.graph.temporal_prop_vec(prop_id) + } + + #[inline] + fn has_temporal_prop_window(&self, prop_id: usize, w: Range) -> bool { + self.graph.has_temporal_prop_window(prop_id, w) + } + + fn temporal_prop_vec_window( + &self, + prop_id: usize, + t_start: i64, + t_end: i64, + ) -> Vec<(i64, Prop)> { + self.graph.temporal_prop_vec_window(prop_id, t_start, t_end) + } + + #[inline] + fn has_temporal_vertex_prop(&self, v: VID, prop_id: usize) -> bool { + self.graph.has_temporal_vertex_prop(v, prop_id) + } + + fn temporal_vertex_prop_vec(&self, v: VID, prop_id: usize) -> Vec<(i64, Prop)> { + self.graph.temporal_vertex_prop_vec(v, prop_id) + } + + fn has_temporal_vertex_prop_window(&self, v: VID, prop_id: usize, w: Range) -> bool { + self.graph.has_temporal_vertex_prop_window(v, prop_id, w) + } + + fn temporal_vertex_prop_vec_window( + &self, + v: VID, + prop_id: usize, + t_start: i64, + t_end: i64, + ) -> Vec<(i64, Prop)> { + self.graph + .temporal_vertex_prop_vec_window(v, prop_id, t_start, t_end) + } + + fn has_temporal_edge_prop_window( + &self, + e: EdgeRef, + prop_id: usize, + w: Range, + layer_ids: LayerIds, + ) -> bool { + let entry = self.core_edge(e.pid()); + + if entry.has_temporal_prop(&layer_ids, prop_id) { + let search_start = entry + .last_deletion_before(&layer_ids, w.start) + .unwrap_or(i64::MIN); // if property was added at any point since the last deletion, it is still there + match layer_ids { + LayerIds::None => false, + LayerIds::All => entry.layer_ids_iter().any(|id| { + entry + .temporal_prop_layer(id, prop_id) + .filter(|prop| prop.iter_window(search_start..w.end).next().is_some()) + .is_some() + }), + LayerIds::One(id) => entry + .temporal_prop_layer(id, prop_id) + .filter(|prop| prop.iter_window(search_start..w.end).next().is_some()) + .is_some(), + LayerIds::Multiple(ids) => ids.iter().any(|&id| { + entry + .temporal_prop_layer(id, prop_id) + .filter(|prop| prop.iter_window(search_start..w.end).next().is_some()) + .is_some() + }), + } + } else { + false + } + } + + fn temporal_edge_prop_vec_window( + &self, + e: EdgeRef, + prop_id: usize, + t_start: i64, + t_end: i64, + layer_ids: LayerIds, + ) -> Vec<(i64, Prop)> { + let prop = self.temporal_edge_prop(e, prop_id, layer_ids.clone()); + match prop { + Some(p) => { + let entry = self.core_edge(e.pid()); + if self.edge_alive_at(&entry, t_start, &layer_ids) { + p.last_before(t_start.saturating_add(1)) + .into_iter() + .map(|(_, v)| (t_start, v)) + .chain(p.iter_window(t_start.saturating_add(1)..t_end)) + .collect() + } else { + p.iter_window(t_start..t_end).collect() + } + } + None => Default::default(), + } + } + + fn has_temporal_edge_prop(&self, e: EdgeRef, prop_id: usize, layer_ids: LayerIds) -> bool { + self.graph.has_temporal_edge_prop(e, prop_id, layer_ids) + } + + fn temporal_edge_prop_vec( + &self, + e: EdgeRef, + prop_id: usize, + layer_ids: LayerIds, + ) -> Vec<(i64, Prop)> { + self.graph.temporal_edge_prop_vec(e, prop_id, layer_ids) + } +} + +#[cfg(test)] +mod test_deletions { + use crate::{db::graph::views::deletion_graph::GraphWithDeletions, prelude::*}; + use itertools::Itertools; + + #[test] + fn test_edge_deletions() { + let g = GraphWithDeletions::new(); + + g.add_edge(0, 0, 1, [("added", Prop::I64(0))], None) + .unwrap(); + g.delete_edge(10, 0, 1, None).unwrap(); + + assert_eq!(g.edges().id().collect::>(), vec![(0, 1)]); + + assert_eq!( + g.window(1, 2).edges().id().collect::>(), + vec![(0, 1)] + ); + + assert_eq!(g.window(1, 2).count_edges(), 1); + + assert!(g.window(11, 12).is_empty()); + + assert_eq!( + g.window(1, 2) + .edge(0, 1) + .unwrap() + .properties() + .get("added") + .unwrap_i64(), + 0 + ); + + assert!(g.window(11, 12).edge(0, 1).is_none()); + + assert_eq!( + g.window(1, 2) + .edge(0, 1) + .unwrap() + .properties() + .temporal() + .get("added") + .unwrap() + .iter() + .collect_vec(), + vec![(1, Prop::I64(0))] + ); + + assert_eq!(g.window(1, 2).vertex(0).unwrap().out_degree(), 1) + } + + #[test] + fn test_materialize_only_deletion() { + let g = GraphWithDeletions::new(); + g.delete_edge(1, 1, 2, None).unwrap(); + + assert_eq!(g.materialize().unwrap().into_persistent().unwrap(), g); + } + + #[test] + fn test_materialize_window() { + let g = GraphWithDeletions::new(); + g.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); + g.delete_edge(10, 1, 2, None).unwrap(); + + let gm = g + .window(3, 5) + .materialize() + .unwrap() + .into_persistent() + .unwrap(); + assert_eq!(gm, g.window(3, 5)) + } + + #[test] + fn test_exploded_latest_time() { + let g = GraphWithDeletions::new(); + let e = g.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); + g.delete_edge(10, 1, 2, None).unwrap(); + assert_eq!(e.latest_time(), Some(10)); + assert_eq!(e.explode().latest_time().collect_vec(), vec![Some(10)]); + } + + #[test] + fn test_edge_properties() { + let g = GraphWithDeletions::new(); + let e = g.add_edge(0, 1, 2, [("test", "test")], None).unwrap(); + assert_eq!(e.properties().get("test").unwrap_str(), "test"); + e.delete(10, None).unwrap(); + assert_eq!(e.properties().get("test").unwrap_str(), "test"); + e.add_updates(11, [("test", "test11")], None).unwrap(); + assert_eq!( + e.window(10, 12).properties().get("test").unwrap_str(), + "test11" + ); + assert_eq!( + e.window(5, 12) + .properties() + .temporal() + .get("test") + .unwrap() + .iter() + .collect_vec(), + vec![(5, Prop::str("test")), (11i64, Prop::str("test11"))], + ); + } +} diff --git a/raphtory/src/db/graph/views/layer_graph.rs b/raphtory/src/db/graph/views/layer_graph.rs new file mode 100644 index 0000000000..8b01b0c94c --- /dev/null +++ b/raphtory/src/db/graph/views/layer_graph.rs @@ -0,0 +1,182 @@ +use crate::{ + core::entities::{edges::edge_store::EdgeStore, LayerIds}, + db::api::{ + properties::internal::InheritPropertiesOps, + view::{ + internal::{ + Base, EdgeFilter, EdgeFilterOps, Immutable, InheritCoreOps, InheritGraphOps, + InheritMaterialize, InheritTimeSemantics, InternalLayerOps, + }, + Layer, + }, + }, + prelude::GraphViewOps, +}; +use itertools::Itertools; +use std::{ + fmt::{Debug, Formatter}, + sync::Arc, +}; + +#[derive(Clone)] +pub struct LayeredGraph { + /// The underlying `Graph` object. + pub graph: G, + /// The layer this graphs points to. + pub layers: LayerIds, + + edge_filter: EdgeFilter, +} + +impl Immutable for LayeredGraph {} + +impl Debug for LayeredGraph { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("LayeredGraph") + .field("graph", &self.graph) + .field("layers", &self.layers) + .finish() + } +} + +impl Base for LayeredGraph { + type Base = G; + #[inline(always)] + fn base(&self) -> &Self::Base { + &self.graph + } +} + +impl InheritTimeSemantics for LayeredGraph {} + +impl InheritCoreOps for LayeredGraph {} + +impl InheritMaterialize for LayeredGraph {} + +impl InheritPropertiesOps for LayeredGraph {} + +impl InheritGraphOps for LayeredGraph {} + +impl EdgeFilterOps for LayeredGraph { + #[inline] + fn edge_filter(&self) -> Option<&EdgeFilter> { + Some(&self.edge_filter) + } +} + +impl LayeredGraph { + pub fn new(graph: G, layers: LayerIds) -> Self { + let edge_filter: EdgeFilter = match graph.edge_filter().cloned() { + None => Arc::new(|e, l| e.has_layer(l)), + Some(f) => Arc::new(move |e, l| e.has_layer(l) && f(e, l)), + }; + Self { + graph, + layers, + edge_filter, + } + } + + /// Get the intersection between the previously requested layers and the layers of + /// this view + fn constrain(&self, layers: LayerIds) -> LayerIds { + match layers { + LayerIds::None => LayerIds::None, + LayerIds::All => self.layers.clone(), + _ => match &self.layers { + LayerIds::All => layers, + LayerIds::One(id) => match layers.find(*id) { + Some(layer) => LayerIds::One(layer), + None => LayerIds::None, + }, + LayerIds::Multiple(ids) => { + // intersect the layers + let new_layers = ids.iter().filter_map(|id| layers.find(*id)).collect_vec(); + match new_layers.len() { + 0 => LayerIds::None, + 1 => LayerIds::One(new_layers[0]), + _ => LayerIds::Multiple(new_layers.into()), + } + } + LayerIds::None => LayerIds::None, + }, + } + } +} + +impl InternalLayerOps for LayeredGraph { + fn layer_ids(&self) -> LayerIds { + self.layers.clone() + } + + fn layer_ids_from_names(&self, key: Layer) -> LayerIds { + self.constrain(self.graph.layer_ids_from_names(key)) + } + + fn edge_layer_ids(&self, e: &EdgeStore) -> LayerIds { + let layer_ids = self.graph.edge_layer_ids(e); + self.constrain(layer_ids) + } +} + +#[cfg(test)] +mod test_layers { + use crate::prelude::*; + use itertools::Itertools; + #[test] + fn test_layer_vertex() { + let g = Graph::new(); + + g.add_edge(0, 1, 2, NO_PROPS, Some("layer1")).unwrap(); + g.add_edge(0, 2, 3, NO_PROPS, Some("layer2")).unwrap(); + g.add_edge(3, 2, 4, NO_PROPS, Some("layer1")).unwrap(); + let neighbours = g + .layer(vec!["layer1", "layer2"]) + .unwrap() + .vertex(1) + .unwrap() + .neighbours() + .into_iter() + .collect_vec(); + assert_eq!( + neighbours[0] + .layer("layer2") + .unwrap() + .edges() + .id() + .collect_vec(), + vec![(2, 3)] + ); + assert_eq!( + g.layer("layer2") + .unwrap() + .vertex(neighbours[0].name()) + .unwrap() + .edges() + .id() + .collect_vec(), + vec![(2, 3)] + ); + let mut edges = g + .layer("layer1") + .unwrap() + .vertex(neighbours[0].name()) + .unwrap() + .edges() + .id() + .collect_vec(); + edges.sort(); + assert_eq!(edges, vec![(1, 2), (2, 4)]); + let mut edges = g.layer("layer1").unwrap().edges().id().collect_vec(); + edges.sort(); + assert_eq!(edges, vec![(1, 2), (2, 4)]); + let mut edges = g + .layer(vec!["layer1", "layer2"]) + .unwrap() + .edges() + .id() + .collect_vec(); + edges.sort(); + assert_eq!(edges, vec![(1, 2), (2, 3), (2, 4)]); + } +} diff --git a/raphtory/src/db/graph/views/mod.rs b/raphtory/src/db/graph/views/mod.rs new file mode 100644 index 0000000000..099efc6951 --- /dev/null +++ b/raphtory/src/db/graph/views/mod.rs @@ -0,0 +1,4 @@ +pub mod deletion_graph; +pub mod layer_graph; +pub mod vertex_subgraph; +pub mod window_graph; diff --git a/raphtory/src/db/graph/views/vertex_subgraph.rs b/raphtory/src/db/graph/views/vertex_subgraph.rs new file mode 100644 index 0000000000..3569646148 --- /dev/null +++ b/raphtory/src/db/graph/views/vertex_subgraph.rs @@ -0,0 +1,252 @@ +use crate::{ + core::{ + entities::{edges::edge_ref::EdgeRef, vertices::vertex_ref::VertexRef, LayerIds, EID, VID}, + Direction, + }, + db::api::{ + properties::internal::InheritPropertiesOps, + view::internal::{ + Base, EdgeFilter, EdgeFilterOps, GraphOps, Immutable, InheritCoreOps, InheritLayerOps, + InheritMaterialize, InheritTimeSemantics, + }, + }, + prelude::GraphViewOps, +}; +use itertools::Itertools; +use rayon::prelude::*; +use rustc_hash::FxHashSet; +use std::{ + fmt::{Debug, Formatter}, + sync::Arc, +}; + +#[derive(Clone)] +pub struct VertexSubgraph { + graph: G, + vertices: Arc>, + edge_filter: EdgeFilter, +} + +impl Debug for VertexSubgraph { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("VertexSubgraph") + .field("graph", &self.graph) + .field("vertices", &self.vertices) + .finish() + } +} + +impl Base for VertexSubgraph { + type Base = G; + #[inline(always)] + fn base(&self) -> &Self::Base { + &self.graph + } +} + +impl Immutable for VertexSubgraph {} + +impl InheritCoreOps for VertexSubgraph {} +impl InheritTimeSemantics for VertexSubgraph {} +impl InheritPropertiesOps for VertexSubgraph {} +impl InheritMaterialize for VertexSubgraph {} +impl InheritLayerOps for VertexSubgraph {} + +impl VertexSubgraph { + pub fn new(graph: G, vertices: FxHashSet) -> Self { + let vertices = Arc::new(vertices); + let vertices_cloned = vertices.clone(); + let edge_filter: EdgeFilter = match graph.edge_filter().cloned() { + Some(f) => Arc::new(move |e, l| { + vertices_cloned.contains(&e.src()) && vertices_cloned.contains(&e.dst()) && f(e, l) + }), + None => Arc::new(move |e, _l| { + vertices_cloned.contains(&e.src()) && vertices_cloned.contains(&e.dst()) + }), + }; + Self { + graph, + vertices, + edge_filter, + } + } +} + +impl EdgeFilterOps for VertexSubgraph { + #[inline] + fn edge_filter(&self) -> Option<&EdgeFilter> { + Some(&self.edge_filter) + } +} + +impl GraphOps for VertexSubgraph { + fn internal_vertex_ref( + &self, + v: VertexRef, + layer_ids: &LayerIds, + filter: Option<&EdgeFilter>, + ) -> Option { + self.graph + .internal_vertex_ref(v, layer_ids, filter) + .filter(|v| self.vertices.contains(v)) + } + + fn find_edge_id( + &self, + e_id: EID, + layer_ids: &LayerIds, + filter: Option<&EdgeFilter>, + ) -> Option { + self.graph + .find_edge_id(e_id, layer_ids, filter) + .filter(|e| self.vertices.contains(&e.src()) && self.vertices.contains(&e.dst())) + } + + fn vertices_len(&self, _layer_ids: LayerIds, _filter: Option<&EdgeFilter>) -> usize { + self.vertices.len() + } + + fn edges_len(&self, layer: LayerIds, filter: Option<&EdgeFilter>) -> usize { + self.vertices + .par_iter() + .map(|v| self.degree(*v, Direction::OUT, &layer, filter)) + .sum() + } + + fn has_edge_ref( + &self, + src: VID, + dst: VID, + layer: &LayerIds, + filter: Option<&EdgeFilter>, + ) -> bool { + self.graph.has_edge_ref(src, dst, layer, filter) + } + + fn has_vertex_ref( + &self, + v: VertexRef, + layer_ids: &LayerIds, + edge_filter: Option<&EdgeFilter>, + ) -> bool { + self.internal_vertex_ref(v, layer_ids, edge_filter) + .is_some() + } + + fn degree(&self, v: VID, d: Direction, layer: &LayerIds, filter: Option<&EdgeFilter>) -> usize { + self.graph.degree(v, d, layer, filter) + } + + fn vertex_ref(&self, v: u64, layers: &LayerIds, filter: Option<&EdgeFilter>) -> Option { + self.internal_vertex_ref(v.into(), layers, filter) + } + + fn vertex_refs( + &self, + _layers: LayerIds, + _filter: Option<&EdgeFilter>, + ) -> Box + Send> { + // this sucks but seems to be the only way currently (see also http://smallcultfollowing.com/babysteps/blog/2018/09/02/rust-pattern-iterating-an-over-a-rc-vec-t/) + let verts = Vec::from_iter(self.vertices.iter().copied()); + Box::new(verts.into_iter()) + } + + fn edge_ref( + &self, + src: VID, + dst: VID, + layer: &LayerIds, + filter: Option<&EdgeFilter>, + ) -> Option { + self.graph.edge_ref(src, dst, layer, filter) + } + + fn edge_refs( + &self, + layer: LayerIds, + filter: Option<&EdgeFilter>, + ) -> Box + Send> { + let g1 = self.clone(); + let vertices = self.vertices.clone().iter().copied().collect_vec(); + let filter = filter.cloned(); + Box::new( + vertices.into_iter().flat_map(move |v| { + g1.vertex_edges(v, Direction::OUT, layer.clone(), filter.as_ref()) + }), + ) + } + + fn vertex_edges( + &self, + v: VID, + d: Direction, + layer: LayerIds, + filter: Option<&EdgeFilter>, + ) -> Box + Send> { + self.graph.vertex_edges(v, d, layer, filter) + } + + fn neighbours( + &self, + v: VID, + d: Direction, + layers: LayerIds, + filter: Option<&EdgeFilter>, + ) -> Box + Send> { + self.graph.neighbours(v, d, layers, filter) + } +} + +#[cfg(test)] +mod subgraph_tests { + use crate::{algorithms::triangle_count::triangle_count, prelude::*}; + + #[test] + fn test_materialize_no_edges() { + let g = Graph::new(); + + g.add_vertex(1, 1, NO_PROPS).unwrap(); + g.add_vertex(2, 2, NO_PROPS).unwrap(); + let sg = g.subgraph([1, 2]); + + let actual = sg.materialize().unwrap().into_events().unwrap(); + assert_eq!(actual, sg); + } + + #[test] + fn test_remove_degree1_triangle_count() { + let graph = Graph::new(); + let edges = vec![ + (1, 2, 1), + (1, 3, 2), + (1, 4, 3), + (3, 1, 4), + (3, 4, 5), + (3, 5, 6), + (4, 5, 7), + (5, 6, 8), + (5, 8, 9), + (7, 5, 10), + (8, 5, 11), + (1, 9, 12), + (9, 1, 13), + (6, 3, 14), + (4, 8, 15), + (8, 3, 16), + (5, 10, 17), + (10, 5, 18), + (10, 8, 19), + (1, 11, 20), + (11, 1, 21), + (9, 11, 22), + (11, 9, 23), + ]; + for (src, dst, ts) in edges { + graph.add_edge(ts, src, dst, NO_PROPS, None).unwrap(); + } + let subgraph = graph.subgraph(graph.vertices().into_iter().filter(|v| v.degree() > 1)); + let ts = triangle_count(&subgraph, None); + let tg = triangle_count(&graph, None); + assert_eq!(ts, tg) + } +} diff --git a/raphtory/src/db/graph/views/window_graph.rs b/raphtory/src/db/graph/views/window_graph.rs new file mode 100644 index 0000000000..d5da752c58 --- /dev/null +++ b/raphtory/src/db/graph/views/window_graph.rs @@ -0,0 +1,1127 @@ +//! A windowed view is a subset of a graph between a specific time window. +//! For example, lets say you wanted to run an algorithm each month over a graph, graph window +//! would allow you to split the graph into 30 day chunks to do so. +//! +//! This module also defines the `GraphWindow` trait, which represents a window of time over +//! which a graph can be queried. +//! +//! GraphWindowSet implements the `Iterator` trait, producing `WindowedGraph` views +//! for each perspective within it. +//! +//! # Types +//! +//! * `GraphWindowSet` - A struct that allows iterating over a Graph broken down into multiple +//! windowed views. It contains a `Graph` and an iterator of `Perspective`. +//! +//! * `WindowedGraph` - A struct that represents a windowed view of a `Graph`. +//! It contains a `Graph`, a start time (`t_start`) and an end time (`t_end`). +//! +//! # Traits +//! +//! * `GraphViewInternalOps` - A trait that provides operations to a `WindowedGraph` +//! used internally by the `GraphWindowSet`. +//! +//! # Examples +//! +//! ```rust +//! +//! use raphtory::prelude::*; +//! use raphtory::db::api::view::*; +//! +//! let graph = Graph::new(); +//! graph.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); +//! graph.add_edge(1, 1, 3, NO_PROPS, None).unwrap(); +//! graph.add_edge(2, 2, 3, NO_PROPS, None).unwrap(); +//! +//! let wg = graph.window(0, 1); +//! assert_eq!(wg.edge(1, 2).unwrap().src().id(), 1); +//! ``` + +use crate::{ + core::{ + entities::{ + edges::{edge_ref::EdgeRef, edge_store::EdgeStore}, + vertices::vertex_ref::VertexRef, + LayerIds, EID, VID, + }, + utils::time::IntoTime, + ArcStr, Direction, Prop, + }, + db::api::{ + properties::internal::{ + InheritStaticPropertiesOps, TemporalPropertiesOps, TemporalPropertyViewOps, + }, + view::{ + internal::{ + Base, DynamicGraph, EdgeFilter, EdgeFilterOps, GraphOps, Immutable, InheritCoreOps, + InheritLayerOps, InheritMaterialize, IntoDynamic, TimeSemantics, + }, + BoxedIter, + }, + }, + prelude::{GraphViewOps, TimeOps}, + search::IndexedGraph, +}; +use std::{ + cmp::{max, min}, + fmt::{Debug, Formatter}, + ops::Range, + sync::Arc, +}; + +/// A struct that represents a windowed view of a `Graph`. +#[derive(Clone)] +pub struct WindowedGraph { + /// The underlying `Graph` object. + pub graph: G, + /// The inclusive start time of the window. + pub t_start: i64, + /// The exclusive end time of the window. + pub t_end: i64, + filter: EdgeFilter, +} + +impl Debug for WindowedGraph { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "WindowedGraph({:?}, {}..{})", + self.graph, self.t_start, self.t_end + ) + } +} + +impl WindowedGraph> { + pub fn into_dynamic_indexed(self) -> IndexedGraph { + IndexedGraph { + graph: self + .graph + .graph + .window(self.t_start, self.t_end) + .into_dynamic(), + vertex_index: self.graph.vertex_index, + edge_index: self.graph.edge_index, + reader: self.graph.reader, + edge_reader: self.graph.edge_reader, + } + } +} + +impl Base for WindowedGraph { + type Base = G; + #[inline(always)] + fn base(&self) -> &Self::Base { + &self.graph + } +} + +impl Immutable for WindowedGraph {} +impl InheritCoreOps for WindowedGraph {} + +impl InheritMaterialize for WindowedGraph {} + +impl InheritStaticPropertiesOps for WindowedGraph {} + +impl InheritLayerOps for WindowedGraph {} + +impl TemporalPropertyViewOps for WindowedGraph { + fn temporal_history(&self, id: usize) -> Vec { + self.temporal_prop_vec(id) + .into_iter() + .map(|(t, _)| t) + .collect() + } + + fn temporal_values(&self, id: usize) -> Vec { + self.temporal_prop_vec(id) + .into_iter() + .map(|(_, v)| v) + .collect() + } +} + +impl TemporalPropertiesOps for WindowedGraph { + fn get_temporal_prop_id(&self, name: &str) -> Option { + self.graph + .get_temporal_prop_id(name) + .filter(|id| self.has_temporal_prop(*id)) + } + + fn get_temporal_prop_name(&self, id: usize) -> ArcStr { + self.graph.get_temporal_prop_name(id) + } + + fn temporal_prop_ids(&self) -> Box + '_> { + Box::new( + self.graph + .temporal_prop_ids() + .filter(|id| self.has_temporal_prop(*id)), + ) + } +} + +impl TimeSemantics for WindowedGraph { + fn vertex_earliest_time(&self, v: VID) -> Option { + self.graph + .vertex_earliest_time_window(v, self.t_start, self.t_end) + } + + fn vertex_latest_time(&self, v: VID) -> Option { + self.graph + .vertex_latest_time_window(v, self.t_start, self.t_end) + } + + fn view_start(&self) -> Option { + Some(self.t_start) + } + + fn view_end(&self) -> Option { + Some(self.t_end) + } + + #[inline] + fn earliest_time_global(&self) -> Option { + self.graph.earliest_time_window(self.t_start, self.t_end) + } + + #[inline] + fn latest_time_global(&self) -> Option { + self.graph.latest_time_window(self.t_start, self.t_end) + } + + #[inline] + fn earliest_time_window(&self, t_start: i64, t_end: i64) -> Option { + self.graph + .earliest_time_window(self.actual_start(t_start), self.actual_end(t_end)) + } + + #[inline] + fn latest_time_window(&self, t_start: i64, t_end: i64) -> Option { + self.graph + .latest_time_window(self.actual_start(t_start), self.actual_end(t_end)) + } + + #[inline] + fn vertex_earliest_time_window(&self, v: VID, t_start: i64, t_end: i64) -> Option { + self.graph.vertex_earliest_time_window( + v, + self.actual_start(t_start), + self.actual_end(t_end), + ) + } + + #[inline] + fn vertex_latest_time_window(&self, v: VID, t_start: i64, t_end: i64) -> Option { + self.graph + .vertex_latest_time_window(v, self.actual_start(t_start), self.actual_end(t_end)) + } + + #[inline] + fn include_vertex_window( + &self, + v: VID, + w: Range, + layer_ids: &LayerIds, + edge_filter: Option<&EdgeFilter>, + ) -> bool { + self.graph.include_vertex_window( + v, + self.actual_start(w.start)..self.actual_end(w.end), + layer_ids, + edge_filter, + ) + } + + #[inline] + fn include_edge_window(&self, e: &EdgeStore, w: Range, layer_ids: &LayerIds) -> bool { + self.graph.include_edge_window( + e, + self.actual_start(w.start)..self.actual_end(w.end), + layer_ids, + ) + } + + fn vertex_history(&self, v: VID) -> Vec { + self.graph + .vertex_history_window(v, self.t_start..self.t_end) + } + + fn vertex_history_window(&self, v: VID, w: Range) -> Vec { + self.graph + .vertex_history_window(v, self.actual_start(w.start)..self.actual_end(w.end)) + } + + fn edge_exploded(&self, e: EdgeRef, layer_ids: LayerIds) -> BoxedIter { + self.graph + .edge_window_exploded(e, self.t_start..self.t_end, layer_ids) + } + + fn edge_layers(&self, e: EdgeRef, layer_ids: LayerIds) -> BoxedIter { + self.graph + .edge_window_layers(e, self.t_start..self.t_end, layer_ids) + } + + fn edge_window_exploded( + &self, + e: EdgeRef, + w: Range, + layer_ids: LayerIds, + ) -> BoxedIter { + self.graph.edge_window_exploded( + e, + self.actual_start(w.start)..self.actual_end(w.end), + layer_ids, + ) + } + + fn edge_window_layers( + &self, + e: EdgeRef, + w: Range, + layer_ids: LayerIds, + ) -> BoxedIter { + self.graph.edge_window_layers( + e, + self.actual_start(w.start)..self.actual_end(w.end), + layer_ids, + ) + } + + fn edge_earliest_time(&self, e: EdgeRef, layer_ids: LayerIds) -> Option { + self.graph + .edge_earliest_time_window(e, self.t_start..self.t_end, layer_ids) + } + + fn edge_earliest_time_window( + &self, + e: EdgeRef, + w: Range, + layer_ids: LayerIds, + ) -> Option { + self.graph.edge_earliest_time_window( + e, + self.actual_start(w.start)..self.actual_end(w.end), + layer_ids, + ) + } + + fn edge_latest_time(&self, e: EdgeRef, layer_ids: LayerIds) -> Option { + self.graph + .edge_latest_time_window(e, self.t_start..self.t_end, layer_ids) + } + + fn edge_latest_time_window( + &self, + e: EdgeRef, + w: Range, + layer_ids: LayerIds, + ) -> Option { + self.graph.edge_latest_time_window( + e, + self.actual_start(w.start)..self.actual_end(w.end), + layer_ids, + ) + } + + fn edge_deletion_history(&self, e: EdgeRef, layer_ids: LayerIds) -> Vec { + self.graph + .edge_deletion_history_window(e, self.t_start..self.t_end, layer_ids) + } + + fn edge_deletion_history_window( + &self, + e: EdgeRef, + w: Range, + layer_ids: LayerIds, + ) -> Vec { + self.graph.edge_deletion_history_window( + e, + self.actual_start(w.start)..self.actual_end(w.end), + layer_ids, + ) + } + + fn has_temporal_prop(&self, prop_id: usize) -> bool { + self.graph + .has_temporal_prop_window(prop_id, self.t_start..self.t_end) + } + + fn temporal_prop_vec(&self, prop_id: usize) -> Vec<(i64, Prop)> { + self.graph + .temporal_prop_vec_window(prop_id, self.t_start, self.t_end) + } + + fn has_temporal_prop_window(&self, prop_id: usize, w: Range) -> bool { + self.graph + .has_temporal_prop_window(prop_id, self.actual_start(w.start)..self.actual_end(w.end)) + } + + fn temporal_prop_vec_window( + &self, + prop_id: usize, + t_start: i64, + t_end: i64, + ) -> Vec<(i64, Prop)> { + self.graph.temporal_prop_vec_window( + prop_id, + self.actual_start(t_start), + self.actual_end(t_end), + ) + } + + fn has_temporal_vertex_prop(&self, v: VID, prop_id: usize) -> bool { + self.graph + .has_temporal_vertex_prop_window(v, prop_id, self.t_start..self.t_end) + } + + fn temporal_vertex_prop_vec(&self, v: VID, prop_id: usize) -> Vec<(i64, Prop)> { + self.graph + .temporal_vertex_prop_vec_window(v, prop_id, self.t_start, self.t_end) + } + + fn has_temporal_vertex_prop_window(&self, v: VID, prop_id: usize, w: Range) -> bool { + self.graph.has_temporal_vertex_prop_window( + v, + prop_id, + self.actual_start(w.start)..self.actual_end(w.end), + ) + } + + fn temporal_vertex_prop_vec_window( + &self, + v: VID, + prop_id: usize, + t_start: i64, + t_end: i64, + ) -> Vec<(i64, Prop)> { + self.graph.temporal_vertex_prop_vec_window( + v, + prop_id, + self.actual_start(t_start), + self.actual_end(t_end), + ) + } + + fn has_temporal_edge_prop_window( + &self, + e: EdgeRef, + prop_id: usize, + w: Range, + layer_ids: LayerIds, + ) -> bool { + self.graph.has_temporal_edge_prop_window( + e, + prop_id, + self.actual_start(w.start)..self.actual_end(w.end), + layer_ids, + ) + } + + fn temporal_edge_prop_vec_window( + &self, + e: EdgeRef, + prop_id: usize, + t_start: i64, + t_end: i64, + layer_ids: LayerIds, + ) -> Vec<(i64, Prop)> { + self.graph.temporal_edge_prop_vec_window( + e, + prop_id, + self.actual_start(t_start), + self.actual_end(t_end), + layer_ids, + ) + } + + fn has_temporal_edge_prop(&self, e: EdgeRef, prop_id: usize, layer_ids: LayerIds) -> bool { + self.graph + .has_temporal_edge_prop_window(e, prop_id, self.t_start..self.t_end, layer_ids) + } + + fn temporal_edge_prop_vec( + &self, + e: EdgeRef, + prop_id: usize, + layer_ids: LayerIds, + ) -> Vec<(i64, Prop)> { + self.graph + .temporal_edge_prop_vec_window(e, prop_id, self.t_start, self.t_end, layer_ids) + } +} + +impl EdgeFilterOps for WindowedGraph { + #[inline] + fn edge_filter(&self) -> Option<&EdgeFilter> { + Some(&self.filter) + } +} + +/// Implementation of the GraphViewInternalOps trait for WindowedGraph. +/// This trait provides operations to a `WindowedGraph` used internally by the `GraphWindowSet`. +/// *Note: All functions in this are bound by the time set in the windowed graph. +impl GraphOps for WindowedGraph { + #[inline] + fn internal_vertex_ref( + &self, + v: VertexRef, + layers: &LayerIds, + filter: Option<&EdgeFilter>, + ) -> Option { + self.graph + .internal_vertex_ref(v, layers, filter) + .filter(|v| self.include_vertex_window(*v, self.t_start..self.t_end, layers, filter)) + } + + #[inline] + fn find_edge_id( + &self, + e_id: EID, + layer_ids: &LayerIds, + filter: Option<&EdgeFilter>, + ) -> Option { + self.graph.find_edge_id(e_id, layer_ids, filter) + } + + /// Returns the number of vertices in the windowed view. + #[inline] + fn vertices_len(&self, layer_ids: LayerIds, filter: Option<&EdgeFilter>) -> usize { + self.vertex_refs(layer_ids, filter).count() + } + + /// Returns the number of edges in the windowed view. + #[inline] + fn edges_len(&self, layer: LayerIds, filter: Option<&EdgeFilter>) -> usize { + // filter takes care of checking the window + self.graph.edges_len(layer, filter) + } + + /// Check if there is an edge from src to dst in the window. + /// + /// # Arguments + /// + /// - `src` - The source vertex. + /// - `dst` - The destination vertex. + /// + /// # Returns + /// + /// A result containing `true` if there is an edge from src to dst in the window, `false` otherwise. + /// + /// # Errors + /// + /// Returns an error if either `src` or `dst` is not a valid vertex. + #[inline] + fn has_edge_ref( + &self, + src: VID, + dst: VID, + layer: &LayerIds, + filter: Option<&EdgeFilter>, + ) -> bool { + // filter takes care of checking the window + self.graph.has_edge_ref(src, dst, layer, filter) + } + + /// Check if a vertex v exists in the window. + /// + /// # Arguments + /// + /// - `v` - The vertex to check. + /// + /// # Returns + /// + /// A result containing `true` if the vertex exists in the window, `false` otherwise. + /// + /// # Errors + /// + /// Returns an error if `v` is not a valid vertex. + #[inline] + fn has_vertex_ref(&self, v: VertexRef, layers: &LayerIds, filter: Option<&EdgeFilter>) -> bool { + self.internal_vertex_ref(v, layers, filter).is_some() + } + + /// Returns the number of edges from a vertex in the window. + /// + /// # Arguments + /// + /// - `v` - The vertex to check. + /// - `d` - The direction of the edges to count. + /// + /// # Returns + /// + /// A result containing the number of edges from the vertex in the window. + /// + /// # Errors + /// + /// Returns an error if `v` is not a valid vertex. + #[inline] + fn degree(&self, v: VID, d: Direction, layer: &LayerIds, filter: Option<&EdgeFilter>) -> usize { + self.graph.degree(v, d, layer, filter) + } + + /// Get the reference of the vertex with ID v if it exists + /// + /// # Arguments + /// + /// - `v` - The ID of the vertex to get + /// + /// # Returns + /// + /// A result of an option containing the vertex reference if it exists, `None` otherwise. + /// + /// # Errors + /// + /// Returns an error if `v` is not a valid vertex. + #[inline] + fn vertex_ref(&self, v: u64, layers: &LayerIds, filter: Option<&EdgeFilter>) -> Option { + self.internal_vertex_ref(v.into(), layers, filter) + } + + /// Get an iterator over the references of all vertices as references + /// + /// # Returns + /// + /// An iterator over the references of all vertices + #[inline] + fn vertex_refs( + &self, + layers: LayerIds, + filter: Option<&EdgeFilter>, + ) -> Box + Send> { + let g = self.clone(); + let filter_cloned = filter.cloned(); + Box::new( + self.graph + .vertex_refs(layers.clone(), filter) + .filter(move |v| { + g.include_vertex_window(*v, g.t_start..g.t_end, &layers, filter_cloned.as_ref()) + }), + ) + } + + /// Get an iterator over the references of an edges as a reference + /// + /// # Arguments + /// + /// - `src` - The source vertex of the edge + /// - `dst` - The destination vertex of the edge + /// + /// # Returns + /// + /// A result of an option containing the edge reference if it exists, `None` otherwise. + /// + /// # Errors + /// + /// Returns an error if `src` or `dst` are not valid vertices. + #[inline] + fn edge_ref( + &self, + src: VID, + dst: VID, + layer: &LayerIds, + filter: Option<&EdgeFilter>, + ) -> Option { + self.graph.edge_ref(src, dst, layer, filter) + } + + /// Get an iterator of all edges as references + /// + /// # Returns + /// + /// An iterator over all edges as references + #[inline] + fn edge_refs( + &self, + layer: LayerIds, + filter: Option<&EdgeFilter>, + ) -> Box + Send> { + self.graph.edge_refs(layer, filter) + } + + #[inline] + fn vertex_edges( + &self, + v: VID, + d: Direction, + layer: LayerIds, + filter: Option<&EdgeFilter>, + ) -> Box + Send> { + self.graph.vertex_edges(v, d, layer, filter) + } + + /// Get the neighbours of a vertex as references in a given direction + /// + /// # Arguments + /// + /// - `v` - The vertex to get the neighbours for + /// - `d` - The direction of the edges + /// + /// # Returns + /// + /// An iterator over all neighbours in that vertex direction as references + #[inline] + fn neighbours( + &self, + v: VID, + d: Direction, + layer: LayerIds, + filter: Option<&EdgeFilter>, + ) -> Box + Send> { + self.graph.neighbours(v, d, layer, filter) + } +} + +/// A windowed graph is a graph that only allows access to vertices and edges within a time window. +/// +/// This struct is used to represent a graph with a time window. It is constructed +/// by providing a `Graph` object and a time range that defines the window. +/// +/// # Examples +/// +/// ```rust +/// use raphtory::db::api::view::*; +/// use raphtory::prelude::*; +/// +/// let graph = Graph::new(); +/// graph.add_edge(0, 1, 2, NO_PROPS, None).unwrap(); +/// graph.add_edge(1, 2, 3, NO_PROPS, None).unwrap(); +/// let windowed_graph = graph.window(0, 1); +/// ``` +impl WindowedGraph { + /// Create a new windowed graph + /// + /// # Arguments + /// + /// - `graph` - The graph to create the windowed graph from + /// - `t_start` - The inclusive start time of the window. + /// - `t_end` - The exclusive end time of the window. + /// + /// # Returns + /// + /// A new windowed graph + pub fn new(graph: G, t_start: T, t_end: T) -> Self { + let filter_graph = graph.clone(); + let t_start = t_start.into_time(); + let t_end = t_end.into_time(); + let base_filter = filter_graph.edge_filter_window().cloned(); + let filter: EdgeFilter = match base_filter { + Some(f) => Arc::new(move |e, layers| { + f(e, layers) && filter_graph.include_edge_window(e, t_start..t_end, layers) + }), + None => Arc::new(move |e, layers| { + filter_graph.include_edge_window(e, t_start..t_end, layers) + }), + }; + WindowedGraph { + graph, + t_start, + t_end, + filter, + } + } + + /// the larger of `t_start` and `self.start()` (useful for creating nested windows) + #[inline] + fn actual_start(&self, t_start: i64) -> i64 { + max(t_start, self.t_start) + } + + /// the smaller of `t_end` and `self.end()` (useful for creating nested windows) + #[inline] + fn actual_end(&self, t_end: i64) -> i64 { + min(t_end, self.t_end) + } +} + +#[cfg(test)] +mod views_test { + + use super::*; + use crate::{db::api::view::Layer, prelude::*}; + use itertools::Itertools; + use quickcheck::TestResult; + use rand::prelude::*; + use rayon::prelude::*; + + #[test] + fn windowed_graph_vertices_degree() { + let vs = vec![ + (1, 1, 2), + (2, 1, 3), + (-1, 2, 1), + (0, 1, 1), + (7, 3, 2), + (1, 1, 1), + ]; + + let g = Graph::new(); + + for (t, src, dst) in &vs { + g.add_edge(*t, *src, *dst, NO_PROPS, None).unwrap(); + } + + let wg = WindowedGraph::new(g, -1, 1); + + let actual = wg + .vertices() + .iter() + .map(|v| (v.id(), v.degree())) + .collect::>(); + + let expected = vec![(1, 2), (2, 1)]; + + assert_eq!(actual, expected); + } + + #[test] + fn windowed_graph_edge() { + let vs = vec![ + (1, 1, 2), + (2, 1, 3), + (-1, 2, 1), + (0, 1, 1), + (7, 3, 2), + (1, 1, 1), + ]; + + let g = Graph::new(); + + for (t, src, dst) in vs { + g.add_edge(t, src, dst, NO_PROPS, None).unwrap(); + } + + let wg = g.window(i64::MIN, i64::MAX); + assert_eq!(wg.edge(1, 3).unwrap().src().id(), 1); + assert_eq!(wg.edge(1, 3).unwrap().dst().id(), 3); + } + + #[test] + fn windowed_graph_vertex_edges() { + let vs = vec![ + (1, 1, 2), + (2, 1, 3), + (-1, 2, 1), + (0, 1, 1), + (7, 3, 2), + (1, 1, 1), + ]; + + let g = Graph::new(); + + for (t, src, dst) in &vs { + g.add_edge(*t, *src, *dst, NO_PROPS, None).unwrap(); + } + + let wg = WindowedGraph::new(g, -1, 1); + + assert_eq!(wg.vertex(1).unwrap().id(), 1); + } + + #[test] + fn graph_has_vertex_check_fail() { + let vs: Vec<(i64, u64)> = vec![ + (1, 0), + (-100, 262), + // (327226439, 108748364996394682), + (1, 9135428456135679950), + // (0, 1), + // (2, 2), + ]; + let g = Graph::new(); + + for (t, v) in &vs { + g.add_vertex(*t, *v, NO_PROPS) + .map_err(|err| println!("{:?}", err)) + .ok(); + } + + let wg = WindowedGraph::new(g, 1, 2); + assert!(!wg.has_vertex(262)) + } + + #[quickcheck] + fn windowed_graph_has_vertex(mut vs: Vec<(i64, u64)>) -> TestResult { + if vs.is_empty() { + return TestResult::discard(); + } + + vs.sort_by_key(|v| v.1); // Sorted by vertex + vs.dedup_by_key(|v| v.1); // Have each vertex only once to avoid headaches + vs.sort_by_key(|v| v.0); // Sorted by time + + let rand_start_index = thread_rng().gen_range(0..vs.len()); + let rand_end_index = thread_rng().gen_range(rand_start_index..vs.len()); + + let g = Graph::new(); + + for (t, v) in &vs { + g.add_vertex(*t, *v, NO_PROPS) + .map_err(|err| println!("{:?}", err)) + .ok(); + } + + let start = vs.get(rand_start_index).expect("start index in range").0; + let end = vs.get(rand_end_index).expect("end index in range").0; + + let wg = WindowedGraph::new(g, start, end); + + let rand_test_index: usize = thread_rng().gen_range(0..vs.len()); + + let (i, v) = vs.get(rand_test_index).expect("test index in range"); + if (start..end).contains(i) { + if wg.has_vertex(*v) { + TestResult::passed() + } else { + TestResult::error(format!( + "Vertex {:?} was not in window {:?}", + (i, v), + start..end + )) + } + } else if !wg.has_vertex(*v) { + TestResult::passed() + } else { + TestResult::error(format!( + "Vertex {:?} was in window {:?}", + (i, v), + start..end + )) + } + } + + #[quickcheck] + fn windowed_graph_has_edge(mut edges: Vec<(i64, (u64, u64))>) -> TestResult { + if edges.is_empty() { + return TestResult::discard(); + } + + edges.sort_by_key(|e| e.1); // Sorted by edge + edges.dedup_by_key(|e| e.1); // Have each edge only once to avoid headaches + edges.sort_by_key(|e| e.0); // Sorted by time + + let rand_start_index = thread_rng().gen_range(0..edges.len()); + let rand_end_index = thread_rng().gen_range(rand_start_index..edges.len()); + + let g = Graph::new(); + + for (t, e) in &edges { + g.add_edge(*t, e.0, e.1, NO_PROPS, None).unwrap(); + } + + let start = edges.get(rand_start_index).expect("start index in range").0; + let end = edges.get(rand_end_index).expect("end index in range").0; + + let wg = WindowedGraph::new(g, start, end); + + let rand_test_index: usize = thread_rng().gen_range(0..edges.len()); + + let (i, e) = edges.get(rand_test_index).expect("test index in range"); + if (start..end).contains(i) { + if wg.has_edge(e.0, e.1, Layer::All) { + TestResult::passed() + } else { + TestResult::error(format!( + "Edge {:?} was not in window {:?}", + (i, e), + start..end + )) + } + } else if !wg.has_edge(e.0, e.1, Layer::All) { + TestResult::passed() + } else { + TestResult::error(format!("Edge {:?} was in window {:?}", (i, e), start..end)) + } + } + + #[quickcheck] + fn windowed_graph_edge_count( + mut edges: Vec<(i64, (u64, u64))>, + window: Range, + ) -> TestResult { + if window.end < window.start { + return TestResult::discard(); + } + edges.sort_by_key(|e| e.1); // Sorted by edge + edges.dedup_by_key(|e| e.1); // Have each edge only once to avoid headaches + + let true_edge_count = edges.iter().filter(|e| window.contains(&e.0)).count(); + + let g = Graph::new(); + + for (t, e) in &edges { + g.add_edge(*t, e.0, e.1, [("test".to_owned(), Prop::Bool(true))], None) + .unwrap(); + } + + let wg = WindowedGraph::new(g, window.start, window.end); + if wg.count_edges() != true_edge_count { + println!( + "failed, g.num_edges() = {}, true count = {}", + wg.count_edges(), + true_edge_count + ); + println!("g.edges() = {:?}", wg.edges().collect_vec()); + } + TestResult::from_bool(wg.count_edges() == true_edge_count) + } + + #[quickcheck] + fn trivial_window_has_all_edges(edges: Vec<(i64, u64, u64)>) -> bool { + let g = Graph::new(); + edges + .into_par_iter() + .filter(|e| e.0 < i64::MAX) + .for_each(|(t, src, dst)| { + g.add_edge(t, src, dst, [("test".to_owned(), Prop::Bool(true))], None) + .unwrap(); + }); + let w = g.window(i64::MIN, i64::MAX); + g.edges() + .all(|e| w.has_edge(e.src().id(), e.dst().id(), Layer::All)) + } + + #[quickcheck] + fn large_vertex_in_window(dsts: Vec) -> bool { + let dsts: Vec = dsts.into_iter().unique().collect(); + let n = dsts.len(); + let g = Graph::new(); + + for dst in dsts { + let t = 1; + g.add_edge(t, 0, dst, NO_PROPS, None).unwrap(); + } + let w = g.window(i64::MIN, i64::MAX); + w.count_edges() == n + } + + #[test] + fn windowed_graph_vertex_ids() { + let vs = vec![(1, 1, 2), (3, 3, 4), (5, 5, 6), (7, 7, 1)]; + + let args = vec![(i64::MIN, 8), (i64::MIN, 2), (i64::MIN, 4), (3, 6)]; + + let expected = vec![ + vec![1, 2, 3, 4, 5, 6, 7], + vec![1, 2], + vec![1, 2, 3, 4], + vec![3, 4, 5, 6], + ]; + + let g = Graph::new(); + + for (t, src, dst) in &vs { + g.add_edge(*t, *src, *dst, NO_PROPS, None).unwrap(); + } + + let res: Vec<_> = (0..=3) + .map(|i| { + let wg = g.window(args[i].0, args[i].1); + let mut e = wg.vertices().id().collect::>(); + e.sort(); + e + }) + .collect_vec(); + + assert_eq!(res, expected); + + let g = Graph::new(); + for (src, dst, t) in &vs { + g.add_edge(*src, *dst, *t, NO_PROPS, None).unwrap(); + } + let res: Vec<_> = (0..=3) + .map(|i| { + let wg = g.window(args[i].0, args[i].1); + let mut e = wg.vertices().id().collect::>(); + e.sort(); + e + }) + .collect_vec(); + assert_eq!(res, expected); + } + + #[test] + fn windowed_graph_vertices() { + let vs = vec![ + (1, 1, 2), + (2, 1, 3), + (-1, 2, 1), + (0, 1, 1), + (7, 3, 2), + (1, 1, 1), + ]; + + let g = Graph::new(); + + g.add_vertex( + 0, + 1, + [("type", "wallet".into_prop()), ("cost", 99.5.into_prop())], + ) + .map_err(|err| println!("{:?}", err)) + .ok(); + + g.add_vertex( + -1, + 2, + [("type", "wallet".into_prop()), ("cost", 10.0.into_prop())], + ) + .map_err(|err| println!("{:?}", err)) + .ok(); + + g.add_vertex( + 6, + 3, + [("type", "wallet".into_prop()), ("cost", 76.2.into_prop())], + ) + .map_err(|err| println!("{:?}", err)) + .ok(); + + for (t, src, dst) in &vs { + g.add_edge(*t, *src, *dst, [("eprop", "commons")], None) + .unwrap(); + } + + let wg = g.window(-2, 0); + + let actual = wg.vertices().id().collect::>(); + + let expected = vec![1, 2]; + + assert_eq!(actual, expected); + + // Check results from multiple graphs with different number of shards + let g = Graph::new(); + + g.add_vertex( + 0, + 1, + [("type", "wallet".into_prop()), ("cost", 99.5.into_prop())], + ) + .map_err(|err| println!("{:?}", err)) + .ok(); + + g.add_vertex( + -1, + 2, + [("type", "wallet".into_prop()), ("cost", 10.0.into_prop())], + ) + .map_err(|err| println!("{:?}", err)) + .ok(); + + g.add_vertex( + 6, + 3, + [("type", "wallet".into_prop()), ("cost", 76.2.into_prop())], + ) + .map_err(|err| println!("{:?}", err)) + .ok(); + + for (t, src, dst) in &vs { + g.add_edge(*t, *src, *dst, NO_PROPS, None).unwrap(); + } + + let expected = wg.vertices().id().collect::>(); + + assert_eq!(actual, expected); + } +} diff --git a/raphtory/src/db/graph_immutable.rs b/raphtory/src/db/graph_immutable.rs deleted file mode 100644 index 5a133583a9..0000000000 --- a/raphtory/src/db/graph_immutable.rs +++ /dev/null @@ -1,729 +0,0 @@ -//! Defines the `ImmutableGraph` struct, which represents a raphtory graph in a frozen state. -//! This graph can be queried in a read-only format avoiding any locks placed when using a -//! non-immutable graph. -//! -//! # Examples -//! -//! ```rust -//! use raphtory::db::graph::Graph; -//! use raphtory::db::view_api::*; -//! -//! let graph = Graph::new(2); -//! // Add vertices and edges -//! -//! let immutable_graph = graph.freeze(); -//! ``` - -use crate::core::edge_ref::EdgeRef; -use crate::core::tgraph::TemporalGraph; -use crate::core::tgraph_shard::ImmutableTGraphShard; -use crate::core::utils; -use crate::core::vertex_ref::{LocalVertexRef, VertexRef}; -use crate::core::Direction; -use crate::db::graph::Graph; -use itertools::Itertools; -use rustc_hash::FxHashMap; -use serde::{Deserialize, Serialize}; -use std::cmp::{max, min}; -use std::iter; -use std::sync::Arc; - -use super::view_api::internal::GraphViewInternalOps; - -/// A raphtory graph in a frozen state that is read-only. -/// This graph can be queried in a read-only format avoiding any locks placed when using a -/// non-immutable graph. -/// -/// # Examples -/// -/// ```rust -/// use raphtory::db::graph::Graph; -/// use raphtory::db::view_api::*; -/// -/// let graph = Graph::new(2); -/// // Add vertices and edges -/// -/// let immutable_graph = graph.freeze(); -/// ``` -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct ImmutableGraph { - pub(crate) nr_shards: usize, - pub(crate) shards: Vec>, - pub(crate) layer_ids: Arc>, -} - -/// Failure if there is an issue with unfreezing a frozen graph -#[derive(Debug, PartialEq)] -pub struct UnfreezeFailure; - -/// Implements the `ImmutableGraph` struct. -impl ImmutableGraph { - /// Unfreeze the immutable graph and convert it to a mutable `Graph`. - /// - /// # Examples - /// - /// ```rust - /// use raphtory::db::graph::Graph; - /// use raphtory::db::view_api::*; - /// - /// let graph = Graph::new(2); - /// // Add vertices and edges - /// let immutable_graph = graph.freeze(); - /// // Unfreeze the graph - /// let graph = immutable_graph.unfreeze().unwrap(); - /// ``` - pub fn unfreeze(self) -> Result { - let mut shards = Vec::with_capacity(self.shards.len()); - for shard in self.shards { - match shard.unfreeze() { - Ok(t) => shards.push(t), - Err(_) => return Err(UnfreezeFailure), - } - } - Ok(Graph::new_from_frozen( - self.nr_shards, - shards, - Arc::new(parking_lot::RwLock::new((*self.layer_ids).clone())), - )) - } - - /// Get the shard id for a given global vertex id. - /// - /// # Examples - /// - /// ```rust - /// use raphtory::db::graph::Graph; - /// use raphtory::db::view_api::*; - /// - /// let graph = Graph::new(2); - /// graph.add_vertex(0, 1, &vec![]).unwrap(); - /// // ... Add vertices and edges ... - /// let immutable_graph = graph.freeze(); - /// // Unfreeze the graph - /// immutable_graph.shard_id(1); - /// ``` - pub fn shard_id(&self, g_id: u64) -> usize { - utils::get_shard_id_from_global_vid(g_id, self.nr_shards) - } - - /// Get an immutable graph shard for a given global vertex id. - /// - /// # Examples - /// - /// ```rust - /// use raphtory::db::graph::Graph; - /// use raphtory::db::view_api::*; - /// - /// let graph = Graph::new(2); - /// graph.add_vertex(0, 1, &vec![]).unwrap(); - /// // ... Add vertices and edges ... - /// let immutable_graph = graph.freeze(); - /// // Unfreeze the graph - /// let shard = immutable_graph.get_shard_from_id(1); - /// ``` - pub fn get_shard_from_id(&self, g_id: u64) -> &ImmutableTGraphShard { - &self.shards[self.shard_id(g_id)] - } - - /// Get an immutable graph shard for a given vertex. - /// - pub fn get_shard_from_v(&self, v: VertexRef) -> &ImmutableTGraphShard { - match v { - VertexRef::Local(local) => &self.shards[local.shard_id], - VertexRef::Remote(g_id) => &self.shards[self.shard_id(g_id)], - } - } - - pub fn get_shard_from_local_v( - &self, - v: LocalVertexRef, - ) -> &ImmutableTGraphShard { - &self.shards[v.shard_id] - } - - /// Get an immutable graph shard for a given edge. - /// - pub fn get_shard_from_e(&self, e: EdgeRef) -> &ImmutableTGraphShard { - &self.shards[e.shard()] - } - - // Get the earliest time in the graph. - /// - /// # Examples - /// - /// ```rust - /// use raphtory::db::graph::Graph; - /// use raphtory::db::view_api::*; - /// - /// let graph = Graph::new(2); - /// graph.add_vertex(0, 1, &vec![]).unwrap(); - /// // ... Add vertices and edges ... - /// let immutable_graph = graph.freeze(); - /// // Unfreeze the graph - /// let time = immutable_graph.earliest_time(); - /// ``` - pub fn earliest_time(&self) -> Option { - let min_from_shards = self.shards.iter().map(|shard| shard.earliest_time()).min(); - min_from_shards.filter(|&min| min != i64::MAX) - } - - // Get the latest time in the graph. - /// - /// # Examples - /// - /// ```rust - /// use raphtory::db::graph::Graph; - /// use raphtory::db::view_api::*; - /// - /// let graph = Graph::new(2); - /// graph.add_vertex(0, 1, &vec![]).unwrap(); - /// // ... Add vertices and edges ... - /// let immutable_graph = graph.freeze(); - /// // Unfreeze the graph - /// let time = immutable_graph.latest_time(); - /// ``` - pub fn latest_time(&self) -> Option { - let max_from_shards = self.shards.iter().map(|shard| shard.latest_time()).max(); - max_from_shards.filter(|&max| max != i64::MIN) - } - - /// Get the degree for a vertex in the graph given its direction. - pub fn degree(&self, v: LocalVertexRef, d: Direction) -> usize { - self.get_shard_from_local_v(v).degree(v, d, None) - } - - /// Get all vertices in the graph. - /// - /// # Examples - /// - /// ```rust - /// use raphtory::db::graph::Graph; - /// use raphtory::db::view_api::*; - /// - /// let graph = Graph::new(2); - /// graph.add_vertex(0, 1, &vec![]).unwrap(); - /// // ... Add vertices and edges ... - /// let immutable_graph = graph.freeze(); - /// // Unfreeze the graph - /// let vertices = immutable_graph.vertices(); - /// ``` - pub fn vertices(&self) -> Box + Send + '_> { - Box::new(self.shards.iter().flat_map(|s| s.vertices())) - } - - /// Get all edges in the graph. - /// - /// # Examples - /// - /// ```rust - /// use raphtory::db::graph::Graph; - /// use raphtory::db::view_api::*; - /// - /// let graph = Graph::new(2); - /// graph.add_edge(0, 1, 1, &vec![], None).unwrap(); - /// // ... Add vertices and edges ... - /// let immutable_graph = graph.freeze(); - /// // Unfreeze the graph - /// let edges = immutable_graph.edges(); - /// ``` - pub fn edges(&self) -> Box + Send + '_> { - Box::new(self.vertices().flat_map(|v| { - self.get_shard_from_local_v(v) - .vertex_edges(v, Direction::OUT, None) - })) - } - - /// Get number of edges in the graph. - /// - /// # Examples - /// - /// ```rust - /// use raphtory::db::graph::Graph; - /// use raphtory::db::view_api::*; - /// - /// let graph = Graph::new(2); - /// graph.add_edge(0, 1, 2, &vec![], None).unwrap(); - /// // ... Add vertices and edges ... - /// let immutable_graph = graph.freeze(); - /// // Unfreeze the graph - /// let num_edges = immutable_graph.num_edges(); - /// ``` - pub fn num_edges(&self) -> usize { - self.shards - .iter() - .map(|shard| shard.out_edges_len(None)) - .sum() - } - - fn localise_edge(&self, src: VertexRef, dst: VertexRef) -> (usize, VertexRef, VertexRef) { - match src { - VertexRef::Local(local_src) => match dst { - VertexRef::Local(local_dst) => { - if local_src.shard_id == local_dst.shard_id { - (local_src.shard_id, src, dst) - } else { - ( - local_src.shard_id, - src, - VertexRef::Remote(self.vertex_id(local_dst)), - ) - } - } - VertexRef::Remote(_) => (local_src.shard_id, src, dst), - }, - VertexRef::Remote(gid) => match dst { - VertexRef::Local(local_dst) => (local_dst.shard_id, src, dst), - VertexRef::Remote(_) => (self.shard_id(gid), src, dst), - }, - } - } -} - -impl GraphViewInternalOps for ImmutableGraph { - fn local_vertex(&self, v: VertexRef) -> Option { - self.get_shard_from_v(v).local_vertex(v) - } - - fn local_vertex_window( - &self, - v: VertexRef, - t_start: i64, - t_end: i64, - ) -> Option { - self.get_shard_from_v(v) - .local_vertex_window(v, t_start..t_end) - } - - fn get_unique_layers_internal(&self) -> Vec { - let a = iter::once(0); - let b = self.layer_ids.values().copied(); - a.chain(b).collect_vec() - } - - fn get_layer_name_by_id(&self, layer_id: usize) -> String { - self.layer_ids - .iter() - .find_map(|(name, &id)| (layer_id == id).then_some(name)) - .expect(&format!("layer id '{layer_id}' doesn't exist")) - .to_string() - } - - fn get_layer(&self, key: Option<&str>) -> Option { - match key { - None => Some(0), - Some(key) => self.layer_ids.get(key).copied(), - } - } - - fn view_start(&self) -> Option { - self.earliest_time_global() - } - - fn view_end(&self) -> Option { - self.latest_time_global().map(|t| t + 1) // so it is exclusive - } - - fn earliest_time_global(&self) -> Option { - let min_from_shards = self.shards.iter().map(|shard| shard.earliest_time()).min(); - min_from_shards.filter(|&min| min != i64::MAX) - } - - fn earliest_time_window(&self, t_start: i64, t_end: i64) -> Option { - //FIXME: this is not correct, should actually be the earliest activity in window - let earliest = self.earliest_time_global()?; - if earliest > t_end { - None - } else { - Some(max(earliest, t_start)) - } - } - - fn latest_time_global(&self) -> Option { - let max_from_shards = self.shards.iter().map(|shard| shard.latest_time()).max(); - max_from_shards.filter(|&max| max != i64::MIN) - } - - fn latest_time_window(&self, t_start: i64, t_end: i64) -> Option { - //FIXME: this is not correct, should actually be the latest activity in window - let latest = self.latest_time_global()?; - if latest < t_start { - None - } else { - Some(min(latest, t_end)) - } - } - - fn vertices_len(&self) -> usize { - self.shards.iter().map(|shard| shard.len()).sum() - } - - fn vertices_len_window(&self, t_start: i64, t_end: i64) -> usize { - //FIXME: This nees to be optimised ideally - self.shards - .iter() - .map(|shard| shard.vertices_window(t_start..t_end).count()) - .sum() - } - - fn edges_len(&self, layer: Option) -> usize { - let vs: Vec = self - .shards - .iter() - .map(|shard| shard.out_edges_len(layer)) - .collect(); - vs.iter().sum() - } - - fn edges_len_window(&self, t_start: i64, t_end: i64, layer: Option) -> usize { - self.shards - .iter() - .map(|shard| shard.out_edges_len_window(&(t_start..t_end), layer)) - .sum() - } - - fn has_edge_ref(&self, src: VertexRef, dst: VertexRef, layer: usize) -> bool { - let (shard, src, dst) = self.localise_edge(src, dst); - self.shards[shard].has_edge(src, dst, layer) - } - - fn has_edge_ref_window( - &self, - src: VertexRef, - dst: VertexRef, - t_start: i64, - t_end: i64, - layer: usize, - ) -> bool { - let (shard, src, dst) = self.localise_edge(src, dst); - self.shards[shard].has_edge_window(src, dst, t_start..t_end, layer) - } - - fn has_vertex_ref(&self, v: VertexRef) -> bool { - self.get_shard_from_v(v).has_vertex(v) - } - - fn has_vertex_ref_window(&self, v: VertexRef, t_start: i64, t_end: i64) -> bool { - self.get_shard_from_v(v) - .has_vertex_window(v, t_start..t_end) - } - - fn degree(&self, v: LocalVertexRef, d: Direction, layer: Option) -> usize { - self.get_shard_from_local_v(v).degree(v, d, layer) - } - - fn degree_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - d: Direction, - layer: Option, - ) -> usize { - self.get_shard_from_local_v(v) - .degree_window(v, t_start..t_end, d, layer) - } - - fn vertex_ref(&self, v: u64) -> Option { - self.get_shard_from_id(v).vertex(v) - } - - fn vertex_id(&self, v: LocalVertexRef) -> u64 { - self.shards[v.shard_id].vertex_id(v) - } - - fn vertex_ref_window(&self, v: u64, t_start: i64, t_end: i64) -> Option { - self.get_shard_from_id(v).vertex_window(v, t_start..t_end) - } - - fn vertex_earliest_time(&self, v: LocalVertexRef) -> Option { - self.get_shard_from_local_v(v).vertex_earliest_time(v) - } - - fn vertex_earliest_time_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - ) -> Option { - self.get_shard_from_local_v(v) - .vertex_earliest_time_window(v, t_start..t_end) - } - - fn vertex_latest_time(&self, v: LocalVertexRef) -> Option { - self.get_shard_from_local_v(v).vertex_latest_time(v) - } - - fn vertex_latest_time_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - ) -> Option { - self.get_shard_from_local_v(v) - .vertex_latest_time_window(v, t_start..t_end) - } - - fn vertex_refs(&self) -> Box + Send> { - let shards = self.shards.clone(); - Box::new(shards.into_iter().flat_map(|s| s.vertices())) - } - - fn vertex_refs_window( - &self, - t_start: i64, - t_end: i64, - ) -> Box + Send> { - let shards = self.shards.clone(); - Box::new( - shards - .into_iter() - .flat_map(move |s| s.vertices_window(t_start..t_end)), - ) - } - - fn vertex_refs_shard(&self, shard: usize) -> Box + Send> { - let shard = self.shards[shard].clone(); - Box::new(shard.vertices()) - } - - fn vertex_refs_window_shard( - &self, - shard: usize, - t_start: i64, - t_end: i64, - ) -> Box + Send> { - let shard = self.shards[shard].clone(); - Box::new(shard.vertices_window(t_start..t_end)) - } - - fn edge_ref(&self, src: VertexRef, dst: VertexRef, layer: usize) -> Option { - let (shard_id, src, dst) = self.localise_edge(src, dst); - self.shards[shard_id].edge(src, dst, layer) - } - - fn edge_ref_window( - &self, - src: VertexRef, - dst: VertexRef, - t_start: i64, - t_end: i64, - layer: usize, - ) -> Option { - let (shard_id, src, dst) = self.localise_edge(src, dst); - self.shards[shard_id].edge_window(src, dst, t_start..t_end, layer) - } - - fn edge_refs(&self, layer: Option) -> Box + Send> { - //FIXME: needs low-level primitive - let g = self.clone(); - match layer { - Some(layer) => Box::new( - self.vertex_refs() - .flat_map(move |v| g.vertex_edges(v, Direction::OUT, Some(layer))), - ), - None => Box::new( - self.vertex_refs() - .flat_map(move |v| g.vertex_edges(v, Direction::OUT, None)), - ), - } - } - - fn edge_refs_window( - &self, - t_start: i64, - t_end: i64, - layer: Option, - ) -> Box + Send> { - //FIXME: needs low-level primitive - let g = self.clone(); - Box::new( - self.vertex_refs() - .flat_map(move |v| g.vertex_edges_window(v, t_start, t_end, Direction::OUT, layer)), - ) - } - - fn vertex_edges( - &self, - v: LocalVertexRef, - d: Direction, - layer: Option, - ) -> Box + Send> { - Box::new(self.get_shard_from_local_v(v).vertex_edges(v, d, layer)) - } - - fn vertex_edges_t( - &self, - v: LocalVertexRef, - d: Direction, - layer: Option, - ) -> Box + Send> { - // FIXME: missing low-level implementation - Box::new(self.get_shard_from_local_v(v).vertex_edges_window_t( - v, - i64::MIN..i64::MAX, - d, - layer, - )) - } - - fn vertex_edges_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - d: Direction, - layer: Option, - ) -> Box + Send> { - Box::new( - self.get_shard_from_local_v(v) - .vertex_edges_window(v, t_start..t_end, d, layer), - ) - } - - fn vertex_edges_window_t( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - d: Direction, - layer: Option, - ) -> Box + Send> { - Box::new( - self.get_shard_from_local_v(v) - .vertex_edges_window_t(v, t_start..t_end, d, layer), - ) - } - - fn neighbours( - &self, - v: LocalVertexRef, - d: Direction, - layer: Option, - ) -> Box + Send> { - Box::new(self.get_shard_from_local_v(v).neighbours(v, d, layer)) - } - - fn neighbours_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - d: Direction, - layer: Option, - ) -> Box + Send> { - Box::new( - self.get_shard_from_local_v(v) - .neighbours_window(v, t_start..t_end, d, layer), - ) - } - - fn static_vertex_prop(&self, v: LocalVertexRef, name: String) -> Option { - self.get_shard_from_local_v(v).static_vertex_prop(v, name) - } - - fn static_vertex_prop_names(&self, v: LocalVertexRef) -> Vec { - self.get_shard_from_local_v(v).static_vertex_prop_names(v) - } - - fn temporal_vertex_prop_names(&self, v: LocalVertexRef) -> Vec { - self.get_shard_from_local_v(v).temporal_vertex_prop_names(v) - } - - fn temporal_vertex_prop_vec( - &self, - v: LocalVertexRef, - name: String, - ) -> Vec<(i64, crate::core::Prop)> { - self.get_shard_from_local_v(v) - .temporal_vertex_prop_vec(v, name) - } - - fn vertex_timestamps(&self, v: LocalVertexRef) -> Vec { - self.get_shard_from_local_v(v).vertex_timestamps(v) - } - - fn vertex_timestamps_window(&self, v: LocalVertexRef, t_start: i64, t_end: i64) -> Vec { - self.get_shard_from_local_v(v) - .vertex_timestamps_window(v, t_start..t_end) - } - - fn temporal_vertex_prop_vec_window( - &self, - v: LocalVertexRef, - name: String, - t_start: i64, - t_end: i64, - ) -> Vec<(i64, crate::core::Prop)> { - self.get_shard_from_local_v(v) - .temporal_vertex_prop_vec_window(v, name, t_start..t_end) - } - - fn temporal_vertex_props( - &self, - v: LocalVertexRef, - ) -> std::collections::HashMap> { - self.get_shard_from_local_v(v).temporal_vertex_props(v) - } - - fn temporal_vertex_props_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - ) -> std::collections::HashMap> { - self.get_shard_from_local_v(v) - .temporal_vertex_props_window(v, t_start..t_end) - } - - fn static_edge_prop(&self, e: EdgeRef, name: String) -> Option { - self.get_shard_from_e(e).static_edge_prop(e, name) - } - - fn static_edge_prop_names(&self, e: EdgeRef) -> Vec { - self.get_shard_from_e(e).static_edge_prop_names(e) - } - - fn temporal_edge_prop_names(&self, e: EdgeRef) -> Vec { - self.get_shard_from_e(e).temporal_edge_prop_names(e) - } - - fn temporal_edge_props_vec(&self, e: EdgeRef, name: String) -> Vec<(i64, crate::core::Prop)> { - self.get_shard_from_e(e).temporal_edge_prop_vec(e, name) - } - - fn temporal_edge_props_vec_window( - &self, - e: EdgeRef, - name: String, - t_start: i64, - t_end: i64, - ) -> Vec<(i64, crate::core::Prop)> { - self.get_shard_from_e(e) - .temporal_edge_props_vec_window(e, name, t_start..t_end) - } - - fn edge_timestamps(&self, e: EdgeRef, window: Option>) -> Vec { - self.get_shard_from_e(e).edge_timestamps(e, window) - } - - fn temporal_edge_props( - &self, - e: EdgeRef, - ) -> std::collections::HashMap> { - self.get_shard_from_e(e).temporal_edge_props(e) - } - - fn temporal_edge_props_window( - &self, - e: EdgeRef, - t_start: i64, - t_end: i64, - ) -> std::collections::HashMap> { - self.get_shard_from_e(e) - .temporal_edge_props_window(e, t_start..t_end) - } - - fn num_shards(&self) -> usize { - self.nr_shards - } -} diff --git a/raphtory/src/db/graph_layer.rs b/raphtory/src/db/graph_layer.rs deleted file mode 100644 index 26e6ef2c0b..0000000000 --- a/raphtory/src/db/graph_layer.rs +++ /dev/null @@ -1,426 +0,0 @@ -use crate::core::edge_ref::EdgeRef; -use crate::core::vertex_ref::{LocalVertexRef, VertexRef}; -use crate::core::{Direction, Prop}; -use crate::db::view_api::internal::GraphViewInternalOps; -use itertools::Itertools; -use std::{collections::HashMap, ops::Range}; - -#[derive(Debug, Clone)] -pub struct LayeredGraph { - /// The underlying `Graph` object. - pub graph: G, - /// The layer this graphs points to. - pub layer: usize, -} - -impl LayeredGraph { - pub fn new(graph: G, layer: usize) -> Self { - Self { graph, layer } - } - - /// Return None if the intersection between the previously requested layers and the layer of - /// this view is null - fn constrain(&self, layer: Option) -> Option { - match layer { - None => Some(self.layer), - Some(layer) if layer == self.layer => Some(layer), - _ => None, - } - } -} - -impl GraphViewInternalOps for LayeredGraph { - fn get_unique_layers_internal(&self) -> Vec { - let layers = self.graph.get_unique_layers_internal(); - layers - .into_iter() - .filter(|id| *id == self.layer) - .collect_vec() - } - - fn get_layer(&self, key: Option<&str>) -> Option { - self.graph.get_layer(key) - } - - fn get_layer_name_by_id(&self, layer_id: usize) -> String { - self.graph.get_layer_name_by_id(layer_id) - } - - fn view_start(&self) -> Option { - self.graph.view_start() - } - - fn view_end(&self) -> Option { - self.graph.view_end() - } - - fn earliest_time_global(&self) -> Option { - self.graph.earliest_time_global() - } - - fn earliest_time_window(&self, t_start: i64, t_end: i64) -> Option { - self.graph.earliest_time_window(t_start, t_end) - } - - fn latest_time_global(&self) -> Option { - self.graph.latest_time_global() - } - - fn latest_time_window(&self, t_start: i64, t_end: i64) -> Option { - self.graph.latest_time_window(t_start, t_end) - } - - fn vertices_len(&self) -> usize { - self.graph.vertices_len() - } - - fn vertices_len_window(&self, t_start: i64, t_end: i64) -> usize { - self.graph.vertices_len_window(t_start, t_end) - } - - fn edges_len(&self, layer: Option) -> usize { - self.constrain(layer) - .map(|layer| self.graph.edges_len(Some(layer))) - .unwrap_or(0) - } - - fn edges_len_window(&self, t_start: i64, t_end: i64, layer: Option) -> usize { - self.constrain(layer) - .map(|layer| self.graph.edges_len_window(t_start, t_end, Some(layer))) - .unwrap_or(0) - } - - fn has_edge_ref(&self, src: VertexRef, dst: VertexRef, layer: usize) -> bool { - // FIXME: there is something wrong here, the layer should be able to be None, which would mean, whatever layer this is - layer == self.layer && self.graph.has_edge_ref(src, dst, layer) - } - - fn has_edge_ref_window( - &self, - src: VertexRef, - dst: VertexRef, - t_start: i64, - t_end: i64, - layer: usize, - ) -> bool { - layer == self.layer - && self - .graph - .has_edge_ref_window(src, dst, t_start, t_end, layer) - } - - fn has_vertex_ref(&self, v: VertexRef) -> bool { - self.graph.has_vertex_ref(v) - } - - fn has_vertex_ref_window(&self, v: VertexRef, t_start: i64, t_end: i64) -> bool { - self.graph.has_vertex_ref_window(v, t_start, t_end) - } - - fn degree(&self, v: LocalVertexRef, d: Direction, layer: Option) -> usize { - self.constrain(layer) - .map(|layer| self.graph.degree(v, d, Some(layer))) - .unwrap_or(0) - } - - fn degree_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - d: Direction, - layer: Option, - ) -> usize { - self.constrain(layer) - .map(|layer| self.graph.degree_window(v, t_start, t_end, d, Some(layer))) - .unwrap_or(0) - } - - fn vertex_ref(&self, v: u64) -> Option { - self.graph.vertex_ref(v) - } - - fn vertex_ref_window(&self, v: u64, t_start: i64, t_end: i64) -> Option { - self.graph.vertex_ref_window(v, t_start, t_end) - } - - fn vertex_earliest_time(&self, v: LocalVertexRef) -> Option { - self.graph.vertex_earliest_time(v) - } - - fn vertex_earliest_time_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - ) -> Option { - self.graph.vertex_earliest_time_window(v, t_start, t_end) - } - - fn vertex_latest_time(&self, v: LocalVertexRef) -> Option { - self.graph.vertex_latest_time(v) - } - - fn vertex_latest_time_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - ) -> Option { - self.graph.vertex_latest_time_window(v, t_start, t_end) - } - - fn vertex_refs(&self) -> Box + Send> { - self.graph.vertex_refs() - } - - fn vertex_refs_window( - &self, - t_start: i64, - t_end: i64, - ) -> Box + Send> { - self.graph.vertex_refs_window(t_start, t_end) - } - - fn vertex_refs_shard(&self, shard: usize) -> Box + Send> { - self.graph.vertex_refs_shard(shard) - } - - fn vertex_refs_window_shard( - &self, - shard: usize, - t_start: i64, - t_end: i64, - ) -> Box + Send> { - self.graph.vertex_refs_window_shard(shard, t_start, t_end) - } - - fn edge_ref(&self, src: VertexRef, dst: VertexRef, layer: usize) -> Option { - (layer == self.layer) - .then(|| self.graph.edge_ref(src, dst, layer)) - .flatten() - } - - fn edge_ref_window( - &self, - src: VertexRef, - dst: VertexRef, - t_start: i64, - t_end: i64, - layer: usize, - ) -> Option { - (layer == self.layer) - .then(|| self.graph.edge_ref_window(src, dst, t_start, t_end, layer)) - .flatten() - } - - fn edge_refs(&self, layer: Option) -> Box + Send> { - // TODO: create a function empty_iter which returns a boxed empty iterator so we use it in all these functions - self.constrain(layer) - .map(|layer| self.graph.edge_refs(Some(layer))) - .unwrap_or_else(|| Box::new(std::iter::empty())) - } - - fn edge_refs_window( - &self, - t_start: i64, - t_end: i64, - layer: Option, - ) -> Box + Send> { - self.constrain(layer) - .map(|layer| self.graph.edge_refs_window(t_start, t_end, Some(layer))) - .unwrap_or_else(|| Box::new(std::iter::empty())) - } - - fn vertex_edges_t( - &self, - v: LocalVertexRef, - d: Direction, - layer: Option, - ) -> Box + Send> { - self.constrain(layer) - .map(|layer| self.graph.vertex_edges_t(v, d, Some(layer))) - .unwrap_or(Box::new(std::iter::empty())) - } - - fn vertex_edges_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - d: Direction, - layer: Option, - ) -> Box + Send> { - self.constrain(layer) - .map(|layer| { - self.graph - .vertex_edges_window(v, t_start, t_end, d, Some(layer)) - }) - .unwrap_or_else(|| Box::new(std::iter::empty())) - } - - fn vertex_edges_window_t( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - d: Direction, - layer: Option, - ) -> Box + Send> { - self.constrain(layer) - .map(|layer| { - self.graph - .vertex_edges_window_t(v, t_start, t_end, d, Some(layer)) - }) - .unwrap_or_else(|| Box::new(std::iter::empty())) - } - - fn neighbours( - &self, - v: LocalVertexRef, - d: Direction, - layer: Option, - ) -> Box + Send> { - self.constrain(layer) - .map(|layer| self.graph.neighbours(v, d, Some(layer))) - .unwrap_or_else(|| Box::new(std::iter::empty())) - } - - fn neighbours_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - d: Direction, - layer: Option, - ) -> Box + Send> { - self.constrain(layer) - .map(|layer| { - self.graph - .neighbours_window(v, t_start, t_end, d, Some(layer)) - }) - .unwrap_or_else(|| Box::new(std::iter::empty())) - } - - fn static_vertex_prop(&self, v: LocalVertexRef, name: String) -> Option { - self.graph.static_vertex_prop(v, name) - } - - fn static_vertex_prop_names(&self, v: LocalVertexRef) -> Vec { - self.graph.static_vertex_prop_names(v) - } - - fn temporal_vertex_prop_names(&self, v: LocalVertexRef) -> Vec { - self.graph.temporal_vertex_prop_names(v) - } - - fn temporal_vertex_prop_vec(&self, v: LocalVertexRef, name: String) -> Vec<(i64, Prop)> { - self.graph.temporal_vertex_prop_vec(v, name) - } - - fn temporal_vertex_prop_vec_window( - &self, - v: LocalVertexRef, - name: String, - t_start: i64, - t_end: i64, - ) -> Vec<(i64, Prop)> { - self.graph - .temporal_vertex_prop_vec_window(v, name, t_start, t_end) - } - - fn temporal_vertex_props(&self, v: LocalVertexRef) -> HashMap> { - self.graph.temporal_vertex_props(v) - } - - fn temporal_vertex_props_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - ) -> HashMap> { - self.graph.temporal_vertex_props_window(v, t_start, t_end) - } - - fn static_edge_prop(&self, e: EdgeRef, name: String) -> Option { - self.graph.static_edge_prop(e, name) - } - - fn static_edge_prop_names(&self, e: EdgeRef) -> Vec { - self.graph.static_edge_prop_names(e) - } - - fn temporal_edge_prop_names(&self, e: EdgeRef) -> Vec { - self.graph.temporal_edge_prop_names(e) - } - - fn temporal_edge_props_vec(&self, e: EdgeRef, name: String) -> Vec<(i64, Prop)> { - self.graph.temporal_edge_props_vec(e, name) - } - - fn temporal_edge_props_vec_window( - &self, - e: EdgeRef, - name: String, - t_start: i64, - t_end: i64, - ) -> Vec<(i64, Prop)> { - self.graph - .temporal_edge_props_vec_window(e, name, t_start, t_end) - } - - fn temporal_edge_props(&self, e: EdgeRef) -> HashMap> { - self.graph.temporal_edge_props(e) - } - - fn temporal_edge_props_window( - &self, - e: EdgeRef, - t_start: i64, - t_end: i64, - ) -> HashMap> { - self.graph.temporal_edge_props_window(e, t_start, t_end) - } - - fn num_shards(&self) -> usize { - self.graph.num_shards() - } - - fn vertex_timestamps(&self, v: LocalVertexRef) -> Vec { - self.graph.vertex_timestamps(v) - } - - fn vertex_timestamps_window(&self, v: LocalVertexRef, t_start: i64, t_end: i64) -> Vec { - self.graph.vertex_timestamps_window(v, t_start, t_end) - } - - fn edge_timestamps(&self, e: EdgeRef, window: Option>) -> Vec { - self.graph.edge_timestamps(e, window) - } - - fn vertex_edges( - &self, - v: LocalVertexRef, - d: Direction, - layer: Option, - ) -> Box + Send> { - self.graph.vertex_edges(v, d, self.constrain(layer)) - } - - fn vertex_id(&self, v: LocalVertexRef) -> u64 { - self.graph.vertex_id(v) - } - - fn local_vertex(&self, v: VertexRef) -> Option { - self.graph.local_vertex(v) - } - - fn local_vertex_window( - &self, - v: VertexRef, - t_start: i64, - t_end: i64, - ) -> Option { - self.graph.local_vertex_window(v, t_start, t_end) - } -} diff --git a/raphtory/src/db/graph_window.rs b/raphtory/src/db/graph_window.rs deleted file mode 100644 index e7e822b572..0000000000 --- a/raphtory/src/db/graph_window.rs +++ /dev/null @@ -1,1424 +0,0 @@ -//! A windowed view is a subset of a graph between a specific time window. -//! For example, lets say you wanted to run an algorithm each month over a graph, graph window -//! would allow you to split the graph into 30 day chunks to do so. -//! -//! This module also defines the `GraphWindow` trait, which represents a window of time over -//! which a graph can be queried. -//! -//! GraphWindowSet implements the `Iterator` trait, producing `WindowedGraph` views -//! for each perspective within it. -//! -//! # Types -//! -//! * `GraphWindowSet` - A struct that allows iterating over a Graph broken down into multiple -//! windowed views. It contains a `Graph` and an iterator of `Perspective`. -//! -//! * `WindowedGraph` - A struct that represents a windowed view of a `Graph`. -//! It contains a `Graph`, a start time (`t_start`) and an end time (`t_end`). -//! -//! # Traits -//! -//! * `GraphViewInternalOps` - A trait that provides operations to a `WindowedGraph` -//! used internally by the `GraphWindowSet`. -//! -//! # Examples -//! -//! ```rust -//! -//! use raphtory::db::graph::Graph; -//! use raphtory::db::view_api::*; -//! -//! let graph = Graph::new(2); -//! graph.add_edge(0, 1, 2, &vec![], None); -//! graph.add_edge(1, 1, 3, &vec![], None); -//! graph.add_edge(2, 2, 3, &vec![], None); -//! -//! let wg = graph.window(0, 1); -//! assert_eq!(wg.edge(1, 2, None).unwrap().src().id(), 1); -//! ``` - -use crate::core::edge_ref::EdgeRef; -use crate::core::time::IntoTime; -use crate::core::vertex_ref::{LocalVertexRef, VertexRef}; -use crate::core::{Direction, Prop}; -use crate::db::view_api::internal::GraphViewInternalOps; -use crate::db::view_api::GraphViewOps; -use std::cmp::{max, min}; -use std::{collections::HashMap, ops::Range}; - -/// A struct that represents a windowed view of a `Graph`. -#[derive(Debug, Clone)] -pub struct WindowedGraph { - /// The underlying `Graph` object. - pub graph: G, - /// The inclusive start time of the window. - pub t_start: i64, - /// The exclusive end time of the window. - pub t_end: i64, -} - -/// Implementation of the GraphViewInternalOps trait for WindowedGraph. -/// This trait provides operations to a `WindowedGraph` used internally by the `GraphWindowSet`. -/// *Note: All functions in this are bound by the time set in the windowed graph. -impl GraphViewInternalOps for WindowedGraph { - fn get_unique_layers_internal(&self) -> Vec { - self.graph.get_unique_layers_internal() - } - - fn get_layer(&self, key: Option<&str>) -> Option { - self.graph.get_layer(key) - } - - fn get_layer_name_by_id(&self, layer_id: usize) -> String { - self.graph.get_layer_name_by_id(layer_id) - } - - fn view_start(&self) -> Option { - Some(self.t_start) - } - - fn view_end(&self) -> Option { - Some(self.t_end) - } - - fn earliest_time_global(&self) -> Option { - self.graph.earliest_time_window(self.t_start, self.t_end) - } - - fn earliest_time_window(&self, t_start: i64, t_end: i64) -> Option { - self.graph - .earliest_time_window(self.actual_start(t_start), self.actual_end(t_end)) - } - - fn latest_time_global(&self) -> Option { - self.graph.latest_time_window(self.t_start, self.t_end) - } - - fn latest_time_window(&self, t_start: i64, t_end: i64) -> Option { - self.graph - .latest_time_window(self.actual_start(t_start), self.actual_end(t_end)) - } - - /// Returns the number of vertices in the windowed view. - fn vertices_len(&self) -> usize { - self.graph.vertices_len_window(self.t_start, self.t_end) - } - - /// Returns the number of vertices in the windowed view, for a window specified by start and end times. - /// - /// # Arguments - /// - /// * `t_start` - The inclusive start time of the window. - /// * `t_end` - The exclusive end time of the window. - /// - /// # Returns - /// - /// The number of vertices in the windowed view for the given window. - fn vertices_len_window(&self, t_start: i64, t_end: i64) -> usize { - self.graph - .vertices_len_window(self.actual_start(t_start), self.actual_end(t_end)) - } - - /// Returns the number of edges in the windowed view. - fn edges_len(&self, layer: Option) -> usize { - self.graph.edges_len_window(self.t_start, self.t_end, layer) - } - - /// Returns the number of edges in the windowed view, for a window specified by start and end times. - /// - /// # Arguments - /// - /// * `t_start` - The inclusive start time of the window. - /// * `t_end` - The exclusive end time of the window. - /// - /// # Returns - /// - /// The number of edges in the windowed view for the given window. - fn edges_len_window(&self, t_start: i64, t_end: i64, layer: Option) -> usize { - self.graph - .edges_len_window(self.actual_start(t_start), self.actual_end(t_end), layer) - } - - /// Check if there is an edge from src to dst in the window. - /// - /// # Arguments - /// - /// - `src` - The source vertex. - /// - `dst` - The destination vertex. - /// - /// # Returns - /// - /// A result containing `true` if there is an edge from src to dst in the window, `false` otherwise. - /// - /// # Errors - /// - /// Returns an error if either `src` or `dst` is not a valid vertex. - fn has_edge_ref(&self, src: VertexRef, dst: VertexRef, layer: usize) -> bool { - self.graph - .has_edge_ref_window(src, dst, self.t_start, self.t_end, layer) - } - - /// Check if there is an edge from src to dst in the window defined by t_start and t_end. - /// - /// # Arguments - /// - /// - `src` - The source vertex. - /// - `dst` - The destination vertex. - /// - `t_start` - The inclusive start time of the window. - /// - `t_end` - The exclusive end time of the window. - /// - /// # Returns - /// - /// A result containing `true` if there is an edge from src to dst in the window, `false` otherwise. - /// - /// # Errors - /// - /// Returns an error if either `src` or `dst` is not a valid vertex. - fn has_edge_ref_window( - &self, - src: VertexRef, - dst: VertexRef, - t_start: i64, - t_end: i64, - layer: usize, - ) -> bool { - self.graph.has_edge_ref_window( - src, - dst, - self.actual_start(t_start), - self.actual_end(t_end), - layer, - ) - } - - /// Check if a vertex v exists in the window. - /// - /// # Arguments - /// - /// - `v` - The vertex to check. - /// - /// # Returns - /// - /// A result containing `true` if the vertex exists in the window, `false` otherwise. - /// - /// # Errors - /// - /// Returns an error if `v` is not a valid vertex. - fn has_vertex_ref(&self, v: VertexRef) -> bool { - self.graph - .has_vertex_ref_window(v, self.t_start, self.t_end) - } - - /// Check if a vertex v exists in the window defined by t_start and t_end. - /// - /// # Arguments - /// - /// - `v` - The vertex to check. - /// - `t_start` - The inclusive start time of the window. - /// - `t_end` - The exclusive end time of the window. - /// - /// # Returns - /// - /// A result containing `true` if the vertex exists in the window, `false` otherwise. - /// - /// # Errors - /// - /// Returns an error if `v` is not a valid vertex. - fn has_vertex_ref_window(&self, v: VertexRef, t_start: i64, t_end: i64) -> bool { - self.graph - .has_vertex_ref_window(v, self.actual_start(t_start), self.actual_end(t_end)) - } - - /// Returns the number of edges from a vertex in the window. - /// - /// # Arguments - /// - /// - `v` - The vertex to check. - /// - `d` - The direction of the edges to count. - /// - /// # Returns - /// - /// A result containing the number of edges from the vertex in the window. - /// - /// # Errors - /// - /// Returns an error if `v` is not a valid vertex. - fn degree(&self, v: LocalVertexRef, d: Direction, layer: Option) -> usize { - self.graph - .degree_window(v, self.t_start, self.t_end, d, layer) - } - - /// Returns the number of edges from a vertex in the window defined by t_start and t_end. - /// - /// # Arguments - /// - /// - `v` - The vertex to check. - /// - `t_start` - The inclusive start time of the window. - /// - `t_end` - The exclusive end time of the window. - /// - `d` - The direction of the edges to count. - /// - /// # Returns - /// - /// A result containing the number of edges from the vertex in the window. - /// - /// # Errors - /// - /// Returns an error if `v` is not a valid vertex. - fn degree_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - d: Direction, - layer: Option, - ) -> usize { - self.graph.degree_window( - v, - self.actual_start(t_start), - self.actual_end(t_end), - d, - layer, - ) - } - - /// Get the reference of the vertex with ID v if it exists - /// - /// # Arguments - /// - /// - `v` - The ID of the vertex to get - /// - /// # Returns - /// - /// A result of an option containing the vertex reference if it exists, `None` otherwise. - /// - /// # Errors - /// - /// Returns an error if `v` is not a valid vertex. - fn vertex_ref(&self, v: u64) -> Option { - self.graph.vertex_ref_window(v, self.t_start, self.t_end) - } - - /// Get the reference of the vertex with ID v if it exists in a window - /// - /// # Arguments - /// - /// - `v` - The ID of the vertex to get - /// - `t_start` - The inclusive start time of the window. - /// - `t_end` - The exclusive end time of the window. - /// - /// # Returns - /// - /// A result of an option containing the vertex reference if it exists, `None` otherwise. - /// - /// # Errors - /// - /// Returns an error if `v` is not a valid vertex. - fn vertex_ref_window(&self, v: u64, t_start: i64, t_end: i64) -> Option { - self.graph - .vertex_ref_window(v, self.actual_start(t_start), self.actual_end(t_end)) - } - - fn vertex_earliest_time(&self, v: LocalVertexRef) -> Option { - self.graph - .vertex_earliest_time_window(v, self.t_start, self.t_end) - } - - fn vertex_earliest_time_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - ) -> Option { - self.graph.vertex_earliest_time_window( - v, - self.actual_start(t_start), - self.actual_end(t_end), - ) - } - - fn vertex_latest_time(&self, v: LocalVertexRef) -> Option { - self.graph - .vertex_latest_time_window(v, self.t_start, self.t_end) - } - - fn vertex_latest_time_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - ) -> Option { - self.graph - .vertex_latest_time_window(v, self.actual_start(t_start), self.actual_end(t_end)) - } - - /// Get an iterator over the references of all vertices as references - /// - /// # Returns - /// - /// An iterator over the references of all vertices - fn vertex_refs(&self) -> Box + Send> { - self.graph.vertex_refs_window(self.t_start, self.t_end) - } - - fn vertex_refs_window( - &self, - t_start: i64, - t_end: i64, - ) -> Box + Send> { - self.graph - .vertex_refs_window(self.actual_start(t_start), self.actual_end(t_end)) - } - - fn vertex_refs_shard(&self, shard: usize) -> Box + Send> { - self.graph - .vertex_refs_window_shard(shard, self.t_start, self.t_end) - } - - fn vertex_refs_window_shard( - &self, - shard: usize, - t_start: i64, - t_end: i64, - ) -> Box + Send> { - self.graph.vertex_refs_window_shard( - shard, - self.actual_start(t_start), - self.actual_end(t_end), - ) - } - - /// Get an iterator over the references of an edges as a reference - /// - /// # Arguments - /// - /// - `src` - The source vertex of the edge - /// - `dst` - The destination vertex of the edge - /// - /// # Returns - /// - /// A result of an option containing the edge reference if it exists, `None` otherwise. - /// - /// # Errors - /// - /// Returns an error if `src` or `dst` are not valid vertices. - fn edge_ref(&self, src: VertexRef, dst: VertexRef, layer: usize) -> Option { - self.graph - .edge_ref_window(src, dst, self.t_start, self.t_end, layer) - } - - /// Get an iterator over the references of an edges as a reference in a window - /// - /// # Arguments - /// - /// - `src` - The source vertex of the edge - /// - `dst` - The destination vertex of the edge - /// - `t_start` - The inclusive start time of the window. - /// - `t_end` - The exclusive end time of the window. - /// - /// # Returns - /// - /// A result of an option containing the edge reference if it exists, `None` otherwise. - /// - /// # Errors - /// - /// Returns an error if `src` or `dst` are not valid vertices. - fn edge_ref_window( - &self, - src: VertexRef, - dst: VertexRef, - t_start: i64, - t_end: i64, - layer: usize, - ) -> Option { - self.graph.edge_ref_window( - src, - dst, - self.actual_start(t_start), - self.actual_end(t_end), - layer, - ) - } - - /// Get an iterator of all edges as references - /// - /// # Returns - /// - /// An iterator over all edges as references - fn edge_refs(&self, layer: Option) -> Box + Send> { - self.graph.edge_refs_window(self.t_start, self.t_end, layer) - } - - /// Get an iterator of all edges as references in a window - /// - /// # Arguments - /// - /// - `t_start` - The inclusive start time of the window. - /// - `t_end` - The exclusive end time of the window. - /// - /// # Returns - /// - /// An iterator over all edges as references - fn edge_refs_window( - &self, - t_start: i64, - t_end: i64, - layer: Option, - ) -> Box + Send> { - self.graph - .edge_refs_window(self.actual_start(t_start), self.actual_end(t_end), layer) - } - - fn vertex_edges_t( - &self, - v: LocalVertexRef, - d: Direction, - layer: Option, - ) -> Box + Send> { - self.graph - .vertex_edges_window_t(v, self.t_start, self.t_end, d, layer) - } - - /// Get an iterator of all edges as references for a given vertex and direction in a window - /// - /// # Arguments - /// - /// - `v` - The vertex to get the edges for - /// - `t_start` - The inclusive start time of the window. - /// - `t_end` - The exclusive end time of the window. - /// - `d` - The direction of the edges - /// - /// # Returns - /// - /// An iterator over all edges in that vertex direction as references - fn vertex_edges_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - d: Direction, - layer: Option, - ) -> Box + Send> { - self.graph.vertex_edges_window( - v, - self.actual_start(t_start), - self.actual_end(t_end), - d, - layer, - ) - } - - /// Get an iterator of all edges as references for a given vertex and direction in a window - /// but exploded. This means, if a timestamp has two edges, they will be returned as two - /// seperate edges. - /// - /// # Arguments - /// - /// - `v` - The vertex to get the edges for - /// - `t_start` - The inclusive start time of the window. - /// - `t_end` - The exclusive end time of the window. - /// - `d` - The direction of the edges - /// - /// # Returns - /// - /// An iterator over all edges in that vertex direction as references - - fn vertex_edges_window_t( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - d: Direction, - layer: Option, - ) -> Box + Send> { - self.graph.vertex_edges_window_t( - v, - self.actual_start(t_start), - self.actual_end(t_end), - d, - layer, - ) - } - - /// Get the neighbours of a vertex as references in a given direction - /// - /// # Arguments - /// - /// - `v` - The vertex to get the neighbours for - /// - `d` - The direction of the edges - /// - /// # Returns - /// - /// An iterator over all neighbours in that vertex direction as references - fn neighbours( - &self, - v: LocalVertexRef, - d: Direction, - layer: Option, - ) -> Box + Send> { - self.graph - .neighbours_window(v, self.t_start, self.t_end, d, layer) - } - - /// Get the neighbours of a vertex as references in a given direction across a window - /// - /// # Arguments - /// - /// - `v` - The vertex to get the neighbours for - /// - `t_start` - The inclusive start time of the window. - /// - `t_end` - The exclusive end time of the window. - /// - `d` - The direction of the edges - /// - /// # Returns - /// - /// An iterator over all neighbours in that vertex direction as references - fn neighbours_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - d: Direction, - layer: Option, - ) -> Box + Send> { - self.graph.neighbours_window( - v, - self.actual_start(t_start), - self.actual_end(t_end), - d, - layer, - ) - } - - /// Get the static property of a vertex - /// - /// # Arguments - /// - /// - `v` - The vertex to get the property for - /// - `name` - The name of the property - /// - /// # Returns - /// - /// A result of an option of a property - fn static_vertex_prop(&self, v: LocalVertexRef, name: String) -> Option { - self.graph.static_vertex_prop(v, name) - } - - /// Get all static property names of a vertex - /// - /// # Arguments - /// - /// - `v` - The vertex to get the property for - /// - /// # Returns - /// - /// a Vector of Strings representing all the property names - fn static_vertex_prop_names(&self, v: LocalVertexRef) -> Vec { - self.graph.static_vertex_prop_names(v) - } - - /// Get all temporal property names of a vertex - /// - /// # Arguments - /// - /// - `v` - The vertex to get the property for - /// - /// # Returns - /// - /// a Vector of Strings representing all the property names - fn temporal_vertex_prop_names(&self, v: LocalVertexRef) -> Vec { - self.graph.temporal_vertex_prop_names(v) - } - - /// Get the temporal property of a vertex - /// - /// # Arguments - /// - /// - `v` - The vertex to get the property for - /// - `name` - The name of the property - /// - /// # Returns - /// - /// A result of an vector of a tuple of a timestamp and a property - fn temporal_vertex_prop_vec(&self, v: LocalVertexRef, name: String) -> Vec<(i64, Prop)> { - self.graph - .temporal_vertex_prop_vec_window(v, name, self.t_start, self.t_end) - } - - /// Get the temporal property of a vertex in a window - /// - /// # Arguments - /// - /// - `v` - The vertex to get the property for - /// - `name` - The name of the property - /// - `t_start` - The inclusive start time of the window. - /// - `t_end` - The exclusive end time of the window. - /// - /// # Returns - /// - /// A result of an vector of a tuple of a timestamp and a property - /// - /// # Errors - /// - /// - `GraphError` - Raised if vertex or property does not exist - fn temporal_vertex_prop_vec_window( - &self, - v: LocalVertexRef, - name: String, - t_start: i64, - t_end: i64, - ) -> Vec<(i64, Prop)> { - self.graph.temporal_vertex_prop_vec_window( - v, - name, - self.actual_start(t_start), - self.actual_end(t_end), - ) - } - - /// Get the timestamps of a vertex - /// - /// # Arguments - /// - /// - `v` - The vertex to get the timestamps for - /// - /// # Returns - /// - /// A result of a vector of timestamps - /// - /// # Errors - /// - /// - `GraphError` - Raised if vertex does not exist - fn vertex_timestamps(&self, v: LocalVertexRef) -> Vec { - self.graph - .vertex_timestamps_window(v, self.t_start, self.t_end) - } - - /// Get the timestamps of a vertex in a window - /// - /// # Arguments - /// - /// - `v` - The vertex to get the timestamps for - /// - `t_start` - The start of the window - /// - `t_end` - The end of the window - /// - /// # Returns - /// - /// A result of a vector of timestamps - /// - /// # Errors - /// - /// - `GraphError` - Raised if vertex does not exist - - fn vertex_timestamps_window(&self, v: LocalVertexRef, t_start: i64, t_end: i64) -> Vec { - self.graph - .vertex_timestamps_window(v, self.actual_start(t_start), self.actual_end(t_end)) - } - - /// Get the timestamps of an edge in a window - /// - /// # Arguments - /// - /// - `e` - The edge to get the timestamps for - /// - `window` - The window to get the timestamps for - /// - /// # Returns - /// - /// A result of a vector of timestamps - /// - /// # Errors - /// - /// - `GraphError` - Raised if edge does not exist - - fn edge_timestamps(&self, e: EdgeRef, window: Option>) -> Vec { - let window = match window { - Some(Range { start, end, .. }) => self.actual_start(start)..self.actual_end(end), - None => self.t_start..self.t_end, - }; - self.graph.edge_timestamps(e, Some(window)) - } - - /// Get all temporal properties of a vertex - /// - /// # Arguments - /// - /// - `v` - The vertex to get the property for - /// - /// # Returns - /// - /// A result of an vector of a tuple of a timestamp and a property - /// - /// # Errors - /// - /// - `GraphError` - Raised if vertex or property does not exist - fn temporal_vertex_props(&self, v: LocalVertexRef) -> HashMap> { - self.graph - .temporal_vertex_props_window(v, self.t_start, self.t_end) - } - - /// Get all temporal properties of a vertex in a window - /// - /// # Arguments - /// - /// - `v` - The vertex to get the property for - /// - `t_start` - The inclusive start time of the window. - /// - `t_end` - The exclusive end time of the window. - /// - /// # Returns - /// - /// A result of an hashmap of a tuple of a string being names and - /// vectors of timestamp and the property value - /// - /// # Errors - /// - /// - `GraphError` - Raised if vertex or property does not exist - fn temporal_vertex_props_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - ) -> HashMap> { - self.graph.temporal_vertex_props_window( - v, - self.actual_start(t_start), - self.actual_end(t_end), - ) - } - - /// Get the static property of an edge - /// - /// # Arguments - /// - /// - `e` - The edge to get the property for - /// - `name` - The name of the property - /// - /// # Returns - /// - /// A result of an option of a property or a graph error - /// - /// # Errors - /// - /// - `GraphError` - Raised if edge or property does not exist - fn static_edge_prop(&self, e: EdgeRef, name: String) -> Option { - self.graph.static_edge_prop(e, name) - } - - /// Get the names of all static properties of an edge - /// - /// # Arguments - /// - /// - `e` - The edge to get the property for - /// - /// # Returns - /// - /// A result of an vector of all property names - fn static_edge_prop_names(&self, e: EdgeRef) -> Vec { - self.graph.static_edge_prop_names(e) - } - - /// Get the names of all temporal properties of an edge - /// - /// # Arguments - /// - /// - `e` - The edge to get the property for - /// - /// # Returns - /// - /// A result of an vector of all property names - fn temporal_edge_prop_names(&self, e: EdgeRef) -> Vec { - self.graph.temporal_edge_prop_names(e) - } - - /// Get the temporal property of an edge - /// - /// # Arguments - /// - /// - `e` - The edge to get the property for - /// - `name` - The name of the property - /// - /// # Returns - /// - /// A result of an option of a property or a graph error - /// - /// # Errors - /// - /// - `GraphError` - Raised if edge or property does not exist - fn temporal_edge_props_vec(&self, e: EdgeRef, name: String) -> Vec<(i64, Prop)> { - self.graph - .temporal_edge_props_vec_window(e, name, self.t_start, self.t_end) - } - - /// Get the temporal property of an edge in a window - /// - /// # Arguments - /// - /// - `e` - The edge to get the property for - /// - `name` - The name of the property - /// - `t_start` - The inclusive start time of the window. - /// - `t_end` - The exclusive end time of the window. - /// - /// # Returns - /// - /// A result of an vector of a timestamp and property or a graph error - /// - /// # Errors - /// - /// - `GraphError` - Returned if edge or property does not exist - fn temporal_edge_props_vec_window( - &self, - e: EdgeRef, - name: String, - t_start: i64, - t_end: i64, - ) -> Vec<(i64, Prop)> { - self.graph.temporal_edge_props_vec_window( - e, - name, - self.actual_start(t_start), - self.actual_end(t_end), - ) - } - - /// Get all temporal properties of an edge - /// - /// # Arguments - /// - /// - `e` - The edge to get the property for - /// - /// # Returns - /// - /// A hashmap containing the name of a property as a key - /// and the vector of a timestamp and property value - fn temporal_edge_props(&self, e: EdgeRef) -> HashMap> { - self.graph - .temporal_edge_props_window(e, self.t_start, self.t_end) - } - - /// Get all temporal properties of an edge in a window - /// - /// # Arguments - /// - /// - `e` - The edge to get the property for - /// - `t_start` - The inclusive start time of the window. - /// - `t_end` - The exclusive end time of the window. - /// - /// # Returns - /// - /// A hashmap containing the name of a property as a key - /// and the vector of a timestamp and property value - fn temporal_edge_props_window( - &self, - e: EdgeRef, - t_start: i64, - t_end: i64, - ) -> HashMap> { - self.graph - .temporal_edge_props_window(e, self.actual_start(t_start), self.actual_end(t_end)) - } - - fn num_shards(&self) -> usize { - self.graph.num_shards() - } - - fn vertex_edges( - &self, - v: LocalVertexRef, - d: Direction, - layer: Option, - ) -> Box + Send> { - self.graph - .vertex_edges_window(v, self.t_start, self.t_end, d, layer) - } - - fn vertex_id(&self, v: LocalVertexRef) -> u64 { - self.graph.vertex_id(v) - } - - fn local_vertex(&self, v: VertexRef) -> Option { - self.graph.local_vertex_window(v, self.t_start, self.t_end) - } - - fn local_vertex_window( - &self, - v: VertexRef, - t_start: i64, - t_end: i64, - ) -> Option { - self.graph - .local_vertex_window(v, self.actual_start(t_start), self.actual_end(t_end)) - } -} - -/// A windowed graph is a graph that only allows access to vertices and edges within a time window. -/// -/// This struct is used to represent a graph with a time window. It is constructed -/// by providing a `Graph` object and a time range that defines the window. -/// -/// # Examples -/// -/// ```rust -/// use raphtory::db::graph::Graph; -/// use raphtory::db::view_api::*; -/// -/// let graph = Graph::new(1); -/// graph.add_edge(0, 1, 2, &vec![], None); -/// graph.add_edge(1, 2, 3, &vec![], None); -/// let windowed_graph = graph.window(0, 1); -/// ``` -impl WindowedGraph { - /// Create a new windowed graph - /// - /// # Arguments - /// - /// - `graph` - The graph to create the windowed graph from - /// - `t_start` - The inclusive start time of the window. - /// - `t_end` - The exclusive end time of the window. - /// - /// # Returns - /// - /// A new windowed graph - pub fn new(graph: G, t_start: T, t_end: T) -> Self { - WindowedGraph { - graph, - t_start: t_start.into_time(), - t_end: t_end.into_time(), - } - } - - /// the larger of `t_start` and `self.start()` (useful for creating nested windows) - fn actual_start(&self, t_start: i64) -> i64 { - max(t_start, self.t_start) - } - - /// the smaller of `t_end` and `self.end()` (useful for creating nested windows) - fn actual_end(&self, t_end: i64) -> i64 { - min(t_end, self.t_end) - } -} - -#[cfg(test)] -mod views_test { - - use super::*; - use crate::core::Prop; - use crate::db::graph::Graph; - use crate::db::view_api::edge::EdgeViewOps; - use crate::db::view_api::*; - use itertools::Itertools; - use quickcheck::TestResult; - use rand::prelude::*; - use rayon::prelude::*; - - #[test] - fn windowed_graph_vertices_degree() { - let vs = vec![ - (1, 1, 2), - (2, 1, 3), - (-1, 2, 1), - (0, 1, 1), - (7, 3, 2), - (1, 1, 1), - ]; - - let g = Graph::new(2); - - for (t, src, dst) in &vs { - g.add_edge(*t, *src, *dst, &vec![], None).unwrap(); - } - - let wg = WindowedGraph::new(g, -1, 1); - - let actual = wg - .vertices() - .iter() - .map(|v| (v.id(), v.degree())) - .collect::>(); - - let expected = vec![(2, 1), (1, 2)]; - - assert_eq!(actual, expected); - } - - #[test] - fn windowed_graph_edge() { - let vs = vec![ - (1, 1, 2), - (2, 1, 3), - (-1, 2, 1), - (0, 1, 1), - (7, 3, 2), - (1, 1, 1), - ]; - - let g = Graph::new(2); - - for (t, src, dst) in vs { - g.add_edge(t, src, dst, &vec![], None).unwrap(); - } - - let wg = g.window(i64::MIN, i64::MAX); - assert_eq!(wg.edge(1, 3, None).unwrap().src().id(), 1); - assert_eq!(wg.edge(1, 3, None).unwrap().dst().id(), 3); - } - - #[test] - fn windowed_graph_vertex_edges() { - let vs = vec![ - (1, 1, 2), - (2, 1, 3), - (-1, 2, 1), - (0, 1, 1), - (7, 3, 2), - (1, 1, 1), - ]; - - let g = Graph::new(2); - - for (t, src, dst) in &vs { - g.add_edge(*t, *src, *dst, &vec![], None).unwrap(); - } - - let wg = WindowedGraph::new(g, -1, 1); - - assert_eq!(wg.vertex(1).unwrap().id(), 1); - } - - #[test] - fn graph_has_vertex_check_fail() { - let vs: Vec<(i64, u64)> = vec![ - (1, 0), - (-100, 262), - // (327226439, 108748364996394682), - (1, 9135428456135679950), - // (0, 1), - // (2, 2), - ]; - let g = Graph::new(2); - - for (t, v) in &vs { - g.add_vertex(*t, *v, &vec![]) - .map_err(|err| println!("{:?}", err)) - .ok(); - } - - let wg = WindowedGraph::new(g, 1, 2); - assert!(!wg.has_vertex(262)) - } - - #[quickcheck] - fn windowed_graph_has_vertex(mut vs: Vec<(i64, u64)>) -> TestResult { - if vs.is_empty() { - return TestResult::discard(); - } - - vs.sort_by_key(|v| v.1); // Sorted by vertex - vs.dedup_by_key(|v| v.1); // Have each vertex only once to avoid headaches - vs.sort_by_key(|v| v.0); // Sorted by time - - let rand_start_index = thread_rng().gen_range(0..vs.len()); - let rand_end_index = thread_rng().gen_range(rand_start_index..vs.len()); - - let g = Graph::new(2); - - for (t, v) in &vs { - g.add_vertex(*t, *v, &vec![]) - .map_err(|err| println!("{:?}", err)) - .ok(); - } - - let start = vs.get(rand_start_index).expect("start index in range").0; - let end = vs.get(rand_end_index).expect("end index in range").0; - - let wg = WindowedGraph::new(g, start, end); - - let rand_test_index: usize = thread_rng().gen_range(0..vs.len()); - - let (i, v) = vs.get(rand_test_index).expect("test index in range"); - if (start..end).contains(i) { - if wg.has_vertex(*v) { - TestResult::passed() - } else { - TestResult::error(format!( - "Vertex {:?} was not in window {:?}", - (i, v), - start..end - )) - } - } else if !wg.has_vertex(*v) { - TestResult::passed() - } else { - TestResult::error(format!( - "Vertex {:?} was in window {:?}", - (i, v), - start..end - )) - } - } - - #[quickcheck] - fn windowed_graph_has_edge(mut edges: Vec<(i64, (u64, u64))>) -> TestResult { - if edges.is_empty() { - return TestResult::discard(); - } - - edges.sort_by_key(|e| e.1); // Sorted by edge - edges.dedup_by_key(|e| e.1); // Have each edge only once to avoid headaches - edges.sort_by_key(|e| e.0); // Sorted by time - - let rand_start_index = thread_rng().gen_range(0..edges.len()); - let rand_end_index = thread_rng().gen_range(rand_start_index..edges.len()); - - let g = Graph::new(2); - - for (t, e) in &edges { - g.add_edge(*t, e.0, e.1, &vec![], None).unwrap(); - } - - let start = edges.get(rand_start_index).expect("start index in range").0; - let end = edges.get(rand_end_index).expect("end index in range").0; - - let wg = WindowedGraph::new(g, start, end); - - let rand_test_index: usize = thread_rng().gen_range(0..edges.len()); - - let (i, e) = edges.get(rand_test_index).expect("test index in range"); - if (start..end).contains(i) { - if wg.has_edge(e.0, e.1, None) { - TestResult::passed() - } else { - TestResult::error(format!( - "Edge {:?} was not in window {:?}", - (i, e), - start..end - )) - } - } else if !wg.has_edge(e.0, e.1, None) { - TestResult::passed() - } else { - TestResult::error(format!("Edge {:?} was in window {:?}", (i, e), start..end)) - } - } - - #[quickcheck] - fn windowed_graph_edge_count(mut edges: Vec<(i64, (u64, u64))>) -> TestResult { - edges.sort_by_key(|e| e.1); // Sorted by edge - edges.dedup_by_key(|e| e.1); // Have each edge only once to avoid headaches - - let mut window: [i64; 2] = thread_rng().gen(); - window.sort(); - let window = window[0]..window[1]; - let true_edge_count = edges.iter().filter(|e| window.contains(&e.0)).count(); - - let g = Graph::new(2); - - for (t, e) in &edges { - g.add_edge( - *t, - e.0, - e.1, - &vec![("test".to_owned(), Prop::Bool(true))], - None, - ) - .unwrap(); - } - - let wg = WindowedGraph::new(g, window.start, window.end); - if wg.num_edges() != true_edge_count { - println!( - "failed, g.num_edges() = {}, true count = {}", - wg.num_edges(), - true_edge_count - ); - println!("g.edges() = {:?}", wg.edges().collect_vec()); - } - TestResult::from_bool(wg.num_edges() == true_edge_count) - } - - #[quickcheck] - fn trivial_window_has_all_edges(edges: Vec<(i64, u64, u64)>) -> bool { - let g = Graph::new(10); - edges - .into_par_iter() - .filter(|e| e.0 < i64::MAX) - .for_each(|(t, src, dst)| { - g.add_edge( - t, - src, - dst, - &vec![("test".to_owned(), Prop::Bool(true))], - None, - ) - .unwrap() - }); - let w = g.window(i64::MIN, i64::MAX); - g.edges() - .all(|e| w.has_edge(e.src().id(), e.dst().id(), None)) - } - - #[quickcheck] - fn large_vertex_in_window(dsts: Vec) -> bool { - let dsts: Vec = dsts.into_iter().unique().collect(); - let n = dsts.len(); - let g = Graph::new(1); - - for dst in dsts { - let t = 1; - g.add_edge(t, 0, dst, &vec![], None).unwrap(); - } - let w = g.window(i64::MIN, i64::MAX); - w.num_edges() == n - } - - #[test] - fn windowed_graph_vertex_ids() { - let vs = vec![(1, 1, 2), (3, 3, 4), (5, 5, 6), (7, 7, 1)]; - - let args = vec![(i64::MIN, 8), (i64::MIN, 2), (i64::MIN, 4), (3, 6)]; - - let expected = vec![ - vec![1, 2, 3, 4, 5, 6, 7], - vec![1, 2], - vec![1, 2, 3, 4], - vec![3, 4, 5, 6], - ]; - - let g = Graph::new(1); - - for (t, src, dst) in &vs { - g.add_edge(*t, *src, *dst, &vec![], None).unwrap(); - } - - let res: Vec<_> = (0..=3) - .map(|i| { - let wg = g.window(args[i].0, args[i].1); - let mut e = wg.vertices().id().collect::>(); - e.sort(); - e - }) - .collect_vec(); - - assert_eq!(res, expected); - - let g = Graph::new(3); - for (src, dst, t) in &vs { - g.add_edge(*src, *dst, *t, &vec![], None).unwrap(); - } - let res: Vec<_> = (0..=3) - .map(|i| { - let wg = g.window(args[i].0, args[i].1); - let mut e = wg.vertices().id().collect::>(); - e.sort(); - e - }) - .collect_vec(); - assert_eq!(res, expected); - } - - #[test] - fn windowed_graph_vertices() { - let vs = vec![ - (1, 1, 2), - (2, 1, 3), - (-1, 2, 1), - (0, 1, 1), - (7, 3, 2), - (1, 1, 1), - ]; - - let g = Graph::new(1); - - g.add_vertex( - 0, - 1, - &vec![ - ("type".into(), Prop::Str("wallet".into())), - ("cost".into(), Prop::F32(99.5)), - ], - ) - .map_err(|err| println!("{:?}", err)) - .ok(); - - g.add_vertex( - -1, - 2, - &vec![ - ("type".into(), Prop::Str("wallet".into())), - ("cost".into(), Prop::F32(10.0)), - ], - ) - .map_err(|err| println!("{:?}", err)) - .ok(); - - g.add_vertex( - 6, - 3, - &vec![ - ("type".into(), Prop::Str("wallet".into())), - ("cost".into(), Prop::F32(76.2)), - ], - ) - .map_err(|err| println!("{:?}", err)) - .ok(); - - for (t, src, dst) in &vs { - g.add_edge( - *t, - *src, - *dst, - &vec![("eprop".into(), Prop::Str("commons".into()))], - None, - ) - .unwrap(); - } - - let wg = g.window(-2, 0); - - let actual = wg.vertices().id().collect::>(); - - let expected = vec![1, 2]; - - assert_eq!(actual, expected); - - // Check results from multiple graphs with different number of shards - let g = Graph::new(10); - - g.add_vertex( - 0, - 1, - &vec![ - ("type".into(), Prop::Str("wallet".into())), - ("cost".into(), Prop::F32(99.5)), - ], - ) - .map_err(|err| println!("{:?}", err)) - .ok(); - - g.add_vertex( - -1, - 2, - &vec![ - ("type".into(), Prop::Str("wallet".into())), - ("cost".into(), Prop::F32(10.0)), - ], - ) - .map_err(|err| println!("{:?}", err)) - .ok(); - - g.add_vertex( - 6, - 3, - &vec![ - ("type".into(), Prop::Str("wallet".into())), - ("cost".into(), Prop::F32(76.2)), - ], - ) - .map_err(|err| println!("{:?}", err)) - .ok(); - - for (t, src, dst) in &vs { - g.add_edge(*t, *src, *dst, &vec![], None).unwrap(); - } - - let expected = wg.vertices().id().collect::>(); - - assert_eq!(actual, expected); - } -} diff --git a/raphtory/src/db/internal/addition.rs b/raphtory/src/db/internal/addition.rs new file mode 100644 index 0000000000..fe117a1f57 --- /dev/null +++ b/raphtory/src/db/internal/addition.rs @@ -0,0 +1,89 @@ +use crate::{ + core::{ + entities::{graph::tgraph::InnerTemporalGraph, EID, VID}, + storage::timeindex::TimeIndexEntry, + utils::errors::GraphError, + PropType, + }, + db::api::mutation::internal::InternalAdditionOps, + prelude::Prop, +}; +use std::sync::atomic::Ordering; + +impl InternalAdditionOps for InnerTemporalGraph { + #[inline] + fn next_event_id(&self) -> usize { + self.inner().event_counter.fetch_add(1, Ordering::Relaxed) + } + + #[inline] + fn resolve_layer(&self, layer: Option<&str>) -> usize { + layer + .map(|name| self.inner().edge_meta.get_or_create_layer_id(name)) + .unwrap_or(0) + } + + #[inline] + fn resolve_vertex(&self, id: u64, name: Option<&str>) -> VID { + self.inner().resolve_vertex(id, name) + } + + #[inline] + fn resolve_graph_property(&self, prop: &str, is_static: bool) -> usize { + self.inner().graph_props.resolve_property(prop, is_static) + } + + #[inline] + fn resolve_vertex_property( + &self, + prop: &str, + dtype: PropType, + is_static: bool, + ) -> Result { + self.inner() + .vertex_meta + .resolve_prop_id(prop, dtype, is_static) + } + + #[inline] + fn resolve_edge_property( + &self, + prop: &str, + dtype: PropType, + is_static: bool, + ) -> Result { + self.inner() + .edge_meta + .resolve_prop_id(prop, dtype, is_static) + } + + #[inline] + fn process_prop_value(&self, prop: Prop) -> Prop { + match prop { + Prop::Str(value) => Prop::Str(self.inner().resolve_str(value)), + _ => prop, + } + } + + #[inline] + fn internal_add_vertex( + &self, + t: TimeIndexEntry, + v: VID, + props: Vec<(usize, Prop)>, + ) -> Result<(), GraphError> { + self.inner().add_vertex_internal(t, v, props) + } + + #[inline] + fn internal_add_edge( + &self, + t: TimeIndexEntry, + src: VID, + dst: VID, + props: Vec<(usize, Prop)>, + layer: usize, + ) -> Result { + self.inner().add_edge_internal(t, src, dst, props, layer) + } +} diff --git a/raphtory/src/db/internal/core_ops.rs b/raphtory/src/db/internal/core_ops.rs new file mode 100644 index 0000000000..aa8ed11d7c --- /dev/null +++ b/raphtory/src/db/internal/core_ops.rs @@ -0,0 +1,332 @@ +use crate::{ + core::{ + entities::{ + edges::{edge_ref::EdgeRef, edge_store::EdgeStore}, + graph::tgraph::InnerTemporalGraph, + properties::{ + graph_props::GraphProps, + props::Meta, + tprop::{LockedLayeredTProp, TProp}, + }, + vertices::{vertex_ref::VertexRef, vertex_store::VertexStore}, + LayerIds, EID, VID, + }, + storage::{ + locked_view::LockedView, + timeindex::{LockedLayeredIndex, TimeIndex, TimeIndexEntry}, + ArcEntry, + }, + ArcStr, + }, + db::api::view::{internal::CoreGraphOps, BoxedIter}, + prelude::Prop, +}; +use itertools::Itertools; +use std::{collections::HashMap, iter}; + +impl CoreGraphOps for InnerTemporalGraph { + #[inline] + fn unfiltered_num_vertices(&self) -> usize { + self.inner().internal_num_vertices() + } + + #[inline] + fn vertex_meta(&self) -> &Meta { + &self.inner().vertex_meta + } + + #[inline] + fn edge_meta(&self) -> &Meta { + &self.inner().edge_meta + } + + #[inline] + fn graph_meta(&self) -> &GraphProps { + &self.inner().graph_props + } + + #[inline] + fn get_layer_name(&self, layer_id: usize) -> ArcStr { + self.inner().edge_meta.layer_meta().get_name(layer_id) + } + + #[inline] + fn get_layer_id(&self, name: &str) -> Option { + self.inner().edge_meta.get_layer_id(name) + } + + #[inline] + fn get_layer_names_from_ids(&self, layer_ids: LayerIds) -> BoxedIter { + self.inner().layer_names(layer_ids) + } + + #[inline] + fn vertex_id(&self, v: VID) -> u64 { + self.inner().global_vertex_id(v) + } + + #[inline] + fn vertex_name(&self, v: VID) -> String { + self.inner().vertex_name(v) + } + + #[inline] + fn edge_additions( + &self, + eref: EdgeRef, + layer_ids: LayerIds, + ) -> LockedLayeredIndex<'_, TimeIndexEntry> { + let layer_ids = layer_ids.constrain_from_edge(eref); + let edge = self.inner().edge(eref.pid()); + edge.additions(layer_ids).unwrap() + } + + #[inline] + fn vertex_additions(&self, v: VID) -> LockedView> { + let vertex = self.inner().vertex(v); + vertex.additions().unwrap() + } + + #[inline] + fn internalise_vertex(&self, v: VertexRef) -> Option { + self.inner().resolve_vertex_ref(v) + } + + #[inline] + fn internalise_vertex_unchecked(&self, v: VertexRef) -> VID { + match v { + VertexRef::Internal(l) => l, + VertexRef::External(_) => self.inner().resolve_vertex_ref(v).unwrap(), + } + } + + #[inline] + fn constant_prop(&self, id: usize) -> Option { + self.inner().get_constant_prop(id) + } + + #[inline] + fn temporal_prop(&self, id: usize) -> Option> { + self.inner().get_temporal_prop(id) + } + + #[inline] + fn constant_vertex_prop(&self, v: VID, prop_id: usize) -> Option { + let entry = self.inner().node_entry(v); + entry.const_prop(prop_id).cloned() + } + + #[inline] + fn constant_vertex_prop_ids(&self, v: VID) -> Box + '_> { + // FIXME: revisit the locking scheme so we don't have to collect the ids + Box::new( + self.inner() + .node_entry(v) + .const_prop_ids() + .collect_vec() + .into_iter(), + ) + } + + #[inline] + fn temporal_vertex_prop(&self, v: VID, prop_id: usize) -> Option> { + let vertex = self.inner().vertex(v); + vertex.temporal_property(prop_id) + } + + #[inline] + fn temporal_vertex_prop_ids(&self, v: VID) -> Box + '_> { + // FIXME: revisit the locking scheme so we don't have to collect the ids + Box::new( + self.inner() + .node_entry(v) + .temporal_prop_ids() + .collect_vec() + .into_iter(), + ) + } + + fn get_const_edge_prop(&self, e: EdgeRef, prop_id: usize, layer_ids: LayerIds) -> Option { + let layer_ids = layer_ids.constrain_from_edge(e); + let entry = self.inner().edge_entry(e.pid()); + match layer_ids { + LayerIds::None => None, + LayerIds::All => { + if self.inner().num_layers() == 1 { + // iterator has at most 1 element + entry + .layer_iter() + .next() + .and_then(|layer| layer.const_prop(prop_id).cloned()) + } else { + let prop_map: HashMap<_, _> = entry + .layer_iter() + .enumerate() + .flat_map(|(id, layer)| { + layer + .const_prop(prop_id) + .map(|p| (self.inner().get_layer_name(id), p.clone())) + }) + .collect(); + if prop_map.is_empty() { + None + } else { + Some(prop_map.into()) + } + } + } + LayerIds::One(id) => entry.layer(id).and_then(|l| l.const_prop(prop_id).cloned()), + LayerIds::Multiple(ids) => { + let prop_map: HashMap<_, _> = ids + .iter() + .flat_map(|&id| { + entry.layer(id).and_then(|layer| { + layer + .const_prop(prop_id) + .map(|p| (self.inner().get_layer_name(id), p.clone())) + }) + }) + .collect(); + if prop_map.is_empty() { + None + } else { + Some(prop_map.into()) + } + } + } + } + + fn const_edge_prop_ids( + &self, + e: EdgeRef, + layer_ids: LayerIds, + ) -> Box + '_> { + // FIXME: revisit the locking scheme so we don't have to collect all the ids + let layer_ids = layer_ids.constrain_from_edge(e); + let entry = self.inner().edge_entry(e.pid()); + let ids: Vec<_> = match layer_ids { + LayerIds::None => vec![], + LayerIds::All => entry + .layer_iter() + .map(|l| l.const_prop_ids()) + .kmerge() + .dedup() + .collect(), + LayerIds::One(id) => match entry.layer(id) { + Some(l) => l.const_prop_ids().collect(), + None => vec![], + }, + LayerIds::Multiple(ids) => ids + .iter() + .flat_map(|id| entry.layer(*id).map(|l| l.const_prop_ids())) + .kmerge() + .dedup() + .collect(), + }; + Box::new(ids.into_iter()) + } + + #[inline] + fn temporal_edge_prop( + &self, + e: EdgeRef, + prop_id: usize, + layer_ids: LayerIds, + ) -> Option { + let layer_ids = layer_ids.constrain_from_edge(e); + let edge = self.inner().edge(e.pid()); + edge.temporal_property(layer_ids, prop_id) + } + + fn temporal_edge_prop_ids( + &self, + e: EdgeRef, + layer_ids: LayerIds, + ) -> Box + '_> { + // FIXME: revisit the locking scheme so we don't have to collect the ids + let entry = self.inner().edge_entry(e.pid()); + match layer_ids { + LayerIds::None => Box::new(iter::empty()), + LayerIds::All => Box::new(entry.temp_prop_ids(None).collect_vec().into_iter()), + LayerIds::One(id) => Box::new(entry.temp_prop_ids(Some(id)).collect_vec().into_iter()), + LayerIds::Multiple(ids) => Box::new( + ids.iter() + .map(|id| entry.temp_prop_ids(Some(*id))) + .kmerge() + .dedup() + .collect_vec() + .into_iter(), + ), + } + } + + #[inline] + fn core_edges(&self) -> Box>> { + Box::new(self.inner().storage.edges.read_lock().into_iter()) + } + + #[inline] + fn core_edge(&self, eid: EID) -> ArcEntry { + self.inner().storage.edges.entry_arc(eid.into()) + } + + #[inline] + fn core_vertices(&self) -> Box>> { + Box::new(self.inner().storage.nodes.read_lock().into_iter()) + } + + #[inline] + fn core_vertex(&self, vid: VID) -> ArcEntry { + self.inner().storage.nodes.entry_arc(vid.into()) + } +} + +#[cfg(test)] +mod test_edges { + use crate::{ + core::{ArcStr, IntoPropMap}, + prelude::*, + }; + use std::collections::HashMap; + + #[test] + fn test_edge_properties_for_layers() { + let g = Graph::new(); + + g.add_edge(0, 1, 2, [("t", 0)], Some("layer1")) + .unwrap() + .add_constant_properties( + [("layer1", "1".into_prop()), ("layer", 1.into_prop())], + Some("layer1"), + ) + .unwrap(); + g.add_edge(1, 1, 2, [("t", 1)], Some("layer2")) + .unwrap() + .add_constant_properties([("layer", 2)], Some("layer2")) + .unwrap(); + + g.add_edge(2, 1, 2, [("t2", 2)], Some("layer3")) + .unwrap() + .add_constant_properties([("layer", 3)], Some("layer3")) + .unwrap(); + + let e_all = g.edge(1, 2).unwrap(); + assert_eq!( + e_all.properties().constant().as_map(), + HashMap::from([ + ( + ArcStr::from("layer"), + [("layer1", 1), ("layer2", 2), ("layer3", 3)].into_prop_map() + ), + (ArcStr::from("layer1"), [("layer1", "1")].into_prop_map()) + ]) + ); + assert_eq!( + e_all.properties().temporal().get("t").unwrap().values(), + vec![0.into(), 1.into()] + ); + + let e = g.edge(1, 2).unwrap().layer("layer1").unwrap(); + assert!(e.properties().constant().contains("layer1")); + } +} diff --git a/raphtory/src/db/internal/deletion.rs b/raphtory/src/db/internal/deletion.rs new file mode 100644 index 0000000000..c4505fb87f --- /dev/null +++ b/raphtory/src/db/internal/deletion.rs @@ -0,0 +1,31 @@ +use crate::{ + core::{ + entities::{edges::edge_ref::EdgeRef, graph::tgraph::InnerTemporalGraph, LayerIds, VID}, + storage::timeindex::{LockedLayeredIndex, TimeIndexEntry}, + utils::errors::GraphError, + }, + db::api::{mutation::internal::InternalDeletionOps, view::internal::CoreDeletionOps}, +}; + +impl InternalDeletionOps for InnerTemporalGraph { + fn internal_delete_edge( + &self, + t: TimeIndexEntry, + src: VID, + dst: VID, + layer: usize, + ) -> Result<(), GraphError> { + self.inner().delete_edge(t, src, dst, layer) + } +} + +impl CoreDeletionOps for InnerTemporalGraph { + fn edge_deletions( + &self, + eref: EdgeRef, + layer_ids: LayerIds, + ) -> LockedLayeredIndex<'_, TimeIndexEntry> { + let edge = self.inner().edge(eref.pid()); + edge.deletions(layer_ids).unwrap() + } +} diff --git a/raphtory/src/db/internal/edge_filter_ops.rs b/raphtory/src/db/internal/edge_filter_ops.rs new file mode 100644 index 0000000000..1991d3b0e0 --- /dev/null +++ b/raphtory/src/db/internal/edge_filter_ops.rs @@ -0,0 +1,11 @@ +use crate::db::{ + api::view::internal::{EdgeFilter, EdgeFilterOps}, + graph::graph::InternalGraph, +}; + +impl EdgeFilterOps for InternalGraph { + #[inline] + fn edge_filter(&self) -> Option<&EdgeFilter> { + None + } +} diff --git a/raphtory/src/db/internal/graph_ops.rs b/raphtory/src/db/internal/graph_ops.rs new file mode 100644 index 0000000000..ec74cdd453 --- /dev/null +++ b/raphtory/src/db/internal/graph_ops.rs @@ -0,0 +1,249 @@ +use crate::{ + core::{ + entities::{ + edges::edge_ref::{Dir, EdgeRef}, + graph::tgraph::InnerTemporalGraph, + vertices::vertex_ref::VertexRef, + LayerIds, EID, VID, + }, + Direction, + }, + db::api::view::internal::{EdgeFilter, GraphOps}, +}; +use itertools::Itertools; +use std::iter; + +impl GraphOps for InnerTemporalGraph { + fn internal_vertex_ref( + &self, + v: VertexRef, + _layer_ids: &LayerIds, + _filter: Option<&EdgeFilter>, + ) -> Option { + match v { + VertexRef::Internal(l) => Some(l), + VertexRef::External(_) => { + let vid = self.inner().resolve_vertex_ref(v)?; + Some(vid) + } + } + } + + fn find_edge_id( + &self, + e_id: EID, + layer_ids: &LayerIds, + filter: Option<&EdgeFilter>, + ) -> Option { + let e_id_usize: usize = e_id.into(); + if e_id_usize >= self.inner().storage.edges.len() { + return None; + } + let e = self.inner().storage.edges.get(e_id_usize); + filter + .map(|f| f(&e, layer_ids)) + .unwrap_or(true) + .then(|| EdgeRef::new_outgoing(e_id, e.src(), e.dst())) + } + + fn vertices_len(&self, _layer_ids: LayerIds, _filter: Option<&EdgeFilter>) -> usize { + self.inner().internal_num_vertices() + } + + fn edges_len(&self, layers: LayerIds, filter: Option<&EdgeFilter>) -> usize { + self.inner().num_edges(&layers, filter) + } + + #[inline] + fn degree( + &self, + v: VID, + d: Direction, + layers: &LayerIds, + filter: Option<&EdgeFilter>, + ) -> usize { + self.inner().degree(v, d, layers, filter) + } + + fn vertex_refs( + &self, + _layers: LayerIds, + _filter: Option<&EdgeFilter>, + ) -> Box + Send> { + Box::new(self.inner().vertex_ids()) + } + + fn edge_ref( + &self, + src: VID, + dst: VID, + layer: &LayerIds, + filter: Option<&EdgeFilter>, + ) -> Option { + self.inner() + .find_edge(src, dst, layer) + .filter(|eid| { + filter + .map(|f| f(&self.inner().storage.edges.get((*eid).into()), layer)) + .unwrap_or(true) + }) + .map(|e_id| EdgeRef::new_outgoing(e_id, src, dst)) + } + + fn edge_refs( + &self, + layers: LayerIds, + filter: Option<&EdgeFilter>, + ) -> Box + Send> { + let filter = filter.cloned(); + match layers { + LayerIds::None => Box::new(iter::empty()), + LayerIds::All => { + let iter = self + .inner() + .storage + .edges + .read_lock() + .into_iter() + .filter(move |e| filter.as_ref().map(|f| f(e, &layers)).unwrap_or(true)) + .map_into(); + Box::new(iter) + } + _ => Box::new( + self.inner() + .storage + .edges + .read_lock() + .into_iter() + .filter(move |edge| { + filter + .as_ref() + .map(|f| f(edge, &layers)) + .unwrap_or_else(|| edge.has_layer(&layers)) + }) + .map(|edge| edge.into()), + ), + } + } + + fn vertex_edges( + &self, + v: VID, + d: Direction, + layers: LayerIds, + filter: Option<&EdgeFilter>, + ) -> Box + Send> { + let entry = self.inner().storage.nodes.entry_arc(v.into()); + match d { + Direction::OUT => { + let iter: Box + Send> = + match &layers { + LayerIds::None => Box::new(iter::empty()), + LayerIds::All => Box::new( + entry + .into_layers() + .map(move |layer| { + layer + .into_tuples(Dir::Out) + .map(move |(n, e)| EdgeRef::new_outgoing(e, v, n)) + }) + .kmerge() + .dedup(), + ), + LayerIds::One(layer) => { + Box::new(entry.into_layer(*layer).into_iter().flat_map(move |it| { + it.into_tuples(Dir::Out) + .map(move |(n, e)| EdgeRef::new_outgoing(e, v, n)) + })) + } + LayerIds::Multiple(ids) => Box::new( + ids.iter() + .map(move |&layer| { + entry.clone().into_layer(layer).into_iter().flat_map( + move |it| { + it.into_tuples(Dir::Out) + .map(move |(n, e)| EdgeRef::new_outgoing(e, v, n)) + }, + ) + }) + .kmerge() + .dedup(), + ), + }; + match filter.cloned() { + None => iter, + Some(filter) => { + let edge_store = self.inner().storage.edges.read_lock(); + Box::new(iter.filter(move |eref| { + filter(&edge_store.get(eref.pid().into()), &layers) + })) + } + } + } + Direction::IN => { + let iter: Box + Send> = + match &layers { + LayerIds::None => Box::new(iter::empty()), + LayerIds::All => Box::new( + entry + .into_layers() + .map(move |layer| { + layer + .into_tuples(Dir::Into) + .map(move |(n, e)| EdgeRef::new_incoming(e, n, v)) + }) + .kmerge() + .dedup(), + ), + LayerIds::One(layer) => { + Box::new(entry.into_layer(*layer).into_iter().flat_map(move |it| { + it.into_tuples(Dir::Into) + .map(move |(n, e)| EdgeRef::new_incoming(e, n, v)) + })) + } + LayerIds::Multiple(ids) => Box::new( + ids.iter() + .map(move |&layer| { + entry.clone().into_layer(layer).into_iter().flat_map( + move |it| { + it.into_tuples(Dir::Into) + .map(move |(n, e)| EdgeRef::new_incoming(e, n, v)) + }, + ) + }) + .kmerge() + .dedup(), + ), + }; + match filter.cloned() { + None => iter, + Some(filter) => { + let edge_store = self.inner().storage.edges.read_lock(); + Box::new(iter.filter(move |eref| { + filter(&edge_store.get(eref.pid().into()), &layers) + })) + } + } + } + Direction::BOTH => Box::new( + self.vertex_edges(v, Direction::IN, layers.clone(), filter) + .merge(self.vertex_edges(v, Direction::OUT, layers, filter)), + ), + } + } + + fn neighbours( + &self, + v: VID, + d: Direction, + layers: LayerIds, + filter: Option<&EdgeFilter>, + ) -> Box + Send> { + let iter = self.vertex_edges(v, d, layers, filter).map(|e| e.remote()); + if matches!(d, Direction::BOTH) { + Box::new(iter.dedup()) + } else { + Box::new(iter) + } + } +} diff --git a/raphtory/src/db/internal/layer_ops.rs b/raphtory/src/db/internal/layer_ops.rs new file mode 100644 index 0000000000..96b465e256 --- /dev/null +++ b/raphtory/src/db/internal/layer_ops.rs @@ -0,0 +1,19 @@ +use crate::{ + core::entities::{edges::edge_store::EdgeStore, LayerIds}, + db::{api::view::internal::InternalLayerOps, graph::graph::InternalGraph}, + prelude::Layer, +}; + +impl InternalLayerOps for InternalGraph { + fn layer_ids(&self) -> LayerIds { + LayerIds::All + } + + fn layer_ids_from_names(&self, key: Layer) -> LayerIds { + self.inner().layer_id(key) + } + + fn edge_layer_ids(&self, e: &EdgeStore) -> LayerIds { + e.layer_ids() + } +} diff --git a/raphtory/src/db/internal/materialize.rs b/raphtory/src/db/internal/materialize.rs new file mode 100644 index 0000000000..3a1daa780c --- /dev/null +++ b/raphtory/src/db/internal/materialize.rs @@ -0,0 +1,18 @@ +use crate::{ + core::entities::graph::tgraph::InnerTemporalGraph, + db::{ + api::view::internal::{InternalMaterialize, MaterializedGraph}, + graph::graph::{Graph, InternalGraph}, + }, +}; +use std::sync::Arc; + +impl InternalMaterialize for InnerTemporalGraph { + fn new_base_graph(&self, graph: InternalGraph) -> MaterializedGraph { + MaterializedGraph::EventGraph(Graph::new_from_inner(Arc::new(graph))) + } + + fn include_deletions(&self) -> bool { + false + } +} diff --git a/raphtory/src/db/internal/mod.rs b/raphtory/src/db/internal/mod.rs new file mode 100644 index 0000000000..15f84749c6 --- /dev/null +++ b/raphtory/src/db/internal/mod.rs @@ -0,0 +1,11 @@ +pub(crate) mod addition; +pub(crate) mod core_ops; +pub(crate) mod deletion; +pub(crate) mod edge_filter_ops; +pub(crate) mod graph_ops; +pub(crate) mod layer_ops; +pub(crate) mod materialize; +pub(crate) mod prop_add; +pub(crate) mod static_properties; +pub(crate) mod temporal_properties; +pub(crate) mod time_semantics; diff --git a/raphtory/src/db/internal/prop_add.rs b/raphtory/src/db/internal/prop_add.rs new file mode 100644 index 0000000000..74408b4ab2 --- /dev/null +++ b/raphtory/src/db/internal/prop_add.rs @@ -0,0 +1,69 @@ +use crate::{ + core::{ + entities::{graph::tgraph::InnerTemporalGraph, EID, VID}, + storage::timeindex::TimeIndexEntry, + utils::errors::GraphError, + }, + db::api::{mutation::internal::InternalPropertyAdditionOps, view::internal::CoreGraphOps}, + prelude::Prop, +}; + +impl InternalPropertyAdditionOps for InnerTemporalGraph { + fn internal_add_properties( + &self, + t: TimeIndexEntry, + props: Vec<(usize, Prop)>, + ) -> Result<(), GraphError> { + self.inner().add_properties(t, props) + } + + fn internal_add_static_properties(&self, props: Vec<(usize, Prop)>) -> Result<(), GraphError> { + self.inner().add_constant_properties(props) + } + + fn internal_add_constant_vertex_properties( + &self, + vid: VID, + props: Vec<(usize, Prop)>, + ) -> Result<(), GraphError> { + let mut node = self.inner().storage.get_node_mut(vid); + for (prop_id, value) in props { + node.add_constant_prop(prop_id, value).map_err(|err| { + let name = self.vertex_meta().get_prop_name(prop_id, true); + GraphError::ConstantPropertyMutationError { + name, + new: err.new_value.expect("new value exists"), + old: err + .previous_value + .expect("previous value exists if set failed"), + } + })?; + } + Ok(()) + } + + fn internal_add_constant_edge_properties( + &self, + eid: EID, + layer: usize, + props: Vec<(usize, Prop)>, + ) -> Result<(), GraphError> { + let mut edge = self.inner().storage.get_edge_mut(eid); + let mut edge_layer = edge.layer_mut(layer); + for (prop_id, value) in props { + edge_layer + .add_constant_prop(prop_id, value) + .map_err(|err| { + let name = self.edge_meta().get_prop_name(prop_id, true); + GraphError::ConstantPropertyMutationError { + name, + new: err.new_value.expect("new value exists"), + old: err + .previous_value + .expect("previous value exists if set failed"), + } + })?; + } + Ok(()) + } +} diff --git a/raphtory/src/db/internal/static_properties.rs b/raphtory/src/db/internal/static_properties.rs new file mode 100644 index 0000000000..37857a0f4e --- /dev/null +++ b/raphtory/src/db/internal/static_properties.rs @@ -0,0 +1,26 @@ +use crate::{ + core::{entities::graph::tgraph::InnerTemporalGraph, ArcStr, Prop}, + db::api::properties::internal::ConstPropertiesOps, +}; + +impl ConstPropertiesOps for InnerTemporalGraph { + fn get_const_prop_id(&self, name: &str) -> Option { + self.inner().graph_props.get_const_prop_id(name) + } + + fn get_const_prop_name(&self, id: usize) -> ArcStr { + self.inner().graph_props.get_const_prop_name(id) + } + + fn const_prop_ids(&self) -> Box> { + Box::new(self.inner().graph_props.const_prop_ids()) + } + + fn const_prop_keys(&self) -> Box> { + Box::new(self.inner().const_prop_names().into_iter()) + } + + fn get_const_prop(&self, prop_id: usize) -> Option { + self.inner().get_constant_prop(prop_id) + } +} diff --git a/raphtory/src/db/internal/temporal_properties.rs b/raphtory/src/db/internal/temporal_properties.rs new file mode 100644 index 0000000000..2a3e734109 --- /dev/null +++ b/raphtory/src/db/internal/temporal_properties.rs @@ -0,0 +1,50 @@ +use crate::{ + core::{entities::graph::tgraph::InnerTemporalGraph, ArcStr, Prop}, + db::api::properties::internal::{TemporalPropertiesOps, TemporalPropertyViewOps}, +}; + +impl TemporalPropertyViewOps for InnerTemporalGraph { + fn temporal_value(&self, id: usize) -> Option { + self.inner() + .get_temporal_prop(id) + .and_then(|prop| prop.last_before(i64::MAX).map(|(_, v)| v)) + } + + fn temporal_history(&self, id: usize) -> Vec { + self.inner() + .get_temporal_prop(id) + .map(|prop| prop.iter().map(|(t, _)| t).collect()) + .unwrap_or_default() + } + + fn temporal_values(&self, id: usize) -> Vec { + self.inner() + .get_temporal_prop(id) + .map(|prop| prop.iter().map(|(_, v)| v).collect()) + .unwrap_or_default() + } + + fn temporal_value_at(&self, id: usize, t: i64) -> Option { + self.inner() + .get_temporal_prop(id) + .and_then(|prop| prop.last_before(t.saturating_add(1)).map(|(_, v)| v)) + } +} + +impl TemporalPropertiesOps for InnerTemporalGraph { + fn get_temporal_prop_id(&self, name: &str) -> Option { + self.inner().graph_props.get_temporal_id(name) + } + + fn get_temporal_prop_name(&self, id: usize) -> ArcStr { + self.inner().graph_props.get_temporal_name(id) + } + + fn temporal_prop_ids(&self) -> Box + '_> { + Box::new(self.inner().graph_props.temporal_ids()) + } + + fn temporal_prop_keys(&self) -> Box + '_> { + Box::new(self.inner().graph_props.temporal_names().into_iter()) + } +} diff --git a/raphtory/src/db/internal/time_semantics.rs b/raphtory/src/db/internal/time_semantics.rs new file mode 100644 index 0000000000..a4bd71511b --- /dev/null +++ b/raphtory/src/db/internal/time_semantics.rs @@ -0,0 +1,335 @@ +use crate::{ + core::{ + entities::{ + edges::{edge_ref::EdgeRef, edge_store::EdgeStore}, + graph::tgraph::InnerTemporalGraph, + LayerIds, VID, + }, + storage::timeindex::{AsTime, TimeIndexOps}, + }, + db::api::view::{ + internal::{CoreDeletionOps, CoreGraphOps, EdgeFilter, TimeSemantics}, + BoxedIter, + }, + prelude::Prop, +}; +use genawaiter::sync::GenBoxed; +use rayon::prelude::*; +use std::ops::Range; + +impl TimeSemantics for InnerTemporalGraph { + fn vertex_earliest_time(&self, v: VID) -> Option { + self.inner().node_entry(v).value().timestamps().first_t() + } + + fn vertex_latest_time(&self, v: VID) -> Option { + self.inner().node_entry(v).value().timestamps().last_t() + } + + fn view_start(&self) -> Option { + self.earliest_time_global() + } + + fn view_end(&self) -> Option { + self.latest_time_global().map(|t| t.saturating_add(1)) // so it is exclusive + } + + fn earliest_time_global(&self) -> Option { + self.inner().graph_earliest_time() + } + + fn latest_time_global(&self) -> Option { + self.inner().graph_latest_time() + } + + fn earliest_time_window(&self, t_start: i64, t_end: i64) -> Option { + self.inner() + .storage + .nodes + .read_lock() + .into_par_iter() + .flat_map(|v| v.timestamps().range(t_start..t_end).first_t()) + .min() + } + + fn latest_time_window(&self, t_start: i64, t_end: i64) -> Option { + self.inner() + .storage + .nodes + .read_lock() + .into_par_iter() + .flat_map(|v| v.timestamps().range(t_start..t_end).last_t()) + .max() + } + + fn vertex_earliest_time_window(&self, v: VID, t_start: i64, t_end: i64) -> Option { + self.inner() + .node_entry(v) + .value() + .timestamps() + .range(t_start..t_end) + .first_t() + } + + fn vertex_latest_time_window(&self, v: VID, t_start: i64, t_end: i64) -> Option { + self.inner() + .node_entry(v) + .value() + .timestamps() + .range(t_start..t_end) + .last_t() + } + + #[inline] + fn include_vertex_window( + &self, + v: VID, + w: Range, + _layer_ids: &LayerIds, + _edge_filter: Option<&EdgeFilter>, + ) -> bool { + self.inner().node_entry(v).timestamps().active(w) + } + + #[inline] + fn include_edge_window(&self, e: &EdgeStore, w: Range, layer_ids: &LayerIds) -> bool { + e.active(layer_ids, w) + } + + fn vertex_history(&self, v: VID) -> Vec { + self.vertex_additions(v).iter_t().copied().collect() + } + + fn vertex_history_window(&self, v: VID, w: Range) -> Vec { + self.vertex_additions(v) + .range(w) + .iter_t() + .copied() + .collect() + } + + fn edge_exploded(&self, e: EdgeRef, layer_ids: LayerIds) -> BoxedIter { + let arc = self.inner().edge_arc(e.pid()); + let layer_id = layer_ids.constrain_from_edge(e); + let iter: GenBoxed = GenBoxed::new_boxed(|co| async move { + // this is for when we explode edges we want to select the layer we get the timestamps from + for (l, t) in arc.timestamps_and_layers(layer_id) { + co.yield_(e.at(*t).at_layer(l)).await; + } + }); + Box::new(iter.into_iter()) + } + + fn edge_layers(&self, e: EdgeRef, layer_ids: LayerIds) -> BoxedIter { + let arc = self.inner().edge_arc(e.pid()); + let layer_ids = layer_ids.constrain_from_edge(e); + let iter: GenBoxed = GenBoxed::new_boxed(|co| async move { + for l in arc.layers() { + if layer_ids.contains(&l) { + co.yield_(e.at_layer(l)).await; + } + } + }); + Box::new(iter.into_iter()) + } + + fn edge_window_exploded( + &self, + e: EdgeRef, + w: Range, + layer_ids: LayerIds, + ) -> BoxedIter { + let arc = self.inner().edge_arc(e.pid()); + let layer_ids = layer_ids.constrain_from_edge(e); + let iter: GenBoxed = GenBoxed::new_boxed(|co| async move { + // this is for when we explode edges we want to select the layer we get the timestamps from + for (l, t) in arc.timestamps_and_layers_window(layer_ids, w) { + co.yield_(e.at(*t).at_layer(l)).await; + } + }); + Box::new(iter.into_iter()) + } + + fn edge_window_layers( + &self, + e: EdgeRef, + w: Range, + layer_ids: LayerIds, + ) -> BoxedIter { + let arc = self.inner().edge_arc(e.pid()); + let iter: GenBoxed = GenBoxed::new_boxed(|co| async move { + for l in arc.layers_window(w) { + if layer_ids.contains(&l) { + co.yield_(e.at_layer(l)).await; + } + } + }); + Box::new(iter.into_iter()) + } + + fn edge_earliest_time(&self, e: EdgeRef, layer_ids: LayerIds) -> Option { + e.time_t() + .or_else(|| self.edge_additions(e, layer_ids).first_t()) + } + + fn edge_earliest_time_window( + &self, + e: EdgeRef, + w: Range, + layer_ids: LayerIds, + ) -> Option { + e.time_t() + .or_else(|| self.edge_additions(e, layer_ids).range(w).first_t()) + } + + fn edge_latest_time(&self, e: EdgeRef, layer_ids: LayerIds) -> Option { + e.time_t() + .or_else(|| self.edge_additions(e, layer_ids).last_t()) + } + + fn edge_latest_time_window( + &self, + e: EdgeRef, + w: Range, + layer_ids: LayerIds, + ) -> Option { + e.time_t() + .or_else(|| self.edge_additions(e, layer_ids).range(w).last_t()) + } + + fn edge_deletion_history(&self, e: EdgeRef, layer_ids: LayerIds) -> Vec { + self.edge_deletions(e, layer_ids) + .iter_t() + .copied() + .collect() + } + + fn edge_deletion_history_window( + &self, + e: EdgeRef, + w: Range, + layer_ids: LayerIds, + ) -> Vec { + self.edge_deletions(e, layer_ids) + .range(w) + .iter_t() + .copied() + .collect() + } + + fn has_temporal_prop(&self, prop_id: usize) -> bool { + prop_id < self.inner().graph_props.temporal_prop_meta().len() + } + + fn temporal_prop_vec(&self, prop_id: usize) -> Vec<(i64, Prop)> { + self.inner() + .get_temporal_prop(prop_id) + .map(|prop| prop.iter().collect()) + .unwrap_or_default() + } + + fn has_temporal_prop_window(&self, prop_id: usize, w: Range) -> bool { + self.inner() + .graph_props + .get_temporal_prop(prop_id) + .filter(|p| p.iter_window(w).next().is_some()) + .is_some() + } + + fn temporal_prop_vec_window( + &self, + prop_id: usize, + t_start: i64, + t_end: i64, + ) -> Vec<(i64, Prop)> { + self.inner() + .get_temporal_prop(prop_id) + .map(|prop| prop.iter_window(t_start..t_end).collect()) + .unwrap_or_default() + } + + fn has_temporal_vertex_prop(&self, v: VID, prop_id: usize) -> bool { + let entry = self.inner().storage.get_node(v); + entry.temporal_property(prop_id).is_some() + } + + fn temporal_vertex_prop_vec(&self, v: VID, prop_id: usize) -> Vec<(i64, Prop)> { + self.inner() + .vertex(v) + .temporal_properties(prop_id, None) + .collect() + } + + fn has_temporal_vertex_prop_window(&self, v: VID, prop_id: usize, w: Range) -> bool { + let entry = self.inner().storage.get_node(v); + entry + .temporal_property(prop_id) + .filter(|p| p.iter_window(w).next().is_some()) + .is_some() + } + + fn temporal_vertex_prop_vec_window( + &self, + v: VID, + prop_id: usize, + t_start: i64, + t_end: i64, + ) -> Vec<(i64, Prop)> { + self.inner() + .vertex(v) + .temporal_properties(prop_id, Some(t_start..t_end)) + .collect() + } + + fn has_temporal_edge_prop_window( + &self, + e: EdgeRef, + prop_id: usize, + w: Range, + layer_ids: LayerIds, + ) -> bool { + let entry = self.inner().storage.get_edge(e.pid()); + entry.has_temporal_prop_window(layer_ids, prop_id, w) + } + + fn temporal_edge_prop_vec_window( + &self, + e: EdgeRef, + prop_id: usize, + t_start: i64, + t_end: i64, + layer_ids: LayerIds, + ) -> Vec<(i64, Prop)> { + self.temporal_edge_prop(e, prop_id, layer_ids) + .map(|p| match e.time() { + Some(t) => { + if *t.t() >= t_start && *t.t() < t_end { + p.at(&t).map(|v| vec![(*t.t(), v)]).unwrap_or_default() + } else { + vec![] + } + } + None => p.iter_window(t_start..t_end).collect(), + }) + .unwrap_or_default() + } + + fn has_temporal_edge_prop(&self, e: EdgeRef, prop_id: usize, layer_ids: LayerIds) -> bool { + let entry = self.inner().storage.get_edge(e.pid()); + entry.has_temporal_prop(&layer_ids, prop_id) + } + + fn temporal_edge_prop_vec( + &self, + e: EdgeRef, + prop_id: usize, + layer_ids: LayerIds, + ) -> Vec<(i64, Prop)> { + self.temporal_edge_prop(e, prop_id, layer_ids) + .map(|p| match e.time() { + Some(t) => p.at(&t).map(|v| vec![(*t.t(), v)]).unwrap_or_default(), + None => p.iter().collect(), + }) + .unwrap_or_default() + } +} diff --git a/raphtory/src/db/mod.rs b/raphtory/src/db/mod.rs index e188e11a69..80ea7d70d2 100644 --- a/raphtory/src/db/mod.rs +++ b/raphtory/src/db/mod.rs @@ -1,12 +1,5 @@ -pub mod doc_strings; -pub mod edge; +pub mod api; pub mod graph; -pub mod graph_immutable; -pub mod graph_layer; -pub mod graph_window; -pub mod path; -pub mod subgraph_vertex; +pub(crate) mod internal; pub mod task; -pub mod vertex; -pub mod vertices; -pub mod view_api; +pub mod utils; diff --git a/raphtory/src/db/subgraph_vertex.rs b/raphtory/src/db/subgraph_vertex.rs deleted file mode 100644 index 7b02ffea6e..0000000000 --- a/raphtory/src/db/subgraph_vertex.rs +++ /dev/null @@ -1,481 +0,0 @@ -use crate::core::edge_ref::EdgeRef; -use crate::core::vertex_ref::{LocalVertexRef, VertexRef}; -use crate::core::{Direction, Prop}; -use crate::db::view_api::internal::GraphViewInternalOps; -use crate::db::view_api::GraphViewOps; -use rustc_hash::FxHashSet; -use std::collections::HashMap; -use std::iter; -use std::ops::Range; -use std::sync::Arc; - -#[derive(Clone, Debug)] -pub struct VertexSubgraph { - graph: G, - vertices: Arc>, -} - -impl VertexSubgraph { - pub(crate) fn new(graph: G, vertices: FxHashSet) -> Self { - Self {graph, vertices: Arc::new(vertices)} - } -} - - -impl GraphViewInternalOps for VertexSubgraph { - fn local_vertex(&self, v: VertexRef) -> Option { - self.graph - .local_vertex(v) - .filter(|v| self.vertices.contains(v)) - } - - fn local_vertex_window( - &self, - v: VertexRef, - t_start: i64, - t_end: i64, - ) -> Option { - self.graph - .local_vertex_window(v, t_start, t_end) - .filter(|v| self.vertices.contains(v)) - } - - fn get_unique_layers_internal(&self) -> Vec { - self.graph.get_unique_layers_internal() - } - - fn get_layer_name_by_id(&self, layer_id: usize) -> String { - self.graph.get_layer_name_by_id(layer_id) - } - - fn get_layer(&self, key: Option<&str>) -> Option { - self.graph.get_layer(key) - } - - fn view_start(&self) -> Option { - self.graph.view_start() - } - - fn view_end(&self) -> Option { - self.graph.view_end() - } - - fn earliest_time_global(&self) -> Option { - self.vertices - .iter() - .flat_map(|v| self.graph.vertex_earliest_time(*v)) - .min() - } - - fn earliest_time_window(&self, t_start: i64, t_end: i64) -> Option { - self.vertices - .iter() - .flat_map(|v| self.graph.vertex_earliest_time_window(*v, t_start, t_end)) - .min() - } - - fn latest_time_global(&self) -> Option { - self.vertices - .iter() - .flat_map(|v| self.graph.vertex_latest_time(*v)) - .max() - } - - fn latest_time_window(&self, t_start: i64, t_end: i64) -> Option { - self.vertices - .iter() - .flat_map(|v| self.graph.vertex_latest_time_window(*v, t_start, t_end)) - .max() - } - - fn vertices_len(&self) -> usize { - self.vertices.len() - } - - fn vertices_len_window(&self, t_start: i64, t_end: i64) -> usize { - self.vertices - .iter() - .filter(|&&v| { - self.graph - .has_vertex_ref_window(VertexRef::Local(v), t_start, t_end) - }) - .count() - } - - fn edges_len(&self, layer: Option) -> usize { - self.vertices - .iter() - .map(|v| self.degree(*v, Direction::OUT, layer)) - .sum() - } - - fn edges_len_window(&self, t_start: i64, t_end: i64, layer: Option) -> usize { - self.vertices - .iter() - .map(|v| self.degree_window(*v, t_start, t_end, Direction::OUT, layer)) - .sum() - } - - fn has_edge_ref(&self, src: VertexRef, dst: VertexRef, layer: usize) -> bool { - self.has_vertex_ref(src) - && self.has_vertex_ref(dst) - && self.graph.has_edge_ref(src, dst, layer) - } - - fn has_edge_ref_window( - &self, - src: VertexRef, - dst: VertexRef, - t_start: i64, - t_end: i64, - layer: usize, - ) -> bool { - self.has_vertex_ref(src) - && self.has_vertex_ref(dst) - && self - .graph - .has_edge_ref_window(src, dst, t_start, t_end, layer) - } - - fn has_vertex_ref(&self, v: VertexRef) -> bool { - self.local_vertex(v).is_some() - } - - fn has_vertex_ref_window(&self, v: VertexRef, t_start: i64, t_end: i64) -> bool { - self.local_vertex_window(v, t_start, t_end).is_some() - } - - fn degree(&self, v: LocalVertexRef, d: Direction, layer: Option) -> usize { - self.vertex_edges(v, d, layer).count() - } - - fn degree_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - d: Direction, - layer: Option, - ) -> usize { - self.vertex_edges_window(v, t_start, t_end, d, layer) - .count() - } - - fn vertex_ref(&self, v: u64) -> Option { - self.local_vertex(v.into()) - } - - fn vertex_id(&self, v: LocalVertexRef) -> u64 { - self.graph.vertex_id(v) - } - - fn vertex_ref_window(&self, v: u64, t_start: i64, t_end: i64) -> Option { - self.local_vertex_window(v.into(), t_start, t_end) - } - - fn vertex_earliest_time(&self, v: LocalVertexRef) -> Option { - self.vertex_edges(v, Direction::BOTH, None) - .flat_map(|e| self.graph.edge_timestamps(e, None).first().copied()) - .min() - } - - fn vertex_earliest_time_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - ) -> Option { - self.vertex_edges(v, Direction::BOTH, None) - .flat_map(|e| { - self.graph - .edge_timestamps(e, Some(t_start..t_end)) - .first() - .copied() - }) - .min() - } - - fn vertex_latest_time(&self, v: LocalVertexRef) -> Option { - self.vertex_edges(v, Direction::BOTH, None) - .flat_map(|e| self.graph.edge_timestamps(e, None).last().copied()) - .max() - } - - fn vertex_latest_time_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - ) -> Option { - self.vertex_edges(v, Direction::BOTH, None) - .flat_map(|e| { - self.graph - .edge_timestamps(e, Some(t_start..t_end)) - .last() - .copied() - }) - .max() - } - - fn vertex_refs(&self) -> Box + Send> { - // this sucks but seems to be the only way currently (see also http://smallcultfollowing.com/babysteps/blog/2018/09/02/rust-pattern-iterating-an-over-a-rc-vec-t/) - let verts = Vec::from_iter(self.vertices.iter().copied()); - Box::new(verts.into_iter()) - } - - fn vertex_refs_window( - &self, - t_start: i64, - t_end: i64, - ) -> Box + Send> { - let g = self.clone(); - Box::new( - self.vertex_refs() - .filter(move |&v| g.has_vertex_ref_window(VertexRef::Local(v), t_start, t_end)), - ) - } - - fn vertex_refs_shard(&self, shard: usize) -> Box + Send> { - // FIXME: if keep shards, they need to support views (i.e., implement GraphViewInternalOps, this is terrible!) - Box::new(self.vertex_refs().filter(move |&v| v.shard_id == shard)) - } - - fn vertex_refs_window_shard( - &self, - shard: usize, - t_start: i64, - t_end: i64, - ) -> Box + Send> { - // FIXME: if keep shards, they need to support views (i.e., implement GraphViewInternalOps, this is terrible!) - Box::new( - self.vertex_refs_window(t_start, t_end) - .filter(move |&v| v.shard_id == shard), - ) - } - - fn edge_ref(&self, src: VertexRef, dst: VertexRef, layer: usize) -> Option { - if self.has_vertex_ref(src) && self.has_vertex_ref(dst) { - self.graph.edge_ref(src, dst, layer) - } else { - None - } - } - - fn edge_ref_window( - &self, - src: VertexRef, - dst: VertexRef, - t_start: i64, - t_end: i64, - layer: usize, - ) -> Option { - if self.has_vertex_ref(src) && self.has_vertex_ref(dst) { - self.graph.edge_ref_window(src, dst, t_start, t_end, layer) - } else { - None - } - } - - fn edge_refs(&self, layer: Option) -> Box + Send> { - let g1 = self.clone(); - Box::new( - self.vertex_refs() - .flat_map(move |v| g1.vertex_edges(v, Direction::OUT, layer)), - ) - } - - fn edge_refs_window( - &self, - t_start: i64, - t_end: i64, - layer: Option, - ) -> Box + Send> { - let g1 = self.clone(); - Box::new( - self.vertex_refs().flat_map(move |v| { - g1.vertex_edges_window(v, t_start, t_end, Direction::OUT, layer) - }), - ) - } - - fn vertex_edges( - &self, - v: LocalVertexRef, - d: Direction, - layer: Option, - ) -> Box + Send> { - let g = self.clone(); - Box::new( - self.graph - .vertex_edges(v, d, layer) - .filter(move |&e| g.has_vertex_ref(e.remote())), - ) - } - - fn vertex_edges_t( - &self, - v: LocalVertexRef, - d: Direction, - layer: Option, - ) -> Box + Send> { - // FIXME: Could be improved if we had an edge_t function as it calls filter too many times - let g = self.clone(); - Box::new( - self.graph - .vertex_edges_t(v, d, layer) - .filter(move |&e| g.has_vertex_ref(e.remote())), - ) - } - - fn vertex_edges_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - d: Direction, - layer: Option, - ) -> Box + Send> { - let g = self.clone(); - Box::new( - self.graph - .vertex_edges_window(v, t_start, t_end, d, layer) - .filter(move |&e| g.has_vertex_ref(e.remote())), - ) - } - - fn vertex_edges_window_t( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - d: Direction, - layer: Option, - ) -> Box + Send> { - // FIXME: Could be improved if we had an edge_t function as it calls filter too many times - let g = self.clone(); - Box::new( - self.graph - .vertex_edges_window_t(v, t_start, t_end, d, layer) - .filter(move |&e| g.has_vertex_ref(e.remote())), - ) - } - - fn neighbours( - &self, - v: LocalVertexRef, - d: Direction, - layer: Option, - ) -> Box + Send> { - Box::new(self.vertex_edges(v, d, layer).map(|e| e.remote())) - } - - fn neighbours_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - d: Direction, - layer: Option, - ) -> Box + Send> { - Box::new( - self.vertex_edges_window(v, t_start, t_end, d, layer) - .map(|e| e.remote()), - ) - } - - fn static_vertex_prop(&self, v: LocalVertexRef, name: String) -> Option { - self.graph.static_vertex_prop(v, name) - } - - fn static_vertex_prop_names(&self, v: LocalVertexRef) -> Vec { - self.graph.static_vertex_prop_names(v) - } - - fn temporal_vertex_prop_names(&self, v: LocalVertexRef) -> Vec { - self.graph.temporal_vertex_prop_names(v) - } - - fn temporal_vertex_prop_vec(&self, v: LocalVertexRef, name: String) -> Vec<(i64, Prop)> { - self.graph.temporal_vertex_prop_vec(v, name) - } - - fn vertex_timestamps(&self, v: LocalVertexRef) -> Vec { - self.graph.vertex_timestamps(v) - } - - fn vertex_timestamps_window(&self, v: LocalVertexRef, t_start: i64, t_end: i64) -> Vec { - self.graph.vertex_timestamps_window(v, t_start, t_end) - } - - fn temporal_vertex_prop_vec_window( - &self, - v: LocalVertexRef, - name: String, - t_start: i64, - t_end: i64, - ) -> Vec<(i64, Prop)> { - self.graph - .temporal_vertex_prop_vec_window(v, name, t_start, t_end) - } - - fn temporal_vertex_props(&self, v: LocalVertexRef) -> HashMap> { - self.graph.temporal_vertex_props(v) - } - - fn temporal_vertex_props_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - ) -> HashMap> { - self.graph.temporal_vertex_props_window(v, t_start, t_end) - } - - fn static_edge_prop(&self, e: EdgeRef, name: String) -> Option { - self.graph.static_edge_prop(e, name) - } - - fn static_edge_prop_names(&self, e: EdgeRef) -> Vec { - self.graph.static_edge_prop_names(e) - } - - fn temporal_edge_prop_names(&self, e: EdgeRef) -> Vec { - self.graph.temporal_edge_prop_names(e) - } - - fn temporal_edge_props_vec(&self, e: EdgeRef, name: String) -> Vec<(i64, Prop)> { - self.graph.temporal_edge_props_vec(e, name) - } - - fn temporal_edge_props_vec_window( - &self, - e: EdgeRef, - name: String, - t_start: i64, - t_end: i64, - ) -> Vec<(i64, Prop)> { - self.graph - .temporal_edge_props_vec_window(e, name, t_start, t_end) - } - - fn edge_timestamps(&self, e: EdgeRef, window: Option>) -> Vec { - self.graph.edge_timestamps(e, window) - } - - fn temporal_edge_props(&self, e: EdgeRef) -> HashMap> { - self.graph.temporal_edge_props(e) - } - - fn temporal_edge_props_window( - &self, - e: EdgeRef, - t_start: i64, - t_end: i64, - ) -> HashMap> { - self.graph.temporal_edge_props_window(e, t_start, t_end) - } - - fn num_shards(&self) -> usize { - self.graph.num_shards() - } -} diff --git a/raphtory/src/db/task/context.rs b/raphtory/src/db/task/context.rs index 2772917f9a..1124203b74 100644 --- a/raphtory/src/db/task/context.rs +++ b/raphtory/src/db/task/context.rs @@ -1,17 +1,15 @@ -use std::sync::Arc; - +use super::task_state::{Global, Shard}; use crate::{ core::{ - agg::Accumulator, + entities::VID, state::{ - accumulator_id::AccId, compute_state::ComputeState, shuffle_state::ShuffleComputeState, - StateType, + accumulator_id::AccId, agg::Accumulator, compute_state::ComputeState, + shuffle_state::ShuffleComputeState, StateType, }, }, - db::view_api::GraphViewOps, + db::{api::view::GraphViewOps, graph::vertex::VertexView}, }; - -use super::task_state::{Global, Shard}; +use std::{fmt::Debug, sync::Arc}; type MergeFn = Arc, &ShuffleComputeState, usize) + Send + Sync>; @@ -32,6 +30,20 @@ where G: GraphViewOps, CS: ComputeState, { + pub fn new_local_state) -> O>( + &self, + init_f: F, + ) -> Vec { + let n = self.g.unfiltered_num_vertices(); + let mut new_state = Vec::with_capacity(n); + for i in 0..n { + match self.g.vertex(VID(i)) { + Some(v) => new_state.push(init_f(v)), + None => new_state.push(O::default()), + } + } + new_state + } pub fn ss(&self) -> usize { self.ss } @@ -61,7 +73,6 @@ where mut a: Arc>, mut b: Arc>, ) -> Arc> { - // println!("Running merge \na: {:?} \nb: {:?}", a,b); if let Some(left) = Arc::get_mut(&mut a) { for merge_fn in self.merge_fns.iter() { merge_fn(left, &b, self.ss); @@ -82,7 +93,7 @@ where &mut self, id: AccId, ) { - let fn_merge: MergeFn = Arc::new(move |a, b, ss| a.merge_mut_2(b, id, ss)); + let fn_merge: MergeFn = Arc::new(move |a, b, ss| a.merge_mut(b, id, ss)); self.merge_fns.push(fn_merge); } @@ -91,7 +102,7 @@ where &mut self, id: AccId, ) { - let fn_merge: MergeFn = Arc::new(move |a, b, ss| a.merge_mut_2(b, id, ss)); + let fn_merge: MergeFn = Arc::new(move |a, b, ss| a.merge_mut(b, id, ss)); self.merge_fns.push(fn_merge); self.resetable_states.push(id.id()); @@ -146,7 +157,6 @@ pub struct GlobalState { } impl GlobalState { - pub fn finalize>( &self, agg_def: &AccId, @@ -157,7 +167,8 @@ impl GlobalState { { // ss needs to be incremented because the loop ran once and at the end it incremented the state thus // the value is on the previous ss - self.state.inner() + self.state + .inner() .read_global(self.ss + 1, agg_def) .unwrap_or_default() } diff --git a/raphtory/src/db/task/edge/eval_edge.rs b/raphtory/src/db/task/edge/eval_edge.rs new file mode 100644 index 0000000000..b6d5031e61 --- /dev/null +++ b/raphtory/src/db/task/edge/eval_edge.rs @@ -0,0 +1,277 @@ +use crate::{ + core::{ + entities::{edges::edge_ref::EdgeRef, LayerIds, VID}, + state::compute_state::ComputeState, + ArcStr, Prop, + }, + db::{ + api::{ + properties::{ + internal::{ConstPropertiesOps, TemporalPropertiesOps, TemporalPropertyViewOps}, + Properties, + }, + view::*, + }, + task::{ + task_state::Local2, + vertex::{eval_vertex::EvalVertexView, eval_vertex_state::EVState}, + }, + }, +}; +use std::{cell::RefCell, iter, marker::PhantomData, rc::Rc}; + +pub struct EvalEdgeView<'a, G: GraphViewOps, CS: ComputeState, S> { + ss: usize, + ev: EdgeRef, + graph: &'a G, + vertex_state: Rc>>, + local_state_prev: &'a Local2<'a, S>, + _s: PhantomData, +} + +impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> EvalEdgeView<'a, G, CS, S> { + pub(crate) fn new( + ss: usize, + ev: EdgeRef, + graph: &'a G, + vertex_state: Rc>>, + local_state_prev: &'a Local2<'a, S>, + ) -> Self { + Self { + ss, + ev, + graph, + vertex_state, + local_state_prev, + _s: PhantomData, + } + } + + fn layer_ids(&self) -> LayerIds { + self.graph.layer_ids().constrain_from_edge(self.ev) + } +} + +impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> + EdgeViewInternalOps> for EvalEdgeView<'a, G, CS, S> +{ + fn graph(&self) -> G { + self.graph.clone() + } + + fn eref(&self) -> EdgeRef { + self.ev + } + + fn new_vertex(&self, v: VID) -> EvalVertexView<'a, G, CS, S> { + EvalVertexView::new_local( + self.ss, + v, + self.graph, + None, + self.local_state_prev, + self.vertex_state.clone(), + ) + } + + fn new_edge(&self, e: EdgeRef) -> Self { + EvalEdgeView::new_( + self.ss, + e, + self.graph, + self.local_state_prev, + self.vertex_state.clone(), + ) + } +} + +impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> ConstPropertiesOps + for EvalEdgeView<'a, G, CS, S> +{ + fn get_const_prop_id(&self, name: &str) -> Option { + self.graph.edge_meta().const_prop_meta().get_id(name) + } + + fn get_const_prop_name(&self, id: usize) -> ArcStr { + self.graph.edge_meta().const_prop_meta().get_name(id) + } + + fn const_prop_ids(&self) -> Box + '_> { + self.graph + .const_edge_prop_ids(self.ev, self.graph.layer_ids()) + } + + fn get_const_prop(&self, prop_id: usize) -> Option { + self.graph + .get_const_edge_prop(self.ev, prop_id, self.graph.layer_ids()) + } +} + +impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> Clone for EvalEdgeView<'a, G, CS, S> { + fn clone(&self) -> Self { + Self { + ss: self.ss, + ev: self.ev, + graph: self.graph, + vertex_state: self.vertex_state.clone(), + local_state_prev: self.local_state_prev, + _s: Default::default(), + } + } +} + +impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> TemporalPropertyViewOps + for EvalEdgeView<'a, G, CS, S> +{ + fn temporal_history(&self, id: usize) -> Vec { + self.graph + .temporal_edge_prop_vec(self.ev, id, self.graph.layer_ids()) + .into_iter() + .map(|(t, _)| t) + .collect() + } + + fn temporal_values(&self, id: usize) -> Vec { + self.graph + .temporal_edge_prop_vec(self.ev, id, self.graph.layer_ids()) + .into_iter() + .map(|(_, v)| v) + .collect() + } +} + +impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> TemporalPropertiesOps + for EvalEdgeView<'a, G, CS, S> +{ + fn get_temporal_prop_id(&self, name: &str) -> Option { + self.graph + .edge_meta() + .temporal_prop_meta() + .get_id(name) + .filter(|id| { + self.graph + .has_temporal_edge_prop(self.ev, *id, self.layer_ids()) + }) + } + + fn get_temporal_prop_name(&self, id: usize) -> ArcStr { + self.graph.edge_meta().temporal_prop_meta().get_name(id) + } + + fn temporal_prop_ids(&self) -> Box + '_> { + Box::new( + self.graph + .temporal_edge_prop_ids(self.ev, self.layer_ids()) + .filter(|id| { + self.graph + .has_temporal_edge_prop(self.ev, *id, self.layer_ids()) + }), + ) + } +} + +impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> EdgeViewOps for EvalEdgeView<'a, G, CS, S> { + type Graph = G; + type Vertex = EvalVertexView<'a, G, CS, S>; + type EList = Box + 'a>; + + fn explode(&self) -> Self::EList { + let iter: Box> = match self.ev.time() { + Some(_) => Box::new(iter::once(self.ev)), + None => Box::new(self.graph.edge_exploded(self.ev, LayerIds::All)), + }; + + let ss = self.ss; + let g = self.graph; + let vertex_state = self.vertex_state.clone(); + let local_state_prev = self.local_state_prev; + Box::new( + iter.map(move |ev| { + EvalEdgeView::new(ss, ev, g, vertex_state.clone(), local_state_prev) + }), + ) + } + + fn explode_layers(&self) -> Self::EList { + let iter: Box> = match self.ev.time() { + Some(_) => Box::new(iter::once(self.ev)), + None => Box::new(self.graph.edge_layers(self.ev, LayerIds::All)), + }; + + let ss = self.ss; + let g = self.graph; + let vertex_state = self.vertex_state.clone(); + let local_state_prev = self.local_state_prev; + Box::new( + iter.map(move |ev| { + EvalEdgeView::new(ss, ev, g, vertex_state.clone(), local_state_prev) + }), + ) + } +} + +impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> EdgeListOps + for Box> + 'a> +{ + type Graph = G; + type Vertex = EvalVertexView<'a, G, CS, S>; + type Edge = EvalEdgeView<'a, G, CS, S>; + type ValueType = T; + type VList = Box + 'a>; + type IterType = Box + 'a>; + + fn properties(self) -> Self::IterType> { + Box::new(self.map(move |e| e.properties())) + } + + fn src(self) -> Self::VList { + Box::new(self.map(|e| e.src())) + } + + fn dst(self) -> Self::VList { + Box::new(self.map(|e| e.dst())) + } + + fn id(self) -> Self::IterType<(u64, u64)> { + Box::new(self.map(|e| e.id())) + } + + fn explode(self) -> Self::IterType { + Box::new(self.flat_map(|e| e.explode())) + } + + fn earliest_time(self) -> Self::IterType> { + Box::new(self.map(|e| e.earliest_time())) + } + + fn latest_time(self) -> Self::IterType> { + Box::new(self.map(|e| e.latest_time())) + } + + fn time(self) -> Self::IterType> { + Box::new(self.map(|e| e.time())) + } + + fn layer_name(self) -> Self::IterType> { + Box::new(self.map(|e| e.layer_name().map(|v| v.clone()))) + } +} + +impl<'a, G: GraphViewOps, CS: ComputeState, S> EvalEdgeView<'a, G, CS, S> { + pub(crate) fn new_( + ss: usize, + ev: EdgeRef, + graph: &'a G, + local_state_prev: &'a Local2<'a, S>, + vertex_state: Rc>>, + ) -> Self { + Self { + ss, + ev, + graph, + vertex_state, + local_state_prev, + _s: PhantomData, + } + } +} diff --git a/raphtory/src/db/task/edge/mod.rs b/raphtory/src/db/task/edge/mod.rs new file mode 100644 index 0000000000..4bb00ca1e8 --- /dev/null +++ b/raphtory/src/db/task/edge/mod.rs @@ -0,0 +1,2 @@ +pub mod eval_edge; +pub mod window_eval_edge; diff --git a/raphtory/src/db/task/edge/window_eval_edge.rs b/raphtory/src/db/task/edge/window_eval_edge.rs new file mode 100644 index 0000000000..78d5a6c6ed --- /dev/null +++ b/raphtory/src/db/task/edge/window_eval_edge.rs @@ -0,0 +1,391 @@ +use crate::{ + core::{ + entities::{edges::edge_ref::EdgeRef, LayerIds, VID}, + state::compute_state::ComputeState, + ArcStr, Prop, + }, + db::{ + api::{ + properties::{ + internal::{ConstPropertiesOps, TemporalPropertiesOps, TemporalPropertyViewOps}, + Properties, + }, + view::{internal::*, *}, + }, + graph::views::window_graph::WindowedGraph, + task::{ + task_state::Local2, + vertex::{eval_vertex_state::EVState, window_eval_vertex::WindowEvalVertex}, + }, + }, +}; +use std::{cell::RefCell, iter, marker::PhantomData, rc::Rc}; + +pub struct WindowEvalEdgeView<'a, G: GraphViewOps, CS: ComputeState, S: 'static> { + ss: usize, + ev: EdgeRef, + g: &'a G, + vertex_state: Rc>>, + local_state_prev: &'a Local2<'a, S>, + t_start: i64, + t_end: i64, + _s: PhantomData, + edge_filter: Option>, +} + +impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> WindowEvalEdgeView<'a, G, CS, S> { + pub(crate) fn new( + ss: usize, + ev: EdgeRef, + g: &'a G, + local_state_prev: &'a Local2<'a, S>, + vertex_state: Rc>>, + t_start: i64, + t_end: i64, + edge_filter: Option>, + ) -> Self { + Self { + ss, + ev, + g, + vertex_state, + local_state_prev, + t_start, + t_end, + _s: PhantomData, + edge_filter, + } + } + + pub fn history(&self) -> Vec { + self.graph() + .edge_window_exploded(self.eref(), self.t_start..self.t_end, LayerIds::All) + .map(|e| e.time_t().expect("exploded")) + .collect() + } + + fn layer_ids(&self) -> LayerIds { + self.g.layer_ids().constrain_from_edge(self.ev) + } +} +impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> + EdgeViewInternalOps, WindowEvalVertex<'a, G, CS, S>> + for WindowEvalEdgeView<'a, G, CS, S> +{ + fn graph(&self) -> WindowedGraph { + WindowedGraph::new(self.g.clone(), self.t_start, self.t_end) + } + + fn eref(&self) -> EdgeRef { + self.ev.clone() + } + + fn new_vertex(&self, v: VID) -> WindowEvalVertex<'a, G, CS, S> { + WindowEvalVertex::new( + self.ss, + v, + self.g, + None, + self.local_state_prev, + self.vertex_state.clone(), + self.t_start, + self.t_end, + self.edge_filter.clone(), + ) + } + + fn new_edge(&self, e: EdgeRef) -> Self { + WindowEvalEdgeView::new( + self.ss, + e, + self.g, + self.local_state_prev, + self.vertex_state.clone(), + self.t_start, + self.t_end, + self.edge_filter.clone(), + ) + } +} + +impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> ConstPropertiesOps + for WindowEvalEdgeView<'a, G, CS, S> +{ + fn get_const_prop_id(&self, name: &str) -> Option { + self.g.edge_meta().const_prop_meta().get_id(name) + } + + fn get_const_prop_name(&self, id: usize) -> ArcStr { + self.g.edge_meta().const_prop_meta().get_name(id) + } + + fn const_prop_ids(&self) -> Box + '_> { + self.g.const_edge_prop_ids(self.ev, self.g.layer_ids()) + } + + fn get_const_prop(&self, prop_id: usize) -> Option { + self.graph() + .get_const_edge_prop(self.ev, prop_id, self.g.layer_ids()) + } +} + +impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> Clone for WindowEvalEdgeView<'a, G, CS, S> { + fn clone(&self) -> Self { + Self { + ss: self.ss, + ev: self.ev, + g: self.g, + vertex_state: self.vertex_state.clone(), + local_state_prev: self.local_state_prev, + t_start: self.t_start, + t_end: self.t_end, + _s: Default::default(), + edge_filter: self.edge_filter.clone(), + } + } +} + +impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> TemporalPropertyViewOps + for WindowEvalEdgeView<'a, G, CS, S> +{ + fn temporal_value(&self, id: usize) -> Option { + self.g + .temporal_edge_prop_vec_window( + self.ev, + id, + self.t_start, + self.t_end, + self.g.layer_ids(), + ) + .last() + .map(|(_, v)| v.to_owned()) + } + + fn temporal_history(&self, id: usize) -> Vec { + self.g + .temporal_edge_prop_vec_window( + self.ev, + id, + self.t_start, + self.t_end, + self.g.layer_ids(), + ) + .into_iter() + .map(|(t, _)| t) + .collect() + } + + fn temporal_values(&self, id: usize) -> Vec { + self.g + .temporal_edge_prop_vec_window( + self.ev, + id, + self.t_start, + self.t_end, + self.g.layer_ids(), + ) + .into_iter() + .map(|(_, v)| v) + .collect() + } +} + +impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> TemporalPropertiesOps + for WindowEvalEdgeView<'a, G, CS, S> +{ + fn get_temporal_prop_id(&self, key: &str) -> Option { + self.g + .edge_meta() + .temporal_prop_meta() + .get_id(key) + .filter(|&id| { + self.g.has_temporal_edge_prop_window( + self.ev, + id, + self.t_start..self.t_end, + self.layer_ids(), + ) + }) + } + + fn get_temporal_prop_name(&self, id: usize) -> ArcStr { + self.g.edge_meta().temporal_prop_meta().get_name(id) + } + + fn temporal_prop_ids(&self) -> Box + '_> { + Box::new( + self.g + .temporal_edge_prop_ids(self.ev, self.g.layer_ids()) + .filter(|&id| { + self.g.has_temporal_edge_prop_window( + self.ev, + id, + self.t_start..self.t_end, + self.layer_ids(), + ) + }), + ) + } +} + +impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> EdgeViewOps + for WindowEvalEdgeView<'a, G, CS, S> +{ + type Graph = WindowedGraph; + + type Vertex = WindowEvalVertex<'a, G, CS, S>; + + type EList = Box + 'a>; + + fn history(&self) -> Vec { + self.graph() + .edge_window_exploded(self.ev, self.t_start..self.t_end, self.g.layer_ids()) + .map(|eref| eref.time_t().expect("exploded")) + .collect() + } + + /// Check if edge is active at a given time point + fn active(&self, t: i64) -> bool { + match self.eref().time_t() { + Some(tt) => tt <= t && t <= self.latest_time().unwrap_or(tt), + None => { + let layer_ids = self.graph().layer_ids().constrain_from_edge(self.eref()); + let entry = self.graph().core_edge(self.eref().pid()); + (self.t_start..self.t_end).contains(&t) + && self + .graph() + .include_edge_window(&entry, t..t.saturating_add(1), &layer_ids) + } + } + } + + fn explode(&self) -> Self::EList { + let e = self.ev.clone(); + let t_start = self.t_start; + let t_end = self.t_end; + let ss = self.ss; + let g = self.g; + let layer_ids = g.layer_ids(); + let vertex_state = self.vertex_state.clone(); + let local_state_prev = self.local_state_prev; + let edge_filter = self.edge_filter.clone(); + match self.ev.time() { + Some(_) => Box::new(iter::once(self.new_edge(e))), + None => { + let ts = self.g.edge_window_exploded(e, t_start..t_end, layer_ids); + Box::new(ts.map(move |ex| { + WindowEvalEdgeView::new( + ss, + ex, + g, + local_state_prev, + vertex_state.clone(), + t_start, + t_end, + edge_filter.clone(), + ) + })) + } + } + } + + fn explode_layers(&self) -> Self::EList { + let e = self.ev.clone(); + let t_start = self.t_start; + let t_end = self.t_end; + let ss = self.ss; + let g = self.g; + let vertex_state = self.vertex_state.clone(); + let local_state_prev = self.local_state_prev; + let edge_filter = self.edge_filter.clone(); + let layer_ids = g.layer_ids(); + + match self.ev.time() { + Some(_) => Box::new(iter::once(self.new_edge(e))), + None => { + let ts = self.g.edge_window_layers(e, t_start..t_end, layer_ids); + Box::new(ts.map(move |ex| { + WindowEvalEdgeView::new( + ss, + ex, + g, + local_state_prev, + vertex_state.clone(), + t_start, + t_end, + edge_filter.clone(), + ) + })) + } + } + } + + /// Gets the first time an edge was seen + fn earliest_time(&self) -> Option { + self.eref().time_t().or_else(|| { + self.graph().edge_earliest_time_window( + self.eref(), + self.t_start..self.t_end, + LayerIds::All, + ) + }) + } + + /// Gets the latest time an edge was updated + fn latest_time(&self) -> Option { + self.eref().time_t().or_else(|| { + self.graph().edge_latest_time_window( + self.eref(), + self.t_start..self.t_end, + LayerIds::All, + ) + }) + } +} + +impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> EdgeListOps + for Box> + 'a> +{ + type Graph = WindowedGraph; + type Vertex = WindowEvalVertex<'a, G, CS, S>; + type Edge = WindowEvalEdgeView<'a, G, CS, S>; + type ValueType = T; + type VList = Box + 'a>; + type IterType = Box + 'a>; + + fn properties(self) -> Self::IterType> { + Box::new(self.map(move |e| e.properties())) + } + + fn src(self) -> Self::VList { + Box::new(self.map(|e| e.src())) + } + + fn dst(self) -> Self::VList { + Box::new(self.map(|e| e.dst())) + } + + fn id(self) -> Self::IterType<(u64, u64)> { + Box::new(self.map(|e| e.id())) + } + + fn explode(self) -> Self::IterType { + Box::new(self.flat_map(move |it| it.explode())) + } + + fn earliest_time(self) -> Self::IterType> { + Box::new(self.map(|e| e.earliest_time())) + } + + fn latest_time(self) -> Self::IterType> { + Box::new(self.map(|e| e.latest_time())) + } + + fn time(self) -> Self::IterType> { + Box::new(self.map(|e| e.time())) + } + + fn layer_name(self) -> Self::IterType> { + Box::new(self.map(|e| e.layer_name().map(|v| v.clone()))) + } +} diff --git a/raphtory/src/db/task/eval_edge.rs b/raphtory/src/db/task/eval_edge.rs deleted file mode 100644 index 7f9eb9859b..0000000000 --- a/raphtory/src/db/task/eval_edge.rs +++ /dev/null @@ -1,189 +0,0 @@ -use crate::core::edge_ref::EdgeRef; -use crate::core::state::compute_state::ComputeState; -use crate::core::vertex_ref::VertexRef; -use crate::core::Prop; -use crate::db::task::eval_vertex::EvalVertexView; -use crate::db::view_api::edge::{EdgeViewInternalOps, EdgeViewOps}; -use crate::db::view_api::{EdgeListOps, GraphViewOps}; -use std::cell::RefCell; -use std::collections::HashMap; -use std::iter; -use std::marker::PhantomData; -use std::rc::Rc; - -use super::eval_vertex_state::EVState; -use super::task_state::Local2; - -pub struct EvalEdgeView<'a, G: GraphViewOps, CS: ComputeState, S> { - ss: usize, - ev: EdgeRef, - graph: &'a G, - vertex_state: Rc>>, - local_state_prev: &'a Local2<'a, S>, - _s: PhantomData, -} - -impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> EvalEdgeView<'a, G, CS, S> { - pub(crate) fn new( - ss: usize, - ev: EdgeRef, - graph: &'a G, - vertex_state: Rc>>, - local_state_prev: &'a Local2<'a, S>, - ) -> Self { - Self { - ss, - ev, - graph, - vertex_state, - local_state_prev, - _s: PhantomData, - } - } -} - -impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> - EdgeViewInternalOps> for EvalEdgeView<'a, G, CS, S> -{ - fn graph(&self) -> G { - self.graph.clone() - } - - fn eref(&self) -> EdgeRef { - self.ev.clone() - } - - fn new_vertex(&self, v: VertexRef) -> EvalVertexView<'a, G, CS, S> { - EvalVertexView::new_local( - self.ss, - self.graph.localise_vertex_unchecked(v), - self.graph, - None, - self.local_state_prev, - self.vertex_state.clone(), - ) - } - - fn new_edge(&self, e: EdgeRef) -> Self { - EvalEdgeView::new_( - self.ss, - e, - self.graph, - self.local_state_prev, - self.vertex_state.clone(), - ) - } -} - -impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> EdgeViewOps for EvalEdgeView<'a, G, CS, S> { - type Graph = G; - type Vertex = EvalVertexView<'a, G, CS, S>; - type EList = Box + 'a>; - - fn explode(&self) -> Self::EList { - let iter: Box> = match self.ev.time() { - Some(_) => Box::new(iter::once(self.ev.clone())), - None => { - let e = self.ev.clone(); - let ts = self.graph.edge_timestamps(self.ev, None); - Box::new(ts.into_iter().map(move |t| e.at(t))) - } - }; - - let ss = self.ss; - let g = self.graph; - let vertex_state = self.vertex_state.clone(); - let local_state_prev = self.local_state_prev; - Box::new( - iter.map(move |ev| { - EvalEdgeView::new(ss, ev, g, vertex_state.clone(), local_state_prev) - }), - ) - } -} - -impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> EdgeListOps - for Box> + 'a> -{ - type Graph = G; - type Vertex = EvalVertexView<'a, G, CS, S>; - type Edge = EvalEdgeView<'a, G, CS, S>; - type ValueType = T; - type VList = Box + 'a>; - type IterType = Box + 'a>; - - fn has_property(self, name: String, include_static: bool) -> Self::IterType { - Box::new(self.map(move |e| e.has_property(name.clone(), include_static))) - } - - fn property(self, name: String, include_static: bool) -> Self::IterType> { - Box::new(self.map(move |e| e.property(name.clone(), include_static))) - } - - fn properties(self, include_static: bool) -> Self::IterType> { - Box::new(self.map(move |e| e.properties(include_static))) - } - - fn property_names(self, include_static: bool) -> Self::IterType> { - Box::new(self.map(move |e| e.property_names(include_static))) - } - - fn has_static_property(self, name: String) -> Self::IterType { - Box::new(self.map(move |e| e.has_static_property(name.clone()))) - } - - fn static_property(self, name: String) -> Self::IterType> { - Box::new(self.map(move |e| e.static_property(name.clone()))) - } - - fn property_history(self, name: String) -> Self::IterType> { - Box::new(self.map(move |e| e.property_history(name.clone()))) - } - - fn property_histories(self) -> Self::IterType>> { - Box::new(self.map(|e| e.property_histories())) - } - - fn src(self) -> Self::VList { - Box::new(self.map(|e| e.src())) - } - - fn dst(self) -> Self::VList { - Box::new(self.map(|e| e.dst())) - } - - fn id(self) -> Self::IterType<(u64, u64)> { - Box::new(self.map(|e| e.id())) - } - - fn explode(self) -> Self::IterType { - Box::new(self.flat_map(|e| e.explode())) - } - - fn earliest_time(self) -> Self::IterType> { - Box::new(self.map(|e| e.earliest_time())) - } - - fn latest_time(self) -> Self::IterType> { - Box::new(self.map(|e| e.latest_time())) - } -} - -impl<'a, G: GraphViewOps, CS: ComputeState, S> EvalEdgeView<'a, G, CS, S> { - pub(crate) fn new_( - ss: usize, - ev: EdgeRef, - graph: &'a G, - local_state_prev: &'a Local2<'a, S>, - vertex_state: Rc>>, - ) -> Self { - Self { - ss, - ev, - graph, - vertex_state, - local_state_prev, - _s: PhantomData, - } - } -} diff --git a/raphtory/src/db/task/mod.rs b/raphtory/src/db/task/mod.rs index 2d878338e1..e6d1c19532 100644 --- a/raphtory/src/db/task/mod.rs +++ b/raphtory/src/db/task/mod.rs @@ -1,17 +1,13 @@ -use std::sync::Arc; - use once_cell::sync::Lazy; use rayon::{ThreadPool, ThreadPoolBuilder}; +use std::sync::Arc; pub mod context; -pub mod eval_edge; -pub mod eval_vertex; -pub mod eval_vertex_state; +pub mod edge; pub mod task; pub mod task_runner; pub(crate) mod task_state; -pub mod window_eval_vertex; -pub mod window_eval_edge; +pub mod vertex; pub static POOL: Lazy> = Lazy::new(|| { let num_threads = std::env::var("DOCBROWN_MAX_THREADS") @@ -46,7 +42,8 @@ pub fn custom_pool(n_threads: usize) -> Arc { mod task_tests { use crate::{ core::state::{self, compute_state::ComputeStateVec}, - db::graph::Graph, + db::{api::mutation::AdditionOps, task::vertex::eval_vertex::EvalVertexView}, + prelude::*, }; use super::{ @@ -58,7 +55,7 @@ mod task_tests { // count all the vertices with a global state #[test] fn count_all_vertices_with_global_state() { - let graph = Graph::new(2); + let graph = Graph::new(); let edges = vec![ (1, 2, 1), @@ -71,7 +68,7 @@ mod task_tests { ]; for (src, dst, ts) in edges { - graph.add_edge(ts, src, dst, &vec![], None).unwrap(); + graph.add_edge(ts, src, dst, NO_PROPS, None).unwrap(); } let mut ctx: Context = (&graph).into(); @@ -80,17 +77,19 @@ mod task_tests { ctx.global_agg(count.clone()); - let step1 = ATask::new(move |vv| { - vv.global_update(&count, 1); - Step::Done - }); + let step1 = ATask::new( + move |vv: &mut EvalVertexView<'_, Graph, ComputeStateVec, ()>| { + vv.global_update(&count, 1); + Step::Done + }, + ); let mut runner = TaskRunner::new(ctx); let actual = runner.run( vec![], vec![Job::new(step1)], - (), + None, |egs, _, _, _| egs.finalize(&count), Some(2), 1, diff --git a/raphtory/src/db/task/task.rs b/raphtory/src/db/task/task.rs index 32aba17c71..6d90944e07 100644 --- a/raphtory/src/db/task/task.rs +++ b/raphtory/src/db/task/task.rs @@ -1,11 +1,9 @@ -use std::marker::PhantomData; - -use crate::core::state::compute_state::ComputeState; -use crate::db::view_api::internal::GraphViewInternalOps; -use crate::db::view_api::GraphViewOps; - use super::context::GlobalState; -use super::eval_vertex::EvalVertexView; +use crate::{ + core::state::compute_state::ComputeState, + db::{api::view::GraphViewOps, task::vertex::eval_vertex::EvalVertexView}, +}; +use std::marker::PhantomData; pub trait Task where @@ -40,7 +38,7 @@ pub enum Job { Check(Box) -> Step + Send + Sync + 'static>), } -impl Job { +impl Job { pub fn new + Send + Sync + 'static>(t: T) -> Self { Self::Write(Box::new(t)) } diff --git a/raphtory/src/db/task/task_runner.rs b/raphtory/src/db/task/task_runner.rs index 62c90d33b1..5fcdf7d46e 100644 --- a/raphtory/src/db/task/task_runner.rs +++ b/raphtory/src/db/task/task_runner.rs @@ -1,24 +1,29 @@ -use std::{ - borrow::Cow, - rc::Rc, - sync::atomic::{AtomicBool, Ordering}, -}; - -use rayon::{prelude::*, ThreadPool}; - -use crate::core::state::shuffle_state::{EvalLocalState, EvalShardState}; -use crate::core::vertex_ref::LocalVertexRef; -use crate::{core::state::compute_state::ComputeState, db::view_api::GraphViewOps}; - use super::{ context::{Context, GlobalState}, custom_pool, - eval_vertex::EvalVertexView, - eval_vertex_state::EVState, task::{Job, Step, Task}, task_state::{Global, Local2, Shard}, POOL, }; +use crate::{ + core::{ + entities::vertices::vertex_ref::VertexRef, + state::{ + compute_state::ComputeState, + shuffle_state::{EvalLocalState, EvalShardState}, + }, + }, + db::{ + api::view::GraphViewOps, + task::vertex::{eval_vertex::EvalVertexView, eval_vertex_state::EVState}, + }, +}; +use rayon::{prelude::*, ThreadPool}; +use std::{ + borrow::Cow, + rc::Rc, + sync::atomic::{AtomicBool, Ordering}, +}; pub struct TaskRunner { pub(crate) ctx: Context, @@ -43,10 +48,11 @@ impl TaskRunner { &self, shard_state: &Shard, global_state: &Global, - morcel: &mut [Option<(LocalVertexRef, S)>], - prev_local_state: &Vec>, - max_shard_len: usize, + morcel: &mut [S], + prev_local_state: &Vec, atomic_done: &AtomicBool, + morcel_size: usize, + morcel_id: usize, task: &Box + Send + Sync>, ) -> (Shard, Global) { // the view for this task of the global state @@ -59,13 +65,17 @@ impl TaskRunner { let vertex_state = EVState::rc_from(shard_state_view, global_state_view); - let local = Local2::new(max_shard_len, prev_local_state); - - for line in morcel { - if let Some((v_ref, local_state)) = line { + let local = Local2::new(prev_local_state); + let mut v_ref = morcel_id * morcel_size; + for local_state in morcel { + if g.has_vertex_ref( + VertexRef::Internal(v_ref.into()), + &g.layer_ids(), + g.edge_filter().as_deref(), + ) { let mut vv = EvalVertexView::new_local( self.ctx.ss(), - v_ref.clone(), + v_ref.into(), &g, Some(local_state), &local, @@ -79,6 +89,7 @@ impl TaskRunner { Step::Done => {} } } + v_ref += 1; } if !done { @@ -112,19 +123,13 @@ impl TaskRunner { &mut self, tasks: &[Job], pool: &ThreadPool, + morcel_size: usize, shard_state: Shard, global_state: Global, - mut local_state: Vec>, - prev_local_state: &Vec>, - max_shard_len: usize, - ) -> ( - bool, - Shard, - Global, - Vec>, - ) { + mut local_state: Vec, + prev_local_state: &Vec, + ) -> (bool, Shard, Global, Vec) { pool.install(move || { - let chunk_size = 16_000; let mut new_shard_state = shard_state; let mut new_global_state = global_state; @@ -135,31 +140,37 @@ impl TaskRunner { let updated_state: Option<(Shard, Global)> = match task { Job::Write(task) => local_state - .par_chunks_mut(chunk_size) - .map(|morcel| { + .par_chunks_mut(morcel_size) + .enumerate() + .map(|(morcel_id, morcel)| { self.run_task_v2( &new_shard_state, &new_global_state, morcel, prev_local_state, - max_shard_len, &atomic_done, + morcel_size, + morcel_id, task, ) }) .reduce_with(|a, b| self.merge_states(a, b)), Job::Read(task) => { - local_state.par_chunks_mut(chunk_size).for_each(|morcel| { - self.run_task_v2( - &new_shard_state, - &new_global_state, - morcel, - prev_local_state, - max_shard_len, - &atomic_done, - task, - ); - }); + local_state + .par_chunks_mut(morcel_size) + .enumerate() + .for_each(|(morcel_id, morcel)| { + self.run_task_v2( + &new_shard_state, + &new_global_state, + morcel, + prev_local_state, + &atomic_done, + morcel_size, + morcel_id, + task, + ); + }); None } Job::Check(task) => { @@ -188,90 +199,68 @@ impl TaskRunner { }) } - fn make_cur_and_prev_states( - &self, - init: S, - ) -> ( - usize, - Vec>, - Vec>, - ) { + fn make_cur_and_prev_states(&self, mut init: Vec) -> (Vec, Vec) { let g = self.ctx.graph(); + init.resize(g.unfiltered_num_vertices(), S::default()); - // find the shard with the largest number of vertices - let max_shard_len = g.vertex_refs().map(|v| v.pid).max().unwrap_or(0) + 1; - - let n_shards = g.num_shards(); - - let mut states = vec![None; max_shard_len * n_shards]; - - for v_ref in g.vertex_refs() { - let LocalVertexRef { shard_id, pid } = v_ref; - let i = max_shard_len * shard_id + pid; - states[i] = Some((v_ref.clone(), init.clone())); - } - - (max_shard_len, states.clone(), states) + (init.clone(), init) } pub fn run< B: std::fmt::Debug, - F: FnOnce( - GlobalState, - EvalShardState, - EvalLocalState, - &Vec>, - ) -> B - + std::marker::Copy, - S: Send + Sync + Clone + 'static + std::fmt::Debug, + F: FnOnce(GlobalState, EvalShardState, EvalLocalState, Vec) -> B, + S: Send + Sync + Clone + 'static + std::fmt::Debug + Default, >( &mut self, init_tasks: Vec>, tasks: Vec>, - init: S, + init: Option>, f: F, num_threads: Option, steps: usize, shard_initial_state: Option>, global_initial_state: Option>, ) -> B { - let graph_shards = self.ctx.graph().num_shards(); - let pool = num_threads .map(|nt| custom_pool(nt)) .unwrap_or_else(|| POOL.clone()); - let mut shard_state = shard_initial_state.unwrap_or_else(|| Shard::new(graph_shards)); + let num_vertices = self.ctx.graph().unfiltered_num_vertices(); + let morcel_size = num_vertices.min(16_000); + let num_chunks = (num_vertices + morcel_size - 1) / morcel_size; + + let mut shard_state = shard_initial_state + .unwrap_or_else(|| Shard::new(num_vertices, num_chunks, morcel_size)); let mut global_state = global_initial_state.unwrap_or_else(|| Global::new()); - let (max_shard_len, mut cur_local_state, mut prev_local_state) = - self.make_cur_and_prev_states::(init); + let (mut cur_local_state, mut prev_local_state) = + self.make_cur_and_prev_states::(init.unwrap_or_default()); - let mut done = false; + let mut _done = false; - (done, shard_state, global_state, cur_local_state) = self.run_task_list( + (_done, shard_state, global_state, cur_local_state) = self.run_task_list( &init_tasks, &pool, + morcel_size, shard_state, global_state, cur_local_state, &prev_local_state, - max_shard_len, ); // To allow the init step to cache stuff we will copy everything from cur_local_state to prev_local_state prev_local_state.clone_from_slice(&cur_local_state); - while !done && self.ctx.ss() < steps && tasks.len() > 0 { - (done, shard_state, global_state, cur_local_state) = self.run_task_list( + while !_done && self.ctx.ss() < steps && tasks.len() > 0 { + (_done, shard_state, global_state, cur_local_state) = self.run_task_list( &tasks, &pool, + morcel_size, shard_state, global_state, cur_local_state, &prev_local_state, - max_shard_len, ); // copy and reset the state from the step that just ended @@ -292,14 +281,12 @@ impl TaskRunner { } else { prev_local_state }; - //TODO change to log - //println!("Done running iterations: {ss}"); f( GlobalState::new(global_state, ss), EvalShardState::new(ss, self.ctx.graph(), shard_state), EvalLocalState::new(ss, self.ctx.graph(), vec![]), - &last_local_state, + last_local_state, ) } } diff --git a/raphtory/src/db/task/task_state.rs b/raphtory/src/db/task/task_state.rs index aaaccc6769..106f8e2e71 100644 --- a/raphtory/src/db/task/task_state.rs +++ b/raphtory/src/db/task/task_state.rs @@ -1,10 +1,6 @@ +use crate::core::state::{compute_state::ComputeState, shuffle_state::ShuffleComputeState}; use std::{borrow::Cow, sync::Arc}; -use crate::core::{ - state::{compute_state::ComputeState, shuffle_state::ShuffleComputeState}, - vertex_ref::LocalVertexRef, -}; - // this only contains the global state and it is synchronized after each task run #[derive(Clone, Debug)] pub struct Global(Arc>); @@ -19,25 +15,24 @@ pub(crate) struct Local(Arc>>); #[derive(Debug)] pub(crate) struct Local2<'a, S> { - pub(crate) shard_len: usize, - pub(crate) state: &'a Vec>, + pub(crate) state: &'a Vec, } impl<'a, S: 'static> Local2<'a, S> { - pub(crate) fn new( - max_shard_len: usize, - prev_local_state: &'a Vec>, - ) -> Self { + pub(crate) fn new(prev_local_state: &'a Vec) -> Self { Self { - shard_len: max_shard_len, state: prev_local_state, } } } impl Shard { - pub(crate) fn new(graph_shards: usize) -> Self { - Self(Arc::new(ShuffleComputeState::new(graph_shards))) + pub(crate) fn new(total_len: usize, num_morcels: usize, morcel_size: usize) -> Self { + Self(Arc::new(ShuffleComputeState::new( + total_len, + num_morcels, + morcel_size, + ))) } pub(crate) fn as_cow(&self) -> Cow<'_, ShuffleComputeState> { @@ -56,6 +51,10 @@ impl Shard { &self.0 } + pub fn consume(self) -> Result, Arc>> { + Arc::try_unwrap(self.0) + } + pub fn reset(&mut self, ss: usize, resetable_states: &[u32]) { Arc::get_mut(&mut self.0).map(|s| { s.copy_over_next_ss(ss); @@ -72,7 +71,7 @@ impl From>> for Shard { impl Global { pub(crate) fn new() -> Self { - Self(Arc::new(ShuffleComputeState::new(0))) + Self(Arc::new(ShuffleComputeState::global())) } pub(crate) fn as_cow(&self) -> Cow<'_, ShuffleComputeState> { diff --git a/raphtory/src/db/task/eval_vertex.rs b/raphtory/src/db/task/vertex/eval_vertex.rs similarity index 67% rename from raphtory/src/db/task/eval_vertex.rs rename to raphtory/src/db/task/vertex/eval_vertex.rs index 5c30155e5b..0775be6e35 100644 --- a/raphtory/src/db/task/eval_vertex.rs +++ b/raphtory/src/db/task/vertex/eval_vertex.rs @@ -1,31 +1,43 @@ -use crate::core::time::IntoTime; -use crate::core::{Direction, Prop}; -use crate::db::edge::EdgeView; -use crate::db::path::{Operations, PathFromVertex}; -use crate::db::task::eval_edge::EvalEdgeView; -use crate::db::view_api::{BoxedIter, TimeOps, VertexListOps, VertexViewOps}; use crate::{ core::{ - agg::Accumulator, - state::{accumulator_id::AccId, compute_state::ComputeState, StateType}, - vertex_ref::LocalVertexRef, + entities::VID, + state::{accumulator_id::AccId, agg::Accumulator, compute_state::ComputeState, StateType}, + utils::time::IntoTime, + Direction, + }, + db::{ + api::{ + properties::Properties, + view::{ + BoxedIter, EdgeListOps, EdgeViewOps, GraphViewOps, TimeOps, VertexListOps, + VertexViewOps, + }, + }, + graph::{ + edge::EdgeView, + path::{Operations, PathFromVertex}, + vertex::VertexView, + }, + task::{ + edge::eval_edge::EvalEdgeView, + task_state::Local2, + vertex::{ + eval_vertex_state::EVState, + window_eval_vertex::{edge_filter, WindowEvalPathFromVertex, WindowEvalVertex}, + }, + }, }, - db::view_api::GraphViewOps, }; -use std::collections::HashMap; -use std::marker::PhantomData; +use itertools::Itertools; use std::{ cell::{Ref, RefCell}, + marker::PhantomData, rc::Rc, }; -use super::eval_vertex_state::EVState; -use super::task_state::Local2; -use super::window_eval_vertex::{WindowEvalPathFromVertex, WindowEvalVertex}; - pub struct EvalVertexView<'a, G: GraphViewOps, CS: ComputeState, S: 'static> { ss: usize, - vertex: LocalVertexRef, + vertex: VID, pub(crate) graph: &'a G, local_state: Option<&'a mut S>, local_state_prev: &'a Local2<'a, S>, @@ -34,13 +46,8 @@ pub struct EvalVertexView<'a, G: GraphViewOps, CS: ComputeState, S: 'static> { impl<'a, G: GraphViewOps, CS: ComputeState, S> EvalVertexView<'a, G, CS, S> { pub fn prev(&self) -> &S { - let LocalVertexRef { shard_id, pid } = self.vertex; - let shard_size = self.local_state_prev.shard_len; - let i = shard_size * shard_id + pid; - self.local_state_prev.state[i] - .as_ref() - .map(|(_, val)| val) - .unwrap() + let i: usize = self.vertex.into(); + &self.local_state_prev.state[i] } pub fn get_mut(&mut self) -> &mut S { @@ -59,7 +66,7 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S> EvalVertexView<'a, G, CS, S> { pub(crate) fn new_local( ss: usize, - v_ref: LocalVertexRef, + v_ref: VID, g: &'a G, local_state: Option<&'a mut S>, local_state_prev: &'a Local2<'a, S>, @@ -76,7 +83,7 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S> EvalVertexView<'a, G, CS, S> { } fn pid(&self) -> usize { - self.vertex.pid + self.vertex.into() } pub fn update>( @@ -87,7 +94,7 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S> EvalVertexView<'a, G, CS, S> { self.vertex_state .borrow_mut() .shard_mut() - .accumulate_into_pid(self.ss, self.id(), self.pid(), a, id); + .accumulate_into(self.ss, self.pid(), a, id); } pub fn global_update>( @@ -145,7 +152,7 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S> EvalVertexView<'a, G, CS, S> { self.vertex_state .borrow() .shard() - .read_with_pid(self.ss, self.id(), self.pid(), agg_r) + .read_with_pid(self.ss, self.pid(), agg_r) .unwrap_or(ACC::finish(&ACC::zero())) } @@ -159,13 +166,7 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S> EvalVertexView<'a, G, CS, S> { A: StateType, OUT: std::fmt::Debug, { - Entry::new( - self.vertex_state.borrow(), - *agg_r, - &self.vertex, - self.id(), - self.ss, - ) + Entry::new(self.vertex_state.borrow(), *agg_r, &self.vertex, self.ss) } /// Read the prev value of the vertex state using the given accumulator. @@ -181,7 +182,7 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S> EvalVertexView<'a, G, CS, S> { self.vertex_state .borrow() .shard() - .read_with_pid(self.ss + 1, self.id(), self.pid(), agg_r) + .read_with_pid(self.ss + 1, self.pid(), agg_r) .unwrap_or(ACC::finish(&ACC::zero())) } @@ -240,10 +241,10 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> EvalPathFromVertex<'a, G Box::new(self.path.iter_refs().map(|v| { EvalVertexView::new_local( self.ss, - self.g.localise_vertex_unchecked(v), + v, self.g, None, - self.local_state_prev.clone(), + self.local_state_prev, self.vertex_state.clone(), ) })) @@ -263,14 +264,7 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> IntoIterator let ss = self.ss; let g: &G = self.g; Box::new(path.iter_refs().map(move |v| { - EvalVertexView::new_local( - ss, - self.g.localise_vertex_unchecked(v), - g, - None, - self.local_state_prev.clone(), - vertex_state.clone(), - ) + EvalVertexView::new_local(ss, v, g, None, self.local_state_prev, vertex_state.clone()) })) } } @@ -289,14 +283,18 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> TimeOps } fn window(&self, t_start: T, t_end: T) -> Self::WindowedViewType { + let t_start = t_start.into_time(); + let t_end = t_end.into_time(); + let edge_filter = edge_filter(self.g, t_start, t_end).map(Rc::new); WindowEvalPathFromVertex::new( self.path.clone(), self.ss, self.g, self.vertex_state.clone(), - self.local_state_prev.clone(), - t_start.into_time(), - t_end.into_time(), + self.local_state_prev, + t_start, + t_end, + edge_filter, ) } } @@ -325,40 +323,12 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> VertexViewOps self.path.latest_time() } - fn property(&self, name: String, include_static: bool) -> Self::ValueType> { - self.path.property(name, include_static) - } - fn history(&self) -> Self::ValueType> { self.path.history() } - fn property_history(&self, name: String) -> Self::ValueType> { - self.path.property_history(name) - } - - fn properties(&self, include_static: bool) -> Self::ValueType> { - self.path.properties(include_static) - } - - fn property_histories(&self) -> Self::ValueType>> { - self.path.property_histories() - } - - fn property_names(&self, include_static: bool) -> Self::ValueType> { - self.path.property_names(include_static) - } - - fn has_property(&self, name: String, include_static: bool) -> Self::ValueType { - self.path.has_property(name, include_static) - } - - fn has_static_property(&self, name: String) -> Self::ValueType { - self.path.has_static_property(name) - } - - fn static_property(&self, name: String) -> Self::ValueType> { - self.path.static_property(name) + fn properties(&self) -> Self::ValueType>> { + self.path.properties() } fn degree(&self) -> Self::ValueType { @@ -410,15 +380,19 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S> TimeOps for EvalVertexView<'a, G, } fn window(&self, t_start: T, t_end: T) -> Self::WindowedViewType { + let t_start = t_start.into_time(); + let t_end = t_end.into_time(); + let edge_filter = edge_filter(self.graph, t_start, t_end).map(Rc::new); WindowEvalVertex::new( self.ss, self.vertex, self.graph, None, - self.local_state_prev.clone(), + self.local_state_prev, self.vertex_state.clone(), - t_start.into_time(), - t_end.into_time(), + t_start, + t_end, + edge_filter, ) } } @@ -447,89 +421,44 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> VertexViewOps self.graph.vertex_latest_time(self.vertex) } - fn property(&self, name: String, include_static: bool) -> Self::ValueType> { - let props = self.property_history(name.clone()); - match props.last() { - None => { - if include_static { - self.graph.static_vertex_prop(self.vertex, name) - } else { - None - } - } - Some((_, prop)) => Some(prop.clone()), - } - } - fn history(&self) -> Self::ValueType> { - self.graph.vertex_timestamps(self.vertex) - } - - fn property_history(&self, name: String) -> Self::ValueType> { - self.graph.temporal_vertex_prop_vec(self.vertex, name) - } - - fn properties(&self, include_static: bool) -> Self::ValueType> { - let mut props: HashMap = self - .property_histories() - .iter() - .map(|(key, values)| (key.clone(), values.last().unwrap().1.clone())) - .collect(); - - if include_static { - for prop_name in self.graph.static_vertex_prop_names(self.vertex) { - if let Some(prop) = self - .graph - .static_vertex_prop(self.vertex, prop_name.clone()) - { - props.insert(prop_name, prop); - } - } - } - props + self.edges() + .map(|e| e.explode().earliest_time().flatten()) + .kmerge() + .dedup() + .collect() } - fn property_histories(&self) -> Self::ValueType>> { - self.graph.temporal_vertex_props(self.vertex) - } - - fn property_names(&self, include_static: bool) -> Self::ValueType> { - let mut names: Vec = self.graph.temporal_vertex_prop_names(self.vertex); - if include_static { - names.extend(self.graph.static_vertex_prop_names(self.vertex)) - } - names - } - - fn has_property(&self, name: String, include_static: bool) -> Self::ValueType { - (!self.property_history(name.clone()).is_empty()) - || (include_static - && self - .graph - .static_vertex_prop_names(self.vertex) - .contains(&name)) - } - - fn has_static_property(&self, name: String) -> Self::ValueType { - self.graph - .static_vertex_prop_names(self.vertex) - .contains(&name) - } - - fn static_property(&self, name: String) -> Self::ValueType> { - self.graph.static_vertex_prop(self.vertex, name) + fn properties(&self) -> Self::ValueType>> { + //FIXME: need to implement this properly without cloning the graph... + Properties::new(VertexView::new_internal(self.graph.clone(), self.vertex)) } fn degree(&self) -> Self::ValueType { - self.graph.degree(self.vertex, Direction::BOTH, None) + self.graph.degree( + self.vertex, + Direction::BOTH, + &self.graph.layer_ids(), + self.graph.edge_filter(), + ) } fn in_degree(&self) -> Self::ValueType { - self.graph.degree(self.vertex, Direction::IN, None) + self.graph.degree( + self.vertex, + Direction::IN, + &self.graph.layer_ids(), + self.graph.edge_filter(), + ) } fn out_degree(&self) -> Self::ValueType { - self.graph.degree(self.vertex, Direction::OUT, None) + self.graph.degree( + self.vertex, + Direction::OUT, + &self.graph.layer_ids(), + self.graph.edge_filter(), + ) } fn edges(&self) -> Self::EList { @@ -539,7 +468,12 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> VertexViewOps let graph = self.graph; Box::new( self.graph - .vertex_edges(self.vertex, Direction::BOTH, None) + .vertex_edges( + self.vertex, + Direction::BOTH, + self.graph.layer_ids(), + self.graph.edge_filter(), + ) .map(move |e| EvalEdgeView::new_(ss, e, graph, local, vertex_state.clone())), ) } @@ -551,7 +485,12 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> VertexViewOps let graph = self.graph; Box::new( self.graph - .vertex_edges(self.vertex, Direction::IN, None) + .vertex_edges( + self.vertex, + Direction::IN, + self.graph.layer_ids(), + self.graph.edge_filter(), + ) .map(move |e| EvalEdgeView::new_(ss, e, graph, local, vertex_state.clone())), ) } @@ -563,7 +502,12 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> VertexViewOps let graph = self.graph; Box::new( self.graph - .vertex_edges(self.vertex, Direction::OUT, None) + .vertex_edges( + self.vertex, + Direction::OUT, + self.graph.layer_ids(), + self.graph.edge_filter(), + ) .map(move |e| EvalEdgeView::new_(ss, e, graph, local, vertex_state.clone())), ) } @@ -594,7 +538,9 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> VertexViewOps let neighbours = PathFromVertex::new( self.graph.clone(), self.vertex, - Operations::Neighbours { dir: Direction::OUT }, + Operations::Neighbours { + dir: Direction::OUT, + }, ); EvalPathFromVertex::new_from_path_and_vertex(neighbours, self) @@ -609,8 +555,7 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> VertexViewOps pub struct Entry<'a, 'b, A: StateType, IN, OUT, ACC: Accumulator, CS: ComputeState> { state: Ref<'a, EVState<'b, CS>>, acc_id: AccId, - v_ref: &'a LocalVertexRef, - gid: u64, + v_ref: &'a VID, ss: usize, } @@ -629,15 +574,13 @@ impl<'a, 'b, A: StateType, IN, OUT, ACC: Accumulator, CS: ComputeSta pub(crate) fn new( state: Ref<'a, EVState<'b, CS>>, acc_id: AccId, - v_ref: &'a LocalVertexRef, - gid: u64, + v_ref: &'a VID, ss: usize, ) -> Entry<'a, 'b, A, IN, OUT, ACC, CS> { Entry { state, acc_id, v_ref, - gid, ss, } } @@ -646,7 +589,7 @@ impl<'a, 'b, A: StateType, IN, OUT, ACC: Accumulator, CS: ComputeSta pub fn read_ref(&self) -> Option<&A> { self.state .shard() - .read_ref_with_pid(self.ss, self.gid, self.v_ref.pid, &self.acc_id) + .read_ref(self.ss, (*self.v_ref).into(), &self.acc_id) } } @@ -675,6 +618,10 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> VertexListOps Box::new(self.map(move |v| v.window(t_start, t_end))) } + fn at(self, end: i64) -> Self::IterType<::WindowedViewType> { + Box::new(self.map(move |v| v.at(end))) + } + fn id(self) -> Self::IterType { Box::new(self.map(|v| v.id())) } @@ -683,42 +630,14 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> VertexListOps Box::new(self.map(|v| v.name())) } - fn property(self, name: String, include_static: bool) -> Self::IterType> { - Box::new(self.map(move |v| v.property(name.clone(), include_static))) - } - - fn property_history(self, name: String) -> Self::IterType> { - Box::new(self.map(move |v| v.property_history(name.clone()))) - } - - fn properties(self, include_static: bool) -> Self::IterType> { - Box::new(self.map(move |v| v.properties(include_static))) + fn properties(self) -> Self::IterType>> { + Box::new(self.map(move |v| v.properties())) } fn history(self) -> Self::IterType> { Box::new(self.map(|v| v.history())) } - fn property_histories(self) -> Self::IterType>> { - Box::new(self.map(|v| v.property_histories())) - } - - fn property_names(self, include_static: bool) -> Self::IterType> { - Box::new(self.map(move |v| v.property_names(include_static))) - } - - fn has_property(self, name: String, include_static: bool) -> Self::IterType { - Box::new(self.map(move |v| v.has_property(name.clone(), include_static))) - } - - fn has_static_property(self, name: String) -> Self::IterType { - Box::new(self.map(move |v| v.has_static_property(name.clone()))) - } - - fn static_property(self, name: String) -> Self::IterType> { - Box::new(self.map(move |v| v.static_property(name.clone()))) - } - fn degree(self) -> Self::IterType { Box::new(self.map(|v| v.degree())) } diff --git a/raphtory/src/db/task/eval_vertex_state.rs b/raphtory/src/db/task/vertex/eval_vertex_state.rs similarity index 99% rename from raphtory/src/db/task/eval_vertex_state.rs rename to raphtory/src/db/task/vertex/eval_vertex_state.rs index ae8f0669bb..c0d354c162 100644 --- a/raphtory/src/db/task/eval_vertex_state.rs +++ b/raphtory/src/db/task/vertex/eval_vertex_state.rs @@ -1,6 +1,5 @@ -use std::{borrow::Cow, cell::RefCell, rc::Rc}; - use crate::core::state::{compute_state::ComputeState, shuffle_state::ShuffleComputeState}; +use std::{borrow::Cow, cell::RefCell, rc::Rc}; #[derive(Debug)] pub(crate) struct EVState<'a, CS: ComputeState> { diff --git a/raphtory/src/db/task/vertex/mod.rs b/raphtory/src/db/task/vertex/mod.rs new file mode 100644 index 0000000000..ca5802a826 --- /dev/null +++ b/raphtory/src/db/task/vertex/mod.rs @@ -0,0 +1,3 @@ +pub mod eval_vertex; +pub mod eval_vertex_state; +pub mod window_eval_vertex; diff --git a/raphtory/src/db/task/window_eval_vertex.rs b/raphtory/src/db/task/vertex/window_eval_vertex.rs similarity index 56% rename from raphtory/src/db/task/window_eval_vertex.rs rename to raphtory/src/db/task/vertex/window_eval_vertex.rs index 031e6e519a..64921aa62b 100644 --- a/raphtory/src/db/task/window_eval_vertex.rs +++ b/raphtory/src/db/task/vertex/window_eval_vertex.rs @@ -1,33 +1,54 @@ -use std::{cell::RefCell, collections::HashMap, marker::PhantomData, rc::Rc}; - use crate::{ core::{ - state::{compute_state::ComputeState, StateType, accumulator_id::AccId}, time::IntoTime, vertex_ref::LocalVertexRef, Direction, - Prop, agg::Accumulator, + entities::VID, + state::{accumulator_id::AccId, agg::Accumulator, compute_state::ComputeState, StateType}, + utils::time::IntoTime, + Direction, }, db::{ - path::{Operations, PathFromVertex}, - view_api::{GraphViewOps, TimeOps, VertexViewOps, VertexListOps}, + api::{ + properties::Properties, + view::{ + internal::{EdgeFilter, EdgeFilterOps}, + GraphViewOps, TimeOps, VertexListOps, VertexViewOps, + }, + }, + graph::{ + path::{Operations, PathFromVertex}, + vertex::VertexView, + views::window_graph::WindowedGraph, + }, + task::{ + edge::window_eval_edge::WindowEvalEdgeView, task_state::Local2, + vertex::eval_vertex_state::EVState, + }, }, }; +use std::{cell::RefCell, marker::PhantomData, rc::Rc}; -use super::{eval_vertex_state::EVState, task_state::Local2, window_eval_edge::WindowEvalEdgeView}; +pub(crate) fn edge_filter( + graph: &G, + t_start: i64, + t_end: i64, +) -> Option { + graph.window(t_start, t_end).edge_filter().cloned() +} pub struct WindowEvalVertex<'a, G: GraphViewOps, CS: ComputeState, S: 'static> { ss: usize, - vertex: LocalVertexRef, + vertex: VID, pub(crate) graph: &'a G, - local_state: Option<&'a mut S>, + _local_state: Option<&'a mut S>, local_state_prev: &'a Local2<'a, S>, vertex_state: Rc>>, t_start: i64, t_end: i64, + edge_filter: Option>, } impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> WindowEvalVertex<'a, G, CS, S> { - fn pid(&self) -> usize { - self.vertex.pid + self.vertex.into() } pub fn update>( @@ -38,28 +59,30 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> WindowEvalVertex<'a, G, self.vertex_state .borrow_mut() .shard_mut() - .accumulate_into_pid(self.ss, self.id(), self.pid(), a, id); + .accumulate_into(self.ss, self.pid(), a, id); } pub(crate) fn new( ss: usize, - vertex: LocalVertexRef, + vertex: VID, graph: &'a G, local_state: Option<&'a mut S>, local_state_prev: &'a Local2<'a, S>, vertex_state: Rc>>, t_start: i64, t_end: i64, + edge_filter: Option>, ) -> Self { WindowEvalVertex { ss, vertex, graph, - local_state, + _local_state: local_state, local_state_prev, vertex_state, t_start, t_end, + edge_filter, } } } @@ -76,15 +99,19 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> TimeOps for WindowEvalVe } fn window(&self, t_start: T, t_end: T) -> Self::WindowedViewType { + let t_start = t_start.into_time().max(self.t_start); + let t_end = t_end.into_time().min(self.t_end); + let edge_filter = edge_filter(self.graph, t_start, t_end).map(Rc::new); WindowEvalVertex { ss: self.ss, - vertex: self.vertex.clone(), + vertex: self.vertex, graph: self.graph, - local_state: None, + _local_state: None, local_state_prev: self.local_state_prev, vertex_state: self.vertex_state.clone(), - t_start: t_start.into_time().max(self.t_start), - t_end: t_end.into_time().min(self.t_end), + t_start, + t_end, + edge_filter, } } } @@ -92,7 +119,7 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> TimeOps for WindowEvalVe impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> VertexViewOps for WindowEvalVertex<'a, G, CS, S> { - type Graph = G; + type Graph = WindowedGraph; type ValueType = T; type PathType<'b> = WindowEvalPathFromVertex<'a, G, CS, S> where Self: 'b; type EList = Box> + 'a>; @@ -115,107 +142,47 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> VertexViewOps .vertex_latest_time_window(self.vertex, self.t_start, self.t_end) } - fn property( - &self, - name: String, - include_static: bool, - ) -> Self::ValueType> { - let props = self.property_history(name.clone()); - match props.last() { - None => { - if include_static { - self.graph.static_vertex_prop(self.vertex, name) - } else { - None - } - } - Some((_, prop)) => Some(prop.clone()), - } - } - fn history(&self) -> Self::ValueType> { self.graph - .vertex_timestamps_window(self.vertex, self.t_start, self.t_end) - } - - fn property_history(&self, name: String) -> Self::ValueType> { - self.graph - .temporal_vertex_prop_vec_window(self.vertex, name, self.t_start, self.t_end) - } - - fn properties( - &self, - include_static: bool, - ) -> Self::ValueType> { - let mut props: HashMap = self - .property_histories() - .iter() - .map(|(key, values)| (key.clone(), values.last().unwrap().1.clone())) - .collect(); - - if include_static { - for prop_name in self.graph.static_vertex_prop_names(self.vertex) { - if let Some(prop) = self - .graph - .static_vertex_prop(self.vertex, prop_name.clone()) - { - props.insert(prop_name, prop); - } - } - } - props - } - - fn property_histories( - &self, - ) -> Self::ValueType>> { - self.graph - .temporal_vertex_props_window(self.vertex, self.t_start, self.t_end) - } - - fn property_names(&self, include_static: bool) -> Self::ValueType> { - let mut names: Vec = self.graph.temporal_vertex_prop_names(self.vertex); - if include_static { - names.extend(self.graph.static_vertex_prop_names(self.vertex)) - } - names - } - - fn has_property(&self, name: String, include_static: bool) -> Self::ValueType { - (!self.property_history(name.clone()).is_empty()) - || (include_static - && self - .graph - .static_vertex_prop_names(self.vertex) - .contains(&name)) - } - - fn has_static_property(&self, name: String) -> Self::ValueType { - self.graph - .static_vertex_prop_names(self.vertex) - .contains(&name) + .vertex_history_window(self.vertex, self.t_start..self.t_end) } - fn static_property(&self, name: String) -> Self::ValueType> { - self.graph.static_vertex_prop(self.vertex, name) + fn properties(&self) -> Self::ValueType>>> { + //FIXME: Need to implement this properly without cloning the graph + Properties::new(VertexView::new_internal( + WindowedGraph::new(self.graph.clone(), self.t_start, self.t_end), + self.vertex, + )) } fn degree(&self) -> Self::ValueType { let dir = Direction::BOTH; - self.graph - .degree_window(self.vertex, self.t_start, self.t_end, dir, None) + self.graph.degree( + self.vertex, + dir, + &self.graph.layer_ids(), + self.edge_filter.as_deref(), + ) } fn in_degree(&self) -> Self::ValueType { let dir = Direction::IN; - self.graph - .degree_window(self.vertex, self.t_start, self.t_end, dir, None) + self.graph.degree( + self.vertex, + dir, + &self.graph.layer_ids(), + self.edge_filter.as_deref(), + ) } fn out_degree(&self) -> Self::ValueType { let dir = Direction::OUT; - self.graph - .degree_window(self.vertex, self.t_start, self.t_end, dir, None) + self.graph.degree( + self.vertex, + dir, + &self.graph.layer_ids(), + self.edge_filter.as_deref(), + ) } fn edges(&self) -> Self::EList { @@ -225,10 +192,27 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> VertexViewOps let graph = self.graph; let t_start = self.t_start; let t_end = self.t_end; + let edge_filter = self.edge_filter.clone(); Box::new( self.graph - .vertex_edges_window(self.vertex, self.t_start, self.t_end, Direction::BOTH, None) - .map(move |e| WindowEvalEdgeView::new(ss, e, graph, local, vertex_state.clone(), t_start, t_end)), + .vertex_edges( + self.vertex, + Direction::BOTH, + self.graph.layer_ids(), + self.edge_filter.as_deref(), + ) + .map(move |e| { + WindowEvalEdgeView::new( + ss, + e, + graph, + local, + vertex_state.clone(), + t_start, + t_end, + edge_filter.clone(), + ) + }), ) } @@ -239,10 +223,27 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> VertexViewOps let graph = self.graph; let t_start = self.t_start; let t_end = self.t_end; + let edge_filter = self.edge_filter.clone(); Box::new( self.graph - .vertex_edges_window(self.vertex, self.t_start, self.t_end, Direction::IN, None) - .map(move |e| WindowEvalEdgeView::new(ss, e, graph, local, vertex_state.clone(), t_start, t_end)), + .vertex_edges( + self.vertex, + Direction::IN, + self.graph.layer_ids(), + self.edge_filter.as_deref(), + ) + .map(move |e| { + WindowEvalEdgeView::new( + ss, + e, + graph, + local, + vertex_state.clone(), + t_start, + t_end, + edge_filter.clone(), + ) + }), ) } @@ -253,10 +254,27 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> VertexViewOps let graph = self.graph; let t_start = self.t_start; let t_end = self.t_end; + let edge_filter = self.edge_filter.clone(); Box::new( self.graph - .vertex_edges_window(self.vertex, self.t_start, self.t_end, Direction::OUT, None) - .map(move |e| WindowEvalEdgeView::new(ss, e, graph, local, vertex_state.clone(), t_start, t_end)), + .vertex_edges( + self.vertex, + Direction::OUT, + self.graph.layer_ids(), + self.edge_filter.as_deref(), + ) + .map(move |e| { + WindowEvalEdgeView::new( + ss, + e, + graph, + local, + vertex_state.clone(), + t_start, + t_end, + edge_filter.clone(), + ) + }), ) } @@ -312,6 +330,7 @@ pub struct WindowEvalPathFromVertex<'a, G: GraphViewOps, CS: ComputeState, S> { _s: PhantomData, t_start: i64, t_end: i64, + edge_filter: Option>, } impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> WindowEvalPathFromVertex<'a, G, CS, S> { fn update_path(&self, path: PathFromVertex) -> Self { @@ -324,6 +343,7 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> WindowEvalPathFromVertex t_start: self.t_start, t_end: self.t_end, _s: PhantomData, + edge_filter: self.edge_filter.clone(), } } @@ -340,6 +360,7 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> WindowEvalPathFromVertex _s: PhantomData, t_start: vertex.t_start, t_end: vertex.t_end, + edge_filter: vertex.edge_filter.clone(), } } @@ -351,6 +372,7 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> WindowEvalPathFromVertex local_state_prev: &'a Local2<'a, S>, t_start: i64, t_end: i64, + edge_filter: Option>, ) -> Self { WindowEvalPathFromVertex { path, @@ -361,6 +383,7 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> WindowEvalPathFromVertex _s: PhantomData, t_start, t_end, + edge_filter, } } @@ -374,16 +397,26 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> WindowEvalPathFromVertex let local_state_prev = self.local_state_prev; let t_start = self.t_start; let t_end = self.t_end; - + let edge_filter = self.edge_filter.clone(); + let edge_filter_2 = edge_filter.clone(); + let layer_ids = g.layer_ids(); let iter = self .path .iter_refs() .flat_map(move |v_ref| { - let local_ref = g.localise_vertex_unchecked(v_ref); - g.vertex_edges_window(local_ref, t_start, t_end, dir, None) + g.vertex_edges(v_ref, dir, layer_ids.clone(), edge_filter_2.as_deref()) }) .map(move |e_ref| { - WindowEvalEdgeView::new(ss, e_ref, g, local_state_prev, vertex_state.clone(), t_start, t_end) + WindowEvalEdgeView::new( + ss, + e_ref, + g, + local_state_prev, + vertex_state.clone(), + t_start, + t_end, + edge_filter.clone(), + ) }); Box::new(iter) @@ -391,13 +424,12 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> WindowEvalPathFromVertex fn degree(&self, dir: Direction) -> Box + 'a> { let g = self.g; - let t_start = self.t_start; - let t_end = self.t_end; - - let iter = self.path.iter_refs().map(move |v_ref| { - let local_ref = g.localise_vertex_unchecked(v_ref); - g.degree_window(local_ref, t_start, t_end, dir, None) - }); + let edge_filter = self.edge_filter.clone(); + let layer_ids = g.layer_ids(); + let iter = self + .path + .iter_refs() + .map(move |v_ref| g.degree(v_ref, dir, &layer_ids, edge_filter.as_deref())); Box::new(iter) } @@ -417,14 +449,18 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> TimeOps } fn window(&self, t_start: T, t_end: T) -> Self::WindowedViewType { + let t_start = t_start.into_time().max(self.t_start); + let t_end = t_end.into_time().min(self.t_end); + let filter = edge_filter(self.g, t_start, t_end).map(Rc::new); WindowEvalPathFromVertex::new( self.path.clone(), self.ss, self.g, self.vertex_state.clone(), self.local_state_prev, - t_start.into_time().max(self.t_start), - t_end.into_time().min(self.t_end), + t_start, + t_end, + filter, ) } } @@ -432,7 +468,7 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> TimeOps impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> VertexViewOps for WindowEvalPathFromVertex<'a, G, CS, S> { - type Graph = G; + type Graph = WindowedGraph; type ValueType = Box + 'a>; @@ -456,105 +492,21 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> VertexViewOps self.path.latest_time() } - fn property( - &self, - name: String, - include_static: bool, - ) -> Self::ValueType> { - let g = self.g; - let t_start = self.t_start; - let t_end = self.t_end; - - let iter = self.path.iter_refs().map(move |v_ref| { - let local_ref = g.localise_vertex_unchecked(v_ref); - let props = g.temporal_vertex_prop_vec_window(local_ref, name.clone(), t_start, t_end); - match props.last() { - None => { - if include_static { - g.static_vertex_prop(local_ref, name.clone()) - } else { - None - } - } - Some((_, prop)) => Some(prop.clone()), - } - }); - Box::new(iter) - } - fn history(&self) -> Self::ValueType> { let g = self.g; let t_start = self.t_start; let t_end = self.t_end; - let iter = self.path.iter_refs().map(move |v_ref| { - let local_ref = g.localise_vertex_unchecked(v_ref); - g.vertex_timestamps_window(local_ref, t_start, t_end) - }); - - Box::new(iter) - } - - fn property_history(&self, name: String) -> Self::ValueType> { - let g = self.g; - let t_start = self.t_start; - let t_end = self.t_end; - - let iter = self.path.iter_refs().map(move |v_ref| { - let local_ref = g.localise_vertex_unchecked(v_ref); - g.temporal_vertex_prop_vec_window(local_ref, name.clone(), t_start, t_end) - }); - - Box::new(iter) - } - - fn properties( - &self, - include_static: bool, - ) -> Self::ValueType> { - self.path.properties(include_static) - } - - fn property_histories( - &self, - ) -> Self::ValueType>> { - let g = self.g; - let t_start = self.t_start; - let t_end = self.t_end; - - let iter = self.path.iter_refs().map(move |v_ref| { - let local_ref = g.localise_vertex_unchecked(v_ref); - g.temporal_vertex_props_window(local_ref, t_start, t_end) - }); - - Box::new(iter) - } - - fn property_names(&self, include_static: bool) -> Self::ValueType> { - self.path.property_names(include_static) - } - - fn has_property(&self, name: String, include_static: bool) -> Self::ValueType { - let g = self.g; - let t_start = self.t_start; - let t_end = self.t_end; - let iter = self.path.iter_refs().map(move |v_ref| { - let local_ref = g.localise_vertex_unchecked(v_ref); - let props = g.temporal_vertex_prop_vec_window(local_ref, name.clone(), t_start, t_end); - - !props.is_empty() - || (include_static && g.static_vertex_prop_names(local_ref).contains(&name)) - }); + let iter = self + .path + .iter_refs() + .map(move |v_ref| g.vertex_history_window(v_ref, t_start..t_end)); Box::new(iter) } - fn has_static_property(&self, name: String) -> Self::ValueType { - self.path.has_static_property(name) - } - - fn static_property(&self, name: String) -> Self::ValueType> { - self.path.static_property(name) + fn properties(&self) -> Self::ValueType>> { + self.path.window(self.t_start, self.t_end).properties() } fn degree(&self) -> Self::ValueType { @@ -603,11 +555,10 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> VertexViewOps } } - impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> VertexListOps for Box> + 'a> { - type Graph = G; + type Graph = WindowedGraph; type Vertex = WindowEvalVertex<'a, G, CS, S>; type IterType = Box + 'a>; type EList = Box> + 'a>; @@ -629,6 +580,10 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> VertexListOps Box::new(self.map(move |v| v.window(t_start, t_end))) } + fn at(self, end: i64) -> Self::IterType<::WindowedViewType> { + Box::new(self.map(move |v| v.at(end))) + } + fn id(self) -> Self::IterType { Box::new(self.map(|v| v.id())) } @@ -637,42 +592,14 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> VertexListOps Box::new(self.map(|v| v.name())) } - fn property(self, name: String, include_static: bool) -> Self::IterType> { - Box::new(self.map(move |v| v.property(name.clone(), include_static))) - } - - fn property_history(self, name: String) -> Self::IterType> { - Box::new(self.map(move |v| v.property_history(name.clone()))) - } - - fn properties(self, include_static: bool) -> Self::IterType> { - Box::new(self.map(move |v| v.properties(include_static))) + fn properties(self) -> Self::IterType>> { + Box::new(self.map(move |v| v.properties())) } fn history(self) -> Self::IterType> { Box::new(self.map(|v| v.history())) } - fn property_histories(self) -> Self::IterType>> { - Box::new(self.map(|v| v.property_histories())) - } - - fn property_names(self, include_static: bool) -> Self::IterType> { - Box::new(self.map(move |v| v.property_names(include_static))) - } - - fn has_property(self, name: String, include_static: bool) -> Self::IterType { - Box::new(self.map(move |v| v.has_property(name.clone(), include_static))) - } - - fn has_static_property(self, name: String) -> Self::IterType { - Box::new(self.map(move |v| v.has_static_property(name.clone()))) - } - - fn static_property(self, name: String) -> Self::IterType> { - Box::new(self.map(move |v| v.static_property(name.clone()))) - } - fn degree(self) -> Self::IterType { Box::new(self.map(|v| v.degree())) } @@ -694,7 +621,7 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> VertexListOps } fn out_edges(self) -> Self::EList { - Box::new(self.flat_map(|v| v.out_edges()).map(|ev| WindowEvalEdgeView::from(ev))) + Box::new(self.flat_map(|v| v.out_edges())) } fn neighbours(self) -> Self { @@ -710,7 +637,6 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> VertexListOps } } - impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> IntoIterator for WindowEvalPathFromVertex<'a, G, CS, S> { @@ -728,13 +654,14 @@ impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> IntoIterator Box::new(path.iter_refs().map(move |v| { WindowEvalVertex::new( ss, - self.g.localise_vertex_unchecked(v), + v, g, None, - self.local_state_prev.clone(), + self.local_state_prev, vertex_state.clone(), t_start, t_end, + self.edge_filter.clone(), ) })) } diff --git a/raphtory/src/db/task/window_eval_edge.rs b/raphtory/src/db/task/window_eval_edge.rs deleted file mode 100644 index 1387812fe7..0000000000 --- a/raphtory/src/db/task/window_eval_edge.rs +++ /dev/null @@ -1,264 +0,0 @@ -use std::{cell::RefCell, iter, marker::PhantomData, rc::Rc, collections::HashMap}; - -use crate::{ - core::{edge_ref::EdgeRef, state::compute_state::ComputeState, Prop}, - db::view_api::{edge::EdgeViewInternalOps, EdgeListOps, EdgeViewOps, GraphViewOps}, -}; - -use super::{eval_vertex_state::EVState, task_state::Local2, window_eval_vertex::WindowEvalVertex}; - -pub struct WindowEvalEdgeView<'a, G: GraphViewOps, CS: ComputeState, S: 'static> { - ss: usize, - ev: EdgeRef, - g: &'a G, - vertex_state: Rc>>, - local_state_prev: &'a Local2<'a, S>, - t_start: i64, - t_end: i64, - _s: PhantomData, -} - -impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> WindowEvalEdgeView<'a, G, CS, S> { - pub(crate) fn new( - ss: usize, - ev: EdgeRef, - g: &'a G, - local_state_prev: &'a Local2<'a, S>, - vertex_state: Rc>>, - t_start: i64, - t_end: i64, - ) -> Self { - Self { - ss, - ev, - g, - vertex_state, - local_state_prev, - t_start, - t_end, - _s: PhantomData, - } - } - - pub fn history(&self) -> Vec { - self.graph() - .edge_timestamps(self.eref(), Some(self.t_start..self.t_end)) - } -} -impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> - EdgeViewInternalOps> for WindowEvalEdgeView<'a, G, CS, S> -{ - fn graph(&self) -> G { - self.g.clone() - } - - fn eref(&self) -> EdgeRef { - self.ev.clone() - } - - fn new_vertex(&self, v: crate::core::vertex_ref::VertexRef) -> WindowEvalVertex<'a, G, CS, S> { - WindowEvalVertex::new( - self.ss, - self.g.localise_vertex_unchecked(v), - self.g, - None, - self.local_state_prev, - self.vertex_state.clone(), - self.t_start, - self.t_end, - ) - } - - fn new_edge(&self, e: EdgeRef) -> Self { - WindowEvalEdgeView::new( - self.ss, - e, - self.g, - self.local_state_prev, - self.vertex_state.clone(), - self.t_start, - self.t_end, - ) - } -} - -impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> EdgeViewOps - for WindowEvalEdgeView<'a, G, CS, S> -{ - type Graph = G; - - type Vertex = WindowEvalVertex<'a, G, CS, S>; - - type EList = Box + 'a>; - - fn explode(&self) -> Self::EList { - let e = self.ev.clone(); - let t_start = self.t_start; - let t_end = self.t_end; - let ss = self.ss; - let g = self.g; - let vertex_state = self.vertex_state.clone(); - let local_state_prev = self.local_state_prev; - - match self.ev.time() { - Some(_) => Box::new(iter::once(self.new_edge(e))), - None => { - let ts = self.g.edge_timestamps(self.ev, Some(t_start..t_end)); - Box::new(ts.into_iter().map(move |t| { - WindowEvalEdgeView::new( - ss, - e.at(t), - g, - local_state_prev, - vertex_state.clone(), - t_start, - t_end, - ) - })) - } - } - } - - fn history(&self) -> Vec { - self.graph() - .edge_timestamps(self.eref(), Some(self.t_start..self.t_end)) - } - - fn property_history(&self, name: String) -> Vec<(i64, Prop)> { - match self.eref().time() { - None => self.graph().temporal_edge_props_vec_window( - self.eref(), - name, - self.t_start, - self.t_end, - ), - Some(t) => self.graph().temporal_edge_props_vec_window( - self.eref(), - name, - t, - t.saturating_add(1), - ), - } - } - - fn property_histories(&self) -> HashMap> { - // match on the self.edge.time option property and run two function s - // one for static and one for temporal - match self.eref().time() { - None => self.graph().temporal_edge_props_window(self.eref(), self.t_start, self.t_end), - Some(t) => self - .graph() - .temporal_edge_props_window(self.eref(), t, t.saturating_add(1)), - } - } - - /// Check if edge is active at a given time point - fn active(&self, t: i64) -> bool { - match self.eref().time() { - Some(tt) => tt == t, - None => (self.t_start..self.t_end).contains(&t) && self.graph().has_edge_ref_window( - self.eref().src(), - self.eref().dst(), - t, - t.saturating_add(1), - self.eref().layer(), - ), - } - } - - /// Gets the first time an edge was seen - fn earliest_time(&self) -> Option { - self.eref().time().or_else(|| { - self.graph() - .edge_timestamps(self.eref(), Some(self.t_start..self.t_end)) - .first() - .copied() - }) - } - - /// Gets the latest time an edge was updated - fn latest_time(&self) -> Option { - self.eref().time().or_else(|| { - self.graph() - .edge_timestamps(self.eref(), Some(self.t_start..self.t_end)) - .last() - .copied() - }) - } -} - -impl<'a, G: GraphViewOps, CS: ComputeState, S: 'static> EdgeListOps - for Box> + 'a> -{ - type Graph = G; - type Vertex = WindowEvalVertex<'a, G, CS, S>; - type Edge = WindowEvalEdgeView<'a, G, CS, S>; - type ValueType = T; - type VList = Box + 'a>; - type IterType = Box + 'a>; - - fn id(self) -> Self::IterType<(u64, u64)> { - Box::new(self.map(|e| e.id())) - } - - fn has_property(self, name: String, include_static: bool) -> Self::IterType { - Box::new(self.map(move |e| e.has_property(name.clone(), include_static))) - } - - fn property( - self, - name: String, - include_static: bool, - ) -> Self::IterType> { - Box::new(self.map(move |e| e.property(name.clone(), include_static))) - } - - fn properties( - self, - include_static: bool, - ) -> Self::IterType> { - Box::new(self.map(move |e| e.properties(include_static))) - } - - fn property_names(self, include_static: bool) -> Self::IterType> { - Box::new(self.map(move |e| e.property_names(include_static))) - } - - fn has_static_property(self, name: String) -> Self::IterType { - Box::new(self.map(move |e| e.has_static_property(name.clone()))) - } - - fn static_property(self, name: String) -> Self::IterType> { - Box::new(self.map(move |it| it.static_property(name.clone()))) - } - - fn property_history(self, name: String) -> Self::IterType> { - Box::new(self.map(move |it| it.property_history(name.clone()))) - } - - fn property_histories( - self, - ) -> Self::IterType>> { - Box::new(self.map(|it| it.property_histories())) - } - - fn src(self) -> Self::VList { - Box::new(self.map(|e| e.src())) - } - - fn dst(self) -> Self::VList { - Box::new(self.map(|e| e.dst())) - } - - fn explode(self) -> Self::IterType { - Box::new(self.flat_map(move |it| it.explode())) - } - - fn earliest_time(self) -> Self::IterType> { - Box::new(self.map(|e| e.earliest_time())) - } - - fn latest_time(self) -> Self::IterType> { - Box::new(self.map(|e| e.latest_time())) - } -} diff --git a/raphtory/src/db/doc_strings.rs b/raphtory/src/db/utils/doc_strings.rs similarity index 69% rename from raphtory/src/db/doc_strings.rs rename to raphtory/src/db/utils/doc_strings.rs index 9578ac6599..139b5b41d3 100644 --- a/raphtory/src/db/doc_strings.rs +++ b/raphtory/src/db/utils/doc_strings.rs @@ -10,16 +10,16 @@ Returns: } #[macro_export] -macro_rules! layer_doc_string { +macro_rules! layers_doc_string { () => { " -Create a view including all the edges in the layer `name` +Create a view including all the edges in the layers `names` Arguments: - name (str) : the name of the layer + names (str) : the names of the layers to include Returns: - a view including all the edges in the layer `name`" + a view including all the edges in the layers `names`" }; } diff --git a/raphtory/src/db/utils/mod.rs b/raphtory/src/db/utils/mod.rs new file mode 100644 index 0000000000..aefeca739a --- /dev/null +++ b/raphtory/src/db/utils/mod.rs @@ -0,0 +1 @@ +pub mod doc_strings; diff --git a/raphtory/src/db/vertex.rs b/raphtory/src/db/vertex.rs deleted file mode 100644 index c378c33781..0000000000 --- a/raphtory/src/db/vertex.rs +++ /dev/null @@ -1,474 +0,0 @@ -//! Defines the `Vertex` struct, which represents a vertex in the graph. - -use crate::core::time::IntoTime; -use crate::core::vertex_ref::{LocalVertexRef, VertexRef}; -use crate::core::{Direction, Prop}; -use crate::db::edge::{EdgeList, EdgeView}; -use crate::db::graph_layer::LayeredGraph; -use crate::db::graph_window::WindowedGraph; -use crate::db::path::{Operations, PathFromVertex}; -use crate::db::view_api::layer::LayerOps; -use crate::db::view_api::vertex::VertexViewOps; -use crate::db::view_api::{BoxedIter, GraphViewOps, TimeOps, VertexListOps}; -use std::collections::HashMap; - -#[derive(Debug, Clone)] -pub struct VertexView { - pub graph: G, - pub vertex: LocalVertexRef, -} - -impl From> for VertexRef { - fn from(value: VertexView) -> Self { - VertexRef::Local(value.vertex) - } -} - -impl From<&VertexView> for VertexRef { - fn from(value: &VertexView) -> Self { - VertexRef::Local(value.vertex) - } -} - -impl VertexView { - /// Creates a new `VertexView` wrapping a vertex reference and a graph, localising any remote vertices to the correct shard. - pub(crate) fn new(graph: G, vertex: VertexRef) -> VertexView { - match vertex { - VertexRef::Local(local) => Self::new_local(graph, local), - _ => { - let v = graph.localise_vertex_unchecked(vertex); - VertexView { graph, vertex: v } - } - } - } - - /// Creates a new `VertexView` wrapping a local vertex reference and a graph - pub(crate) fn new_local(graph: G, vertex: LocalVertexRef) -> VertexView { - VertexView { graph, vertex } - } -} - -/// View of a Vertex in a Graph -impl VertexViewOps for VertexView { - type Graph = G; - type ValueType = T; - type PathType<'a> = PathFromVertex where Self: 'a; - type EList = BoxedIter>; - - fn id(&self) -> u64 { - self.graph.vertex_id(self.vertex) - } - - fn name(&self) -> String { - self.graph.vertex_name(self.vertex) - } - - fn earliest_time(&self) -> Option { - self.graph.vertex_earliest_time(self.vertex) - } - - fn latest_time(&self) -> Option { - self.graph.vertex_latest_time(self.vertex) - } - - fn property(&self, name: String, include_static: bool) -> Option { - let props = self.property_history(name.clone()); - match props.last() { - None => { - if include_static { - self.graph.static_vertex_prop(self.vertex, name) - } else { - None - } - } - Some((_, prop)) => Some(prop.clone()), - } - } - - fn history(&self) -> Vec { - self.graph.vertex_timestamps(self.vertex) - } - - fn property_history(&self, name: String) -> Vec<(i64, Prop)> { - self.graph.temporal_vertex_prop_vec(self.vertex, name) - } - - fn properties(&self, include_static: bool) -> HashMap { - let mut props: HashMap = self - .property_histories() - .iter() - .map(|(key, values)| (key.clone(), values.last().unwrap().1.clone())) - .collect(); - - if include_static { - for prop_name in self.graph.static_vertex_prop_names(self.vertex) { - if let Some(prop) = self - .graph - .static_vertex_prop(self.vertex, prop_name.clone()) - { - props.insert(prop_name, prop); - } - } - } - props - } - - fn property_histories(&self) -> HashMap> { - self.graph.temporal_vertex_props(self.vertex) - } - - fn property_names(&self, include_static: bool) -> Vec { - let mut names: Vec = self.graph.temporal_vertex_prop_names(self.vertex); - if include_static { - names.extend(self.graph.static_vertex_prop_names(self.vertex)) - } - names - } - - fn has_property(&self, name: String, include_static: bool) -> bool { - (!self.property_history(name.clone()).is_empty()) - || (include_static - && self - .graph - .static_vertex_prop_names(self.vertex) - .contains(&name)) - } - - fn has_static_property(&self, name: String) -> bool { - self.graph - .static_vertex_prop_names(self.vertex) - .contains(&name) - } - - fn static_property(&self, name: String) -> Option { - self.graph.static_vertex_prop(self.vertex, name) - } - - fn degree(&self) -> usize { - let dir = Direction::BOTH; - self.graph.degree(self.vertex, dir, None) - } - - fn in_degree(&self) -> usize { - let dir = Direction::IN; - self.graph.degree(self.vertex, dir, None) - } - - fn out_degree(&self) -> usize { - let dir = Direction::OUT; - self.graph.degree(self.vertex, dir, None) - } - - fn edges(&self) -> EdgeList { - let g = self.graph.clone(); - let dir = Direction::BOTH; - Box::new( - g.vertex_edges(self.vertex, dir, None) - .map(move |e| EdgeView::new(g.clone(), e)), - ) - } - - fn in_edges(&self) -> EdgeList { - let g = self.graph.clone(); - let dir = Direction::IN; - Box::new( - g.vertex_edges(self.vertex, dir, None) - .map(move |e| EdgeView::new(g.clone(), e)), - ) - } - - fn out_edges(&self) -> EdgeList { - let g = self.graph.clone(); - let dir = Direction::OUT; - Box::new( - g.vertex_edges(self.vertex, dir, None) - .map(move |e| EdgeView::new(g.clone(), e)), - ) - } - - fn neighbours(&self) -> PathFromVertex { - let g = self.graph.clone(); - let dir = Direction::BOTH; - PathFromVertex::new(g, self, Operations::Neighbours { dir }) - } - - fn in_neighbours(&self) -> PathFromVertex { - let g = self.graph.clone(); - let dir = Direction::IN; - PathFromVertex::new(g, self, Operations::Neighbours { dir }) - } - - fn out_neighbours(&self) -> PathFromVertex { - let g = self.graph.clone(); - let dir = Direction::OUT; - PathFromVertex::new(g, self, Operations::Neighbours { dir }) - } -} - -impl TimeOps for VertexView { - type WindowedViewType = VertexView>; - - fn start(&self) -> Option { - self.graph.start() - } - - fn end(&self) -> Option { - self.graph.end() - } - - fn window(&self, t_start: T, t_end: T) -> Self::WindowedViewType { - VertexView { - graph: self.graph.window(t_start, t_end), - vertex: self.vertex, - } - } -} - -impl LayerOps for VertexView { - type LayeredViewType = VertexView>; - - fn default_layer(&self) -> Self::LayeredViewType { - VertexView { - graph: self.graph.default_layer(), - vertex: self.vertex, - } - } - - fn layer(&self, name: &str) -> Option { - Some(VertexView { - graph: self.graph.layer(name)?, - vertex: self.vertex, - }) - } -} - -/// Implementation of the VertexListOps trait for an iterator of VertexView objects. -/// -impl VertexListOps for Box> + Send> { - type Graph = G; - type Vertex = VertexView; - type IterType = Box + Send>; - type EList = Box> + Send>; - type ValueType = T; - - fn earliest_time(self) -> BoxedIter> { - Box::new(self.map(|v| v.start())) - } - - fn latest_time(self) -> BoxedIter> { - Box::new(self.map(|v| v.end().map(|t| t - 1))) - } - - fn window(self, t_start: i64, t_end: i64) -> BoxedIter>> { - Box::new(self.map(move |v| v.window(t_start, t_end))) - } - - fn id(self) -> BoxedIter { - Box::new(self.map(|v| v.id())) - } - - fn name(self) -> BoxedIter { - Box::new(self.map(|v| v.name())) - } - - fn property(self, name: String, include_static: bool) -> BoxedIter> { - Box::new(self.map(move |v| v.property(name.clone(), include_static))) - } - - fn property_history(self, name: String) -> BoxedIter> { - Box::new(self.map(move |v| v.property_history(name.clone()))) - } - - fn properties(self, include_static: bool) -> BoxedIter> { - Box::new(self.map(move |v| v.properties(include_static))) - } - - fn history(self) -> BoxedIter> { - Box::new(self.map(|v| v.history())) - } - - fn property_histories(self) -> BoxedIter>> { - Box::new(self.map(|v| v.property_histories())) - } - - fn property_names(self, include_static: bool) -> BoxedIter> { - Box::new(self.map(move |v| v.property_names(include_static))) - } - - fn has_property(self, name: String, include_static: bool) -> BoxedIter { - Box::new(self.map(move |v| v.has_property(name.clone(), include_static))) - } - - fn has_static_property(self, name: String) -> BoxedIter { - Box::new(self.map(move |v| v.has_static_property(name.clone()))) - } - - fn static_property(self, name: String) -> BoxedIter> { - Box::new(self.map(move |v| v.static_property(name.clone()))) - } - - fn degree(self) -> BoxedIter { - Box::new(self.map(|v| v.degree())) - } - - fn in_degree(self) -> BoxedIter { - Box::new(self.map(|v| v.in_degree())) - } - - fn out_degree(self) -> BoxedIter { - Box::new(self.map(|v| v.out_degree())) - } - - fn edges(self) -> Self::EList { - Box::new(self.flat_map(|v| v.edges())) - } - - fn in_edges(self) -> Self::EList { - Box::new(self.flat_map(|v| v.in_edges())) - } - - fn out_edges(self) -> Self::EList { - Box::new(self.flat_map(|v| v.out_edges())) - } - - fn neighbours(self) -> Self { - Box::new(self.flat_map(|v| v.neighbours())) - } - - fn in_neighbours(self) -> Self { - Box::new(self.flat_map(|v| v.in_neighbours())) - } - - fn out_neighbours(self) -> Self { - Box::new(self.flat_map(|v| v.out_neighbours())) - } -} - -impl VertexListOps for BoxedIter>> { - type Graph = G; - type Vertex = VertexView; - type IterType = BoxedIter>; - type EList = BoxedIter>>; - type ValueType = BoxedIter; - - fn earliest_time(self) -> BoxedIter>> { - Box::new(self.map(|it| it.earliest_time())) - } - - fn latest_time(self) -> BoxedIter>> { - Box::new(self.map(|it| it.latest_time())) - } - - fn window( - self, - t_start: i64, - t_end: i64, - ) -> BoxedIter>>> { - Box::new(self.map(move |it| it.window(t_start, t_end))) - } - - fn id(self) -> BoxedIter> { - Box::new(self.map(|it| it.id())) - } - - fn name(self) -> BoxedIter> { - Box::new(self.map(|it| it.name())) - } - - fn property( - self, - name: String, - include_static: bool, - ) -> BoxedIter>> { - Box::new(self.map(move |it| it.property(name.clone(), include_static))) - } - - fn property_history(self, name: String) -> BoxedIter>> { - Box::new(self.map(move |it| it.property_history(name.clone()))) - } - - fn properties(self, include_static: bool) -> BoxedIter>> { - Box::new(self.map(move |it| it.properties(include_static))) - } - - fn history(self) -> BoxedIter>> { - Box::new(self.map(move |it| it.history())) - } - - fn property_histories(self) -> BoxedIter>>> { - Box::new(self.map(|it| it.property_histories())) - } - - fn property_names(self, include_static: bool) -> BoxedIter>> { - Box::new(self.map(move |it| it.property_names(include_static))) - } - - fn has_property(self, name: String, include_static: bool) -> BoxedIter> { - Box::new(self.map(move |it| it.has_property(name.clone(), include_static))) - } - - fn has_static_property(self, name: String) -> BoxedIter> { - Box::new(self.map(move |it| it.has_static_property(name.clone()))) - } - - fn static_property(self, name: String) -> BoxedIter>> { - Box::new(self.map(move |it| it.static_property(name.clone()))) - } - - fn degree(self) -> BoxedIter> { - Box::new(self.map(|it| it.degree())) - } - - fn in_degree(self) -> BoxedIter> { - Box::new(self.map(|it| it.in_degree())) - } - - fn out_degree(self) -> BoxedIter> { - Box::new(self.map(|it| it.out_degree())) - } - - fn edges(self) -> Self::EList { - Box::new(self.map(|it| it.edges())) - } - - fn in_edges(self) -> Self::EList { - Box::new(self.map(|it| it.in_edges())) - } - - fn out_edges(self) -> Self::EList { - Box::new(self.map(|it| it.out_edges())) - } - - fn neighbours(self) -> Self { - Box::new(self.map(|it| it.neighbours())) - } - - fn in_neighbours(self) -> Self { - Box::new(self.map(|it| it.in_neighbours())) - } - - fn out_neighbours(self) -> Self { - Box::new(self.map(|it| it.out_neighbours())) - } -} - -#[cfg(test)] -mod vertex_test { - use crate::db::graph::Graph; - use crate::db::view_api::*; - - #[test] - fn test_earliest_time() { - let g = Graph::new(4); - g.add_vertex(0, 1, &vec![]).unwrap(); - g.add_vertex(1, 1, &vec![]).unwrap(); - g.add_vertex(2, 1, &vec![]).unwrap(); - let mut view = g.at(1); - assert_eq!(view.vertex(1).expect("v").earliest_time().unwrap(), 0); - assert_eq!(view.vertex(1).expect("v").latest_time().unwrap(), 1); - - view = g.at(3); - assert_eq!(view.vertex(1).expect("v").earliest_time().unwrap(), 0); - assert_eq!(view.vertex(1).expect("v").latest_time().unwrap(), 2); - } -} diff --git a/raphtory/src/db/view_api/edge.rs b/raphtory/src/db/view_api/edge.rs deleted file mode 100644 index 054c32a1e8..0000000000 --- a/raphtory/src/db/view_api/edge.rs +++ /dev/null @@ -1,230 +0,0 @@ -use crate::core::edge_ref::EdgeRef; -use crate::core::vertex_ref::VertexRef; -use crate::core::Prop; -use crate::db::view_api::internal::GraphViewInternalOps; -use crate::db::view_api::{GraphViewOps, VertexListOps, VertexViewOps}; -use std::collections::HashMap; - -pub trait EdgeViewInternalOps> { - fn graph(&self) -> G; - - fn eref(&self) -> EdgeRef; - - fn new_vertex(&self, v: VertexRef) -> V; - - fn new_edge(&self, e: EdgeRef) -> Self; -} - -pub trait EdgeViewOps: EdgeViewInternalOps { - type Graph: GraphViewOps; - type Vertex: VertexViewOps; - type EList: EdgeListOps; - - fn property(&self, name: String, include_static: bool) -> Option { - let props = self.property_history(name.clone()); - match props.last() { - None => { - if include_static { - self.graph().static_edge_prop(self.eref(), name) - } else { - None - } - } - Some((_, prop)) => Some(prop.clone()), - } - } - - fn property_history(&self, name: String) -> Vec<(i64, Prop)> { - match self.eref().time() { - None => self.graph().temporal_edge_props_vec(self.eref(), name), - Some(t) => self.graph().temporal_edge_props_vec_window( - self.eref(), - name, - t, - t.saturating_add(1), - ), - } - } - - fn history(&self) -> Vec { - self.graph().edge_timestamps(self.eref(), None) - } - - fn properties(&self, include_static: bool) -> HashMap { - let mut props: HashMap = self - .property_histories() - .iter() - .map(|(key, values)| (key.clone(), values.last().unwrap().1.clone())) - .collect(); - - if include_static { - for prop_name in self.graph().static_edge_prop_names(self.eref()) { - if let Some(prop) = self - .graph() - .static_edge_prop(self.eref(), prop_name.clone()) - { - props.insert(prop_name, prop); - } - } - } - props - } - - fn property_histories(&self) -> HashMap> { - // match on the self.edge.time option property and run two function s - // one for static and one for temporal - match self.eref().time() { - None => self.graph().temporal_edge_props(self.eref()), - Some(t) => self - .graph() - .temporal_edge_props_window(self.eref(), t, t.saturating_add(1)), - } - } - - fn property_names(&self, include_static: bool) -> Vec { - let mut names: Vec = self.graph().temporal_edge_prop_names(self.eref()); - if include_static { - names.extend(self.graph().static_edge_prop_names(self.eref())) - } - names - } - - fn has_property(&self, name: String, include_static: bool) -> bool { - (!self.property_history(name.clone()).is_empty()) - || (include_static - && self - .graph() - .static_edge_prop_names(self.eref()) - .contains(&name)) - } - - fn has_static_property(&self, name: String) -> bool { - self.graph() - .static_edge_prop_names(self.eref()) - .contains(&name) - } - - fn static_property(&self, name: String) -> Option { - self.graph().static_edge_prop(self.eref(), name) - } - - /// Returns the source vertex of the edge. - fn src(&self) -> Self::Vertex { - let vertex = self.eref().src(); - self.new_vertex(vertex) - } - - /// Returns the destination vertex of the edge. - fn dst(&self) -> Self::Vertex { - let vertex = self.eref().dst(); - self.new_vertex(vertex) - } - - /// Check if edge is active at a given time point - fn active(&self, t: i64) -> bool { - match self.eref().time() { - Some(tt) => tt == t, - None => self.graph().has_edge_ref_window( - self.eref().src(), - self.eref().dst(), - t, - t.saturating_add(1), - self.eref().layer(), - ), - } - } - - fn id( - &self, - ) -> ( - ::ValueType, - ::ValueType, - ) { - (self.src().id(), self.dst().id()) - } - - /// Explodes an edge and returns all instances it had been updated as seperate edges - fn explode(&self) -> Self::EList; - - /// Gets the first time an edge was seen - fn earliest_time(&self) -> Option { - self.eref().time().or_else(|| { - self.graph() - .edge_timestamps(self.eref(), None) - .first() - .copied() - }) - } - - /// Gets the latest time an edge was updated - fn latest_time(&self) -> Option { - self.eref().time().or_else(|| { - self.graph() - .edge_timestamps(self.eref(), None) - .last() - .copied() - }) - } - - /// Gets the time stamp of the edge if it is exploded - fn time(&self) -> Option { - self.eref().time() - } - - /// Gets the name of the layer this edge belongs to - fn layer_name(&self) -> String { - if self.eref().layer() == 0 { - "default layer".to_string() - } else { - self.graph().get_layer_name_by_id(self.eref().layer()) - } - } -} - -/// This trait defines the operations that can be -/// performed on a list of edges in a temporal graph view. -pub trait EdgeListOps: - IntoIterator, IntoIter = Self::IterType> + Sized -{ - type Graph: GraphViewOps; - type Vertex: VertexViewOps; - type Edge: EdgeViewOps; - type ValueType; - - /// the type of list of vertices - type VList: VertexListOps; - - /// the type of iterator - type IterType: Iterator>; - - fn has_property(self, name: String, include_static: bool) -> Self::IterType; - - fn property(self, name: String, include_static: bool) -> Self::IterType>; - fn properties(self, include_static: bool) -> Self::IterType>; - fn property_names(self, include_static: bool) -> Self::IterType>; - - fn has_static_property(self, name: String) -> Self::IterType; - fn static_property(self, name: String) -> Self::IterType>; - - /// gets a property of an edge with the given name - /// includes the timestamp of the property - fn property_history(self, name: String) -> Self::IterType>; - fn property_histories(self) -> Self::IterType>>; - - /// gets the source vertices of the edges in the list - fn src(self) -> Self::VList; - - /// gets the destination vertices of the edges in the list - fn dst(self) -> Self::VList; - - fn id(self) -> Self::IterType<(u64, u64)>; - - /// returns a list of exploded edges that include an edge at each point in time - fn explode(self) -> Self::IterType; - - /// Get the timestamp for the earliest activity of the edge - fn earliest_time(self) -> Self::IterType>; - - /// Get the timestamp for the latest activity of the edge - fn latest_time(self) -> Self::IterType>; -} diff --git a/raphtory/src/db/view_api/graph.rs b/raphtory/src/db/view_api/graph.rs deleted file mode 100644 index 11c41920be..0000000000 --- a/raphtory/src/db/view_api/graph.rs +++ /dev/null @@ -1,176 +0,0 @@ -use itertools::Itertools; -use rustc_hash::FxHashSet; - -use crate::core::time::IntoTime; -use crate::core::vertex_ref::{LocalVertexRef, VertexRef}; -use crate::db::edge::EdgeView; -use crate::db::graph_layer::LayeredGraph; -use crate::db::graph_window::WindowedGraph; -use crate::db::subgraph_vertex::VertexSubgraph; -use crate::db::vertex::VertexView; -use crate::db::vertices::Vertices; -use crate::db::view_api::internal::GraphViewInternalOps; -use crate::db::view_api::layer::LayerOps; -use crate::db::view_api::time::TimeOps; -use crate::db::view_api::VertexViewOps; - -/// This trait GraphViewOps defines operations for accessing -/// information about a graph. The trait has associated types -/// that are used to define the type of the vertices, edges -/// and the corresponding iterators. -pub trait GraphViewOps: Send + Sync + Sized + GraphViewInternalOps + 'static + Clone { - fn subgraph, V: Into>( - &self, - vertices: I, - ) -> VertexSubgraph; - fn get_unique_layers(&self) -> Vec; - /// Timestamp of earliest activity in the graph - fn earliest_time(&self) -> Option; - /// Timestamp of latest activity in the graph - fn latest_time(&self) -> Option; - /// Return the number of vertices in the graph. - fn num_vertices(&self) -> usize; - - /// Check if the graph is empty. - fn is_empty(&self) -> bool { - self.num_vertices() == 0 - } - - /// Return the number of edges in the graph. - fn num_edges(&self) -> usize; - - /// Check if the graph contains a vertex `v`. - fn has_vertex>(&self, v: T) -> bool; - - /// Check if the graph contains an edge given a pair of vertices `(src, dst)`. - fn has_edge>(&self, src: T, dst: T, layer: Option<&str>) -> bool; - - /// Get a vertex `v`. - fn vertex>(&self, v: T) -> Option>; - - /// Return a View of the vertices in the Graph - fn vertices(&self) -> Vertices; - - /// Get an edge `(src, dst)`. - fn edge>( - &self, - src: T, - dst: T, - layer: Option<&str>, - ) -> Option>; - - /// Return an iterator over all edges in the graph. - fn edges(&self) -> Box> + Send>; -} - -impl GraphViewOps for G { - fn subgraph, V: Into>( - &self, - vertices: I, - ) -> VertexSubgraph { - let vertices: FxHashSet = vertices - .into_iter() - .flat_map(|v| self.local_vertex(v.into())) - .collect(); - VertexSubgraph::new(self.clone(), vertices) - } - - fn get_unique_layers(&self) -> Vec { - self.get_unique_layers_internal() - .into_iter() - .filter(|id| *id != 0) // the default layer has no name - .map(|id| self.get_layer_name_by_id(id)) - .collect_vec() - } - - fn earliest_time(&self) -> Option { - self.earliest_time_global() - } - - fn latest_time(&self) -> Option { - self.latest_time_global() - } - - fn num_vertices(&self) -> usize { - self.vertices_len() - } - - fn num_edges(&self) -> usize { - self.edges_len(None) - } - - fn has_vertex>(&self, v: T) -> bool { - self.has_vertex_ref(v.into()) - } - - fn has_edge>(&self, src: T, dst: T, layer: Option<&str>) -> bool { - match self.get_layer(layer) { - Some(layer_id) => self.has_edge_ref(src.into(), dst.into(), layer_id), - None => false, - } - } - - fn vertex>(&self, v: T) -> Option> { - let v = v.into(); - self.local_vertex(v) - .map(|v| VertexView::new_local(self.clone(), v)) - } - - fn vertices(&self) -> Vertices { - let graph = self.clone(); - Vertices::new(graph) - } - - fn edge>( - &self, - src: T, - dst: T, - layer: Option<&str>, - ) -> Option> { - let layer_id = match layer { - Some(_) => self.get_layer(layer)?, - None => { - let layers = self.get_unique_layers_internal(); - match layers[..] { - [layer_id] => layer_id, // if only one layer we search the edge there - _ => 0, // if more than one, we point to the default one - } - } - }; - self.edge_ref(src.into(), dst.into(), layer_id) - .map(|e| EdgeView::new(self.clone(), e)) - } - - fn edges(&self) -> Box> + Send> { - Box::new(self.vertices().iter().flat_map(|v| v.out_edges())) - } -} - -impl TimeOps for G { - type WindowedViewType = WindowedGraph; - - fn start(&self) -> Option { - self.view_start() - } - - fn end(&self) -> Option { - self.view_end() - } - - fn window(&self, t_start: T, t_end: T) -> WindowedGraph { - WindowedGraph::new(self.clone(), t_start, t_end) - } -} - -impl LayerOps for G { - type LayeredViewType = LayeredGraph; - - fn default_layer(&self) -> Self::LayeredViewType { - LayeredGraph::new(self.clone(), 0) - } - - fn layer(&self, name: &str) -> Option { - let id = self.get_layer(Some(name))?; - Some(LayeredGraph::new(self.clone(), id)) - } -} diff --git a/raphtory/src/db/view_api/internal.rs b/raphtory/src/db/view_api/internal.rs deleted file mode 100644 index b484bba887..0000000000 --- a/raphtory/src/db/view_api/internal.rs +++ /dev/null @@ -1,1014 +0,0 @@ -use crate::core::edge_ref::EdgeRef; -use crate::core::vertex_ref::{LocalVertexRef, VertexRef}; -use crate::core::{Direction, Prop}; -use std::collections::HashMap; -use std::ops::Range; - -/// The GraphViewInternalOps trait provides a set of methods to query a directed graph -/// represented by the raphtory_core::tgraph::TGraph struct. -pub trait GraphViewInternalOps { - /// Gets the local reference for a remote vertex and keeps local references unchanged. Assumes vertex exists! - fn localise_vertex_unchecked(&self, v: VertexRef) -> LocalVertexRef { - match v { - VertexRef::Local(v) => v, - VertexRef::Remote(g_id) => self.vertex_ref(g_id).expect("Vertex should already exists"), - } - } - - /// Check if a vertex exists locally and returns local reference. - fn local_vertex(&self, v: VertexRef) -> Option; - - /// Check if a vertex exists locally in the window and returns local reference. - fn local_vertex_window(&self, v: VertexRef, t_start: i64, t_end: i64) - -> Option; - - fn get_unique_layers_internal(&self) -> Vec; - - fn get_layer_name_by_id(&self, layer_id: usize) -> String; - - /// Get the layer id for the given layer name - fn get_layer(&self, key: Option<&str>) -> Option; - - /// Returns the default start time for perspectives over the view - fn view_start(&self) -> Option; - - /// Returns the default end time for perspectives over the view - fn view_end(&self) -> Option; - - /// Returns the timestamp for the earliest activity - fn earliest_time_global(&self) -> Option; - - /// Returns the timestamp for the earliest activity in the window - fn earliest_time_window(&self, t_start: i64, t_end: i64) -> Option; - - /// Returns the timestamp for the latest activity - fn latest_time_global(&self) -> Option; - - /// Returns the timestamp for the latest activity in the window - fn latest_time_window(&self, t_start: i64, t_end: i64) -> Option; - - /// Returns the total number of vertices in the graph. - fn vertices_len(&self) -> usize; - - /// Returns the number of vertices in the graph that were created between - /// the start (t_start) and end (t_end) timestamps (inclusive). - /// # Arguments - /// - /// * `t_start` - The start time of the window (inclusive). - /// * `t_end` - The end time of the window (exclusive). - fn vertices_len_window(&self, t_start: i64, t_end: i64) -> usize; - - /// Returns the total number of edges in the graph. - fn edges_len(&self, layer: Option) -> usize; - - /// Returns the number of edges in the graph that were created between the - /// start (t_start) and end (t_end) timestamps (inclusive). - /// # Arguments - /// - /// * `t_start` - The start time of the window (inclusive). - /// * `t_end` - The end time of the window (exclusive). - fn edges_len_window(&self, t_start: i64, t_end: i64, layer: Option) -> usize; - - /// Returns true if the graph contains an edge between the source vertex - /// (src) and the destination vertex (dst). - /// # Arguments - /// - /// * `src` - The source vertex of the edge. - /// * `dst` - The destination vertex of the edge. - fn has_edge_ref(&self, src: VertexRef, dst: VertexRef, layer: usize) -> bool; - - /// Returns true if the graph contains an edge between the source vertex (src) and the - /// destination vertex (dst) created between the start (t_start) and end (t_end) timestamps - /// (inclusive). - /// # Arguments - /// - /// * `t_start` - The start time of the window (inclusive). - /// * `t_end` - The end time of the window (exclusive). - /// * `src` - The source vertex of the edge. - /// * `dst` - The destination vertex of the edge. - fn has_edge_ref_window( - &self, - src: VertexRef, - dst: VertexRef, - t_start: i64, - t_end: i64, - layer: usize, - ) -> bool; - - /// Returns true if the graph contains the specified vertex (v). - /// # Arguments - /// - /// * `v` - VertexRef of the vertex to check. - fn has_vertex_ref(&self, v: VertexRef) -> bool; - - /// Returns true if the graph contains the specified vertex (v) created between the - /// start (t_start) and end (t_end) timestamps (inclusive). - /// # Arguments - /// - /// * `v` - VertexRef of the vertex to check. - /// * `t_start` - The start time of the window (inclusive). - /// * `t_end` - The end time of the window (exclusive). - fn has_vertex_ref_window(&self, v: VertexRef, t_start: i64, t_end: i64) -> bool; - - /// Returns the number of edges that point towards or from the specified vertex - /// (v) based on the direction (d). - /// # Arguments - /// - /// * `v` - LocalVertexRef of the vertex to check. - /// * `d` - Direction of the edges to count. - fn degree(&self, v: LocalVertexRef, d: Direction, layer: Option) -> usize; - - /// Returns the number of edges that point towards or from the specified vertex (v) - /// created between the start (t_start) and end (t_end) timestamps (inclusive) based - /// on the direction (d). - /// # Arguments - /// - /// * `v` - LocalVertexRef of the vertex to check. - /// * `t_start` - The start time of the window (inclusive). - /// * `t_end` - The end time of the window (exclusive). - fn degree_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - d: Direction, - layer: Option, - ) -> usize; - - /// Returns the LocalVertexRef that corresponds to the specified vertex ID (v). - /// Returns None if the vertex ID is not present in the graph. - /// # Arguments - /// - /// * `v` - The vertex ID to lookup. - fn vertex_ref(&self, v: u64) -> Option; - - /// Returns the global ID for a vertex - fn vertex_id(&self, v: LocalVertexRef) -> u64; - - /// Returns the string name for a vertex - fn vertex_name(&self, v: LocalVertexRef) -> String { - match self.static_vertex_prop(v, "_id".to_string()) { - None => self.vertex_id(v).to_string(), - Some(prop) => prop.to_string(), - } - } - - /// Returns the LocalVertexRef that corresponds to the specified vertex ID (v) created - /// between the start (t_start) and end (t_end) timestamps (inclusive). - /// Returns None if the vertex ID is not present in the graph. - /// # Arguments - /// - /// * `v` - The vertex ID to lookup. - /// * `t_start` - The start time of the window (inclusive). - /// * `t_end` - The end time of the window (exclusive). - /// - /// # Returns - /// * `Option` - The LocalVertexRef of the vertex if it exists in the graph. - fn vertex_ref_window(&self, v: u64, t_start: i64, t_end: i64) -> Option; - - /// Return the earliest time for a vertex - fn vertex_earliest_time(&self, v: LocalVertexRef) -> Option; - - /// Return the earliest time for a vertex in a window - fn vertex_earliest_time_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - ) -> Option; - - /// Return the latest time for a vertex - fn vertex_latest_time(&self, v: LocalVertexRef) -> Option; - - /// Return the latest time for a vertex in a window - fn vertex_latest_time_window(&self, v: LocalVertexRef, t_start: i64, t_end: i64) - -> Option; - - /// Returns all the vertex references in the graph. - /// # Returns - /// * `Box + Send>` - An iterator over all the vertex - /// references in the graph. - fn vertex_refs(&self) -> Box + Send>; - - /// Returns all the vertex references in the graph created between the start (t_start) and - /// end (t_end) timestamps (inclusive). - /// # Arguments - /// - /// * `t_start` - The start time of the window (inclusive). - /// * `t_end` - The end time of the window (exclusive). - /// - /// # Returns - /// * `Box + Send>` - An iterator over all the vertexes - fn vertex_refs_window( - &self, - t_start: i64, - t_end: i64, - ) -> Box + Send>; - - fn vertex_refs_shard(&self, shard: usize) -> Box + Send>; - - /// Returns all the vertex references in the graph that are in the specified shard. - /// Between the start (t_start) and end (t_end) - /// - /// # Arguments - /// shard - The shard to return the vertex references for. - /// t_start - The start time of the window (inclusive). - /// t_end - The end time of the window (exclusive). - /// - /// # Returns - /// * `Box + Send>` - An iterator over all the vertexes - fn vertex_refs_window_shard( - &self, - shard: usize, - t_start: i64, - t_end: i64, - ) -> Box + Send>; - - /// Returns the edge reference that corresponds to the specified src and dst vertex - /// # Arguments - /// - /// * `src` - The source vertex. - /// * `dst` - The destination vertex. - /// - /// # Returns - /// - /// * `Option` - The edge reference if it exists. - fn edge_ref(&self, src: VertexRef, dst: VertexRef, layer: usize) -> Option; - - /// Returns the edge reference that corresponds to the specified src and dst vertex - /// created between the start (t_start) and end (t_end) timestamps (exclusive). - /// - /// # Arguments - /// - /// * `src` - The source vertex. - /// * `dst` - The destination vertex. - /// * `t_start` - The start time of the window (inclusive). - /// * `t_end` - The end time of the window (exclusive). - /// - /// # Returns - /// - /// * `Option` - The edge reference if it exists. - fn edge_ref_window( - &self, - src: VertexRef, - dst: VertexRef, - t_start: i64, - t_end: i64, - layer: usize, - ) -> Option; - - /// Returns all the edge references in the graph. - /// - /// # Returns - /// - /// * `Box + Send>` - An iterator over all the edge references. - fn edge_refs(&self, layer: Option) -> Box + Send>; - - /// Returns all the edge references in the graph created between the start (t_start) and - /// end (t_end) timestamps (inclusive). - /// - /// # Arguments - /// - /// * `t_start` - The start time of the window (inclusive). - /// * `t_end` - The end time of the window (exclusive). - /// # Returns - /// - /// * `Box + Send>` - An iterator over all the edge references. - fn edge_refs_window( - &self, - t_start: i64, - t_end: i64, - layer: Option, - ) -> Box + Send>; - - /// Returns an iterator over the edges connected to a given vertex in a given direction. - /// - /// # Arguments - /// - /// * `v` - A reference to the vertex for which the edges are being queried. - /// * `d` - The direction in which to search for edges. - /// * `layer` - The optional layer to consider - /// - /// # Returns - /// - /// Box + Send> - A boxed iterator that yields references to - /// the edges connected to the vertex. - fn vertex_edges( - &self, - v: LocalVertexRef, - d: Direction, - layer: Option, - ) -> Box + Send>; - - /// Returns an iterator over the exploded edges connected to a given vertex in a given direction. - /// - /// # Arguments - /// - /// * `v` - A reference to the vertex for which the edges are being queried. - /// * `d` - The direction in which to search for edges. - /// - /// # Returns - /// - /// Box + Send> - A boxed iterator that yields references to - /// the edges connected to the vertex. - fn vertex_edges_t( - &self, - v: LocalVertexRef, - d: Direction, - layer: Option, - ) -> Box + Send>; - - /// Returns an iterator over the edges connected to a given vertex within a - /// specified time window in a given direction. - /// - /// # Arguments - /// - /// * `v` - A reference to the vertex for which the edges are being queried. - /// * `t_start` - The start time of the window (inclusive). - /// * `t_end` - The end time of the window (exclusive). - /// * `d` - The direction in which to search for edges. - /// - /// # Returns - /// - /// Box + Send> - A boxed iterator that yields references - /// to the edges connected to the vertex within the specified time window. - fn vertex_edges_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - d: Direction, - layer: Option, - ) -> Box + Send>; - - /// Returns an iterator over the edges connected to a given vertex within - /// a specified time window in a given direction but exploded. - /// - /// # Arguments - /// - /// * `v` - A reference to the vertex for which the edges are being queried. - /// * `t_start` - The start time of the window (inclusive). - /// * `t_end` - The end time of the window (exclusive). - /// * `d` - The direction in which to search for edges. - /// - /// # Returns - /// - /// A boxed iterator that yields references to the edges connected to the vertex - /// within the specified time window but exploded. - fn vertex_edges_window_t( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - d: Direction, - layer: Option, - ) -> Box + Send>; - - /// Returns an iterator over the neighbors of a given vertex in a given direction. - /// - /// # Arguments - /// - /// * `v` - A reference to the vertex for which the neighbors are being queried. - /// * `d` - The direction in which to search for neighbors. - /// - /// # Returns - /// - /// A boxed iterator that yields references to the neighboring vertices. - fn neighbours( - &self, - v: LocalVertexRef, - d: Direction, - layer: Option, - ) -> Box + Send>; - - /// Returns an iterator over the neighbors of a given vertex within a specified time window in a given direction. - /// - /// # Arguments - /// - /// * `v` - A reference to the vertex for which the neighbors are being queried. - /// * `t_start` - The start time of the window (inclusive). - /// * `t_end` - The end time of the window (exclusive). - /// * `d` - The direction in which to search for neighbors. - /// - /// # Returns - /// - /// A boxed iterator that yields references to the neighboring vertices within the specified time window. - fn neighbours_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - d: Direction, - layer: Option, - ) -> Box + Send>; - - /// Gets a static property of a given vertex given the name and vertex reference. - /// - /// # Arguments - /// - /// * `v` - A reference to the vertex for which the property is being queried. - /// * `name` - The name of the property. - /// - /// # Returns - /// - /// Option - The property value if it exists. - fn static_vertex_prop(&self, v: LocalVertexRef, name: String) -> Option; - - /// Gets the keys of static properties of a given vertex - /// - /// # Arguments - /// - /// * `v` - A reference to the vertex for which the property is being queried. - /// - /// # Returns - /// - /// Vec - The keys of the static properties. - fn static_vertex_prop_names(&self, v: LocalVertexRef) -> Vec; - - /// Returns a vector of all names of temporal properties within the given vertex - /// - /// # Arguments - /// - /// * `v` - A reference to the vertex for which to retrieve the names. - /// - /// # Returns - /// - /// A vector of strings representing the names of the temporal properties - fn temporal_vertex_prop_names(&self, v: LocalVertexRef) -> Vec; - - /// Returns a vector of all temporal values of the vertex property with the given name for the - /// given vertex - /// - /// # Arguments - /// - /// * `v` - A reference to the vertex for which to retrieve the temporal property vector. - /// * `name` - The name of the property to retrieve. - /// - /// # Returns - /// - /// A vector of tuples representing the temporal values of the property for the given vertex - /// that fall within the specified time window, where the first element of each tuple is the timestamp - /// and the second element is the property value. - fn temporal_vertex_prop_vec(&self, v: LocalVertexRef, name: String) -> Vec<(i64, Prop)>; - - /// Returns a vector of all temporal values of the vertex - /// - /// # Arguments - /// - /// * `v` - A reference to the vertex for which to retrieve the timestamp. - /// - /// # Returns - /// - /// A vector of timestamps representing the temporal values for the given vertex. - fn vertex_timestamps(&self, v: LocalVertexRef) -> Vec; - - /// Returns a vector of all temporal values of the vertex for a given window. - /// - /// # Arguments - /// - /// * `v` - A reference to the vertex for which to retrieve the timestamp. - /// * `t_start` - The start time of the window. - /// * `t_end` - The end time of the window. - /// - /// # Returns - /// - /// A vector of timestamps representing the temporal values for the given vertex in a given window. - fn vertex_timestamps_window(&self, v: LocalVertexRef, t_start: i64, t_end: i64) -> Vec; - - /// Returns a vector of all temporal values of the vertex property with the given name for the given vertex - /// that fall within the specified time window. - /// - /// # Arguments - /// - /// * `v` - A reference to the vertex for which to retrieve the temporal property vector. - /// * `name` - The name of the property to retrieve. - /// * `t_start` - The start time of the window to consider. - /// * `t_end` - The end time of the window to consider. - /// - /// # Returns - /// - /// A vector of tuples representing the temporal values of the property for the given vertex - /// that fall within the specified time window, where the first element of each tuple is the timestamp - /// and the second element is the property value. - fn temporal_vertex_prop_vec_window( - &self, - v: LocalVertexRef, - name: String, - t_start: i64, - t_end: i64, - ) -> Vec<(i64, Prop)>; - - /// Returns a map of all temporal values of the vertex properties for the given vertex. - /// The keys of the map are the names of the properties, and the values are vectors of tuples - /// - /// # Arguments - /// - /// - `v` - A reference to the vertex for which to retrieve the temporal property vector. - /// - /// # Returns - /// - A map of all temporal values of the vertex properties for the given vertex. - fn temporal_vertex_props(&self, v: LocalVertexRef) -> HashMap>; - - /// Returns a map of all temporal values of the vertex properties for the given vertex - /// that fall within the specified time window. - /// - /// # Arguments - /// - /// - `v` - A reference to the vertex for which to retrieve the temporal property vector. - /// - `t_start` - The start time of the window to consider (inclusive). - /// - `t_end` - The end time of the window to consider (exclusive). - /// - /// # Returns - /// - A map of all temporal values of the vertex properties for the given vertex - fn temporal_vertex_props_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - ) -> HashMap>; - - /// Returns a vector of all temporal values of the edge property with the given name for the - /// given edge reference. - /// - /// # Arguments - /// - /// * `e` - An `EdgeRef` reference to the edge of interest. - /// * `name` - A `String` containing the name of the temporal property. - /// - /// # Returns - /// - /// A property if it exists - fn static_edge_prop(&self, e: EdgeRef, name: String) -> Option; - - /// Returns a vector of keys for the static properties of the given edge reference. - /// - /// # Arguments - /// - /// * `e` - An `EdgeRef` reference to the edge of interest. - /// - /// # Returns - /// - /// * A `Vec` of `String` containing the keys for the static properties of the given edge. - fn static_edge_prop_names(&self, e: EdgeRef) -> Vec; - - /// Returns a vector of keys for the temporal properties of the given edge reference. - /// - /// # Arguments - /// - /// * `e` - An `EdgeRef` reference to the edge of interest. - /// - /// # Returns - /// - /// * A `Vec` of `String` containing the keys for the temporal properties of the given edge. - fn temporal_edge_prop_names(&self, e: EdgeRef) -> Vec; - - /// Returns a vector of tuples containing the values of the temporal property with the given name - /// for the given edge reference. - /// - /// # Arguments - /// - /// * `e` - An `EdgeRef` reference to the edge of interest. - /// * `name` - A `String` containing the name of the temporal property. - /// - /// # Returns - /// - /// * A `Vec` of tuples containing the values of the temporal property with the given name for the given edge. - fn temporal_edge_props_vec(&self, e: EdgeRef, name: String) -> Vec<(i64, Prop)>; - - /// Returns a vector of tuples containing the values of the temporal property with the given name - /// for the given edge reference within the specified time window. - /// - /// # Arguments - /// - /// * `e` - An `EdgeRef` reference to the edge of interest. - /// * `name` - A `String` containing the name of the temporal property. - /// * `t_start` - An `i64` containing the start time of the time window (inclusive). - /// * `t_end` - An `i64` containing the end time of the time window (exclusive). - /// - /// # Returns - /// - /// * A `Vec` of tuples containing the values of the temporal property with the given name for the given edge - /// within the specified time window. - /// - fn temporal_edge_props_vec_window( - &self, - e: EdgeRef, - name: String, - t_start: i64, - t_end: i64, - ) -> Vec<(i64, Prop)>; - - fn edge_timestamps(&self, e: EdgeRef, window: Option>) -> Vec; - - /// Returns a hash map containing all the temporal properties of the given edge reference, - /// where each key is the name of a temporal property and each value is a vector of tuples containing - /// the property value and the time it was recorded. - /// - /// # Arguments - /// - /// * `e` - An `EdgeRef` reference to the edge. - /// - /// # Returns - /// - /// * A `HashMap` containing all the temporal properties of the given edge, where each key is the name of a - /// temporal property and each value is a vector of tuples containing the property value and the time it was recorded. - /// - fn temporal_edge_props(&self, e: EdgeRef) -> HashMap>; - - /// Returns a hash map containing all the temporal properties of the given edge reference within the specified - /// time window, where each key is the name of a temporal property and each value is a vector of tuples containing - /// the property value and the time it was recorded. - /// - /// # Arguments - /// - /// * `e` - An `EdgeRef` reference to the edge. - /// * `t_start` - An `i64` containing the start time of the time window (inclusive). - /// * `t_end` - An `i64` containing the end time of the time window (exclusive). - /// - /// # Returns - /// - /// * A `HashMap` containing all the temporal properties of the given edge within the specified time window, - /// where each key is the name of a temporal property and each value is a vector of tuples containing the property - /// value and the time it was recorded. - /// - fn temporal_edge_props_window( - &self, - e: EdgeRef, - t_start: i64, - t_end: i64, - ) -> HashMap>; - - fn num_shards(&self) -> usize; -} - -pub trait WrappedGraph { - type Internal: GraphViewInternalOps + Send + Sync + 'static + ?Sized; - - fn as_graph(&self) -> &Self::Internal; -} - -/// Helper trait for various graphs that just delegate to the internal graph -/// -impl GraphViewInternalOps for G -where - G: WrappedGraph, -{ - fn local_vertex(&self, v: VertexRef) -> Option { - self.as_graph().local_vertex(v) - } - - fn local_vertex_window( - &self, - v: VertexRef, - t_start: i64, - t_end: i64, - ) -> Option { - self.as_graph().local_vertex_window(v, t_start, t_end) - } - - fn get_unique_layers_internal(&self) -> Vec { - self.as_graph().get_unique_layers_internal() - } - - fn get_layer_name_by_id(&self, layer_id: usize) -> String { - self.as_graph().get_layer_name_by_id(layer_id) - } - - fn get_layer(&self, key: Option<&str>) -> Option { - self.as_graph().get_layer(key) - } - - fn view_start(&self) -> Option { - self.as_graph().view_start() - } - - fn view_end(&self) -> Option { - self.as_graph().view_end() - } - - fn earliest_time_global(&self) -> Option { - self.as_graph().earliest_time_global() - } - - fn earliest_time_window(&self, t_start: i64, t_end: i64) -> Option { - self.as_graph().earliest_time_window(t_start, t_end) - } - - fn latest_time_global(&self) -> Option { - self.as_graph().latest_time_global() - } - - fn latest_time_window(&self, t_start: i64, t_end: i64) -> Option { - self.as_graph().latest_time_window(t_start, t_end) - } - - fn vertices_len(&self) -> usize { - self.as_graph().vertices_len() - } - - fn vertices_len_window(&self, t_start: i64, t_end: i64) -> usize { - self.as_graph().vertices_len_window(t_start, t_end) - } - - fn edges_len(&self, layer: Option) -> usize { - self.as_graph().edges_len(layer) - } - - fn edges_len_window(&self, t_start: i64, t_end: i64, layer: Option) -> usize { - self.as_graph().edges_len_window(t_start, t_end, layer) - } - - fn has_edge_ref(&self, src: VertexRef, dst: VertexRef, layer: usize) -> bool { - self.as_graph().has_edge_ref(src, dst, layer) - } - - fn has_edge_ref_window( - &self, - src: VertexRef, - dst: VertexRef, - t_start: i64, - t_end: i64, - layer: usize, - ) -> bool { - self.as_graph() - .has_edge_ref_window(src, dst, t_start, t_end, layer) - } - - fn has_vertex_ref(&self, v: VertexRef) -> bool { - self.as_graph().has_vertex_ref(v) - } - - fn has_vertex_ref_window(&self, v: VertexRef, t_start: i64, t_end: i64) -> bool { - self.as_graph().has_vertex_ref_window(v, t_start, t_end) - } - - fn degree(&self, v: LocalVertexRef, d: Direction, layer: Option) -> usize { - self.as_graph().degree(v, d, layer) - } - - fn degree_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - d: Direction, - layer: Option, - ) -> usize { - self.as_graph().degree_window(v, t_start, t_end, d, layer) - } - - fn vertex_ref(&self, v: u64) -> Option { - self.as_graph().vertex_ref(v) - } - - fn vertex_id(&self, v: LocalVertexRef) -> u64 { - self.as_graph().vertex_id(v) - } - - fn vertex_ref_window(&self, v: u64, t_start: i64, t_end: i64) -> Option { - self.as_graph().vertex_ref_window(v, t_start, t_end) - } - - fn vertex_earliest_time(&self, v: LocalVertexRef) -> Option { - self.as_graph().vertex_earliest_time(v) - } - - fn vertex_earliest_time_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - ) -> Option { - self.as_graph() - .vertex_earliest_time_window(v, t_start, t_end) - } - - fn vertex_latest_time(&self, v: LocalVertexRef) -> Option { - self.as_graph().vertex_latest_time(v) - } - - fn vertex_latest_time_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - ) -> Option { - self.as_graph().vertex_latest_time_window(v, t_start, t_end) - } - - fn vertex_refs(&self) -> Box + Send> { - self.as_graph().vertex_refs() - } - - fn vertex_refs_window( - &self, - t_start: i64, - t_end: i64, - ) -> Box + Send> { - self.as_graph().vertex_refs_window(t_start, t_end) - } - - fn vertex_refs_shard(&self, shard: usize) -> Box + Send> { - self.as_graph().vertex_refs_shard(shard) - } - - fn vertex_refs_window_shard( - &self, - shard: usize, - t_start: i64, - t_end: i64, - ) -> Box + Send> { - self.as_graph() - .vertex_refs_window_shard(shard, t_start, t_end) - } - - fn edge_ref(&self, src: VertexRef, dst: VertexRef, layer: usize) -> Option { - self.as_graph().edge_ref(src, dst, layer) - } - - fn edge_ref_window( - &self, - src: VertexRef, - dst: VertexRef, - t_start: i64, - t_end: i64, - layer: usize, - ) -> Option { - self.as_graph() - .edge_ref_window(src, dst, t_start, t_end, layer) - } - - fn edge_refs(&self, layer: Option) -> Box + Send> { - self.as_graph().edge_refs(layer) - } - - fn edge_refs_window( - &self, - t_start: i64, - t_end: i64, - layer: Option, - ) -> Box + Send> { - self.as_graph().edge_refs_window(t_start, t_end, layer) - } - - fn vertex_edges( - &self, - v: LocalVertexRef, - d: Direction, - layer: Option, - ) -> Box + Send> { - self.as_graph().vertex_edges(v, d, layer) - } - - fn vertex_edges_t( - &self, - v: LocalVertexRef, - d: Direction, - layer: Option, - ) -> Box + Send> { - self.as_graph().vertex_edges_t(v, d, layer) - } - - fn vertex_edges_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - d: Direction, - layer: Option, - ) -> Box + Send> { - self.as_graph() - .vertex_edges_window(v, t_start, t_end, d, layer) - } - - fn vertex_edges_window_t( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - d: Direction, - layer: Option, - ) -> Box + Send> { - self.as_graph() - .vertex_edges_window_t(v, t_start, t_end, d, layer) - } - - fn neighbours( - &self, - v: LocalVertexRef, - d: Direction, - layer: Option, - ) -> Box + Send> { - self.as_graph().neighbours(v, d, layer) - } - - fn neighbours_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - d: Direction, - layer: Option, - ) -> Box + Send> { - self.as_graph() - .neighbours_window(v, t_start, t_end, d, layer) - } - - fn static_vertex_prop(&self, v: LocalVertexRef, name: String) -> Option { - self.as_graph().static_vertex_prop(v, name) - } - - fn static_vertex_prop_names(&self, v: LocalVertexRef) -> Vec { - self.as_graph().static_vertex_prop_names(v) - } - - fn temporal_vertex_prop_names(&self, v: LocalVertexRef) -> Vec { - self.as_graph().temporal_vertex_prop_names(v) - } - - fn temporal_vertex_prop_vec(&self, v: LocalVertexRef, name: String) -> Vec<(i64, Prop)> { - self.as_graph().temporal_vertex_prop_vec(v, name) - } - - fn vertex_timestamps(&self, v: LocalVertexRef) -> Vec { - self.as_graph().vertex_timestamps(v) - } - - fn vertex_timestamps_window(&self, v: LocalVertexRef, t_start: i64, t_end: i64) -> Vec { - self.as_graph().vertex_timestamps_window(v, t_start, t_end) - } - - fn temporal_vertex_prop_vec_window( - &self, - v: LocalVertexRef, - name: String, - t_start: i64, - t_end: i64, - ) -> Vec<(i64, Prop)> { - self.as_graph() - .temporal_vertex_prop_vec_window(v, name, t_start, t_end) - } - - fn temporal_vertex_props(&self, v: LocalVertexRef) -> HashMap> { - self.as_graph().temporal_vertex_props(v) - } - - fn temporal_vertex_props_window( - &self, - v: LocalVertexRef, - t_start: i64, - t_end: i64, - ) -> HashMap> { - self.as_graph() - .temporal_vertex_props_window(v, t_start, t_end) - } - - fn static_edge_prop(&self, e: EdgeRef, name: String) -> Option { - self.as_graph().static_edge_prop(e, name) - } - - fn static_edge_prop_names(&self, e: EdgeRef) -> Vec { - self.as_graph().static_edge_prop_names(e) - } - - fn temporal_edge_prop_names(&self, e: EdgeRef) -> Vec { - self.as_graph().temporal_edge_prop_names(e) - } - - fn temporal_edge_props_vec(&self, e: EdgeRef, name: String) -> Vec<(i64, Prop)> { - self.as_graph().temporal_edge_props_vec(e, name) - } - - fn temporal_edge_props_vec_window( - &self, - e: EdgeRef, - name: String, - t_start: i64, - t_end: i64, - ) -> Vec<(i64, Prop)> { - self.as_graph() - .temporal_edge_props_vec_window(e, name, t_start, t_end) - } - - fn edge_timestamps(&self, e: EdgeRef, window: Option>) -> Vec { - self.as_graph().edge_timestamps(e, window) - } - - fn temporal_edge_props(&self, e: EdgeRef) -> HashMap> { - self.as_graph().temporal_edge_props(e) - } - - fn temporal_edge_props_window( - &self, - e: EdgeRef, - t_start: i64, - t_end: i64, - ) -> HashMap> { - self.as_graph() - .temporal_edge_props_window(e, t_start, t_end) - } - - fn num_shards(&self) -> usize { - self.as_graph().num_shards() - } -} diff --git a/raphtory/src/db/view_api/layer.rs b/raphtory/src/db/view_api/layer.rs deleted file mode 100644 index 409af1e58d..0000000000 --- a/raphtory/src/db/view_api/layer.rs +++ /dev/null @@ -1,10 +0,0 @@ -/// Trait defining layer operations -pub trait LayerOps { - type LayeredViewType; - - /// Return a graph containing only the default edge layer - fn default_layer(&self) -> Self::LayeredViewType; - - /// Return a graph containing the layer `name` - fn layer(&self, name: &str) -> Option; -} diff --git a/raphtory/src/db/view_api/mod.rs b/raphtory/src/db/view_api/mod.rs deleted file mode 100644 index 7a61e4976c..0000000000 --- a/raphtory/src/db/view_api/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -//! Defines the `ViewApi` trait, which represents the API for querying a view of the graph. - -pub mod edge; -pub mod graph; -pub mod internal; -pub mod layer; -pub mod time; -pub mod vertex; - -pub use edge::EdgeListOps; -pub use edge::EdgeViewOps; -pub use graph::GraphViewOps; -pub use time::TimeOps; -pub use vertex::VertexListOps; -pub use vertex::VertexViewOps; - -pub type BoxedIter = Box + Send>; diff --git a/raphtory-io/src/graph_loader/example/company_house.rs b/raphtory/src/graph_loader/example/company_house.rs similarity index 55% rename from raphtory-io/src/graph_loader/example/company_house.rs rename to raphtory/src/graph_loader/example/company_house.rs index 9e39a2e82d..5473b94260 100644 --- a/raphtory-io/src/graph_loader/example/company_house.rs +++ b/raphtory/src/graph_loader/example/company_house.rs @@ -1,12 +1,7 @@ -use crate::graph_loader::source::csv_loader::CsvLoader; +use crate::{graph_loader::source::csv_loader::CsvLoader, prelude::*}; use chrono::NaiveDateTime; -use raphtory::core::Prop; -use raphtory::db::graph::Graph; -use raphtory::db::view_api::internal::GraphViewInternalOps; -use raphtory::db::view_api::{GraphViewOps, VertexViewOps}; use serde::Deserialize; -use std::path::PathBuf; -use std::{fs, time::Instant}; +use std::{fs, path::PathBuf, time::Instant}; #[derive(Deserialize, std::fmt::Debug)] pub struct CompanyHouse { @@ -14,10 +9,10 @@ pub struct CompanyHouse { pincode: String, company: String, owner: String, - illegal_hmo: Option + illegal_hmo: Option, } -pub fn company_house_graph(path: Option, num_shards: usize) -> Graph { +pub fn company_house_graph(path: Option) -> Graph { let default_data_dir: PathBuf = PathBuf::from("/tmp/company-house"); let data_dir = match path { @@ -43,11 +38,10 @@ pub fn company_house_graph(path: Option, num_shards: usize) -> Graph { .ok()?; println!( - "Loaded graph with {} shards from encoded data files {} with {} vertices, {} edges which took {} seconds", - g.num_shards(), + "Loaded graph from encoded data files {} with {} vertices, {} edges which took {} seconds", encoded_data_dir.to_str().unwrap(), - g.num_vertices(), - g.num_edges(), + g.count_vertices(), + g.count_edges(), now.elapsed().as_secs() ); @@ -58,7 +52,7 @@ pub fn company_house_graph(path: Option, num_shards: usize) -> Graph { } let g = restore_from_bincode(&encoded_data_dir).unwrap_or_else(|| { - let g = Graph::new(num_shards); + let g = Graph::new(); let now = Instant::now(); let ts = 1; @@ -67,7 +61,7 @@ pub fn company_house_graph(path: Option, num_shards: usize) -> Graph { .set_delimiter(",") .load_into_graph(&g, |company_house: CompanyHouse, g: &Graph| { let pincode = &company_house.pincode; - let address = company_house.address + ", " + pincode; + let address = format!("{}, {pincode}", company_house.address); let company = company_house.company; let owner = company_house.owner; // let illegal_flag : Option = match company_house.illegal_hmo { @@ -82,81 +76,71 @@ pub fn company_house_graph(path: Option, num_shards: usize) -> Graph { g.add_vertex( NaiveDateTime::from_timestamp_opt(ts, 0).unwrap(), owner.clone(), - &vec![], - ).expect("Failed to add vertex"); - - g.add_vertex_properties(owner.clone(), &vec![ - ("type".into(), Prop::Str("owner".into())) - ]) - .expect("Failed to add vertex static property"); + NO_PROPS, + ) + .expect("Failed to add vertex") + .add_constant_properties([("type", "owner")]) + .expect("Failed to add vertex static property"); - g.add_vertex( NaiveDateTime::from_timestamp_opt(ts, 0).unwrap(), company.clone(), - &vec![], - ).expect("Failed to add vertex"); - - g.add_vertex_properties(company.clone(), &vec![ - ("type".into(), Prop::Str("company".into())), - ("flag".into(), Prop::Str(company_house.illegal_hmo.clone().unwrap_or("None".into()))) - ]) - .expect("Failed to add vertex static property"); + NO_PROPS, + ) + .expect("Failed to add vertex") + .add_constant_properties([ + ("type", "company".into_prop()), + ( + "flag", + (company_house.illegal_hmo.clone().unwrap_or("None".into())).into_prop(), + ), + ]) + .expect("Failed to add vertex static property"); g.add_vertex( NaiveDateTime::from_timestamp_opt(ts, 0).unwrap(), address.clone(), - &vec![], - ).expect("Failed to add vertex"); - - g.add_vertex_properties(address.clone(), &vec![ - ("type".into(), Prop::Str("address".into())), - ("flag".into(), Prop::Str(company_house.illegal_hmo.clone().unwrap_or("None".into()))) - ]) - .expect("Failed to add vertex static property"); + NO_PROPS, + ) + .expect("Failed to add vertex") + .add_constant_properties([ + ("type", "address".into_prop()), + ( + "flag", + (company_house.illegal_hmo.clone().unwrap_or("None".into())).into_prop(), + ), + ]) + .expect("Failed to add vertex static property"); g.add_edge( NaiveDateTime::from_timestamp_opt(ts, 0).unwrap(), owner.clone(), company.clone(), - &vec![], + NO_PROPS, Some(pincode), ) - .expect("Failed to add edge"); - - g.add_edge_properties( - owner, - company.clone(), - &vec![("rel".into(), Prop::Str("owns".into()))], - Some(pincode), - ) - .expect("Failed to add edge static property"); + .expect("Failed to add edge") + .add_constant_properties([("rel", "owns")], Some(pincode)) + .expect("Failed to add edge static property"); g.add_edge( NaiveDateTime::from_timestamp_opt(ts, 0).unwrap(), company.clone(), address.clone(), - &vec![], - None, - ) - .expect("Failed to add edge"); - - g.add_edge_properties( - company, - address, - &vec![("rel".into(), Prop::Str("owns".into()))], + NO_PROPS, None, ) - .expect("Failed to add edge static property"); + .expect("Failed to add edge") + .add_constant_properties([("rel", "owns")], None) + .expect("Failed to add edge static property"); }) .expect("Failed to load graph from CSV data files"); println!( - "Loaded graph with {} shards from CSV data files {} with {} vertices, {} edges which took {} seconds", - g.num_shards(), + "Loaded graph from CSV data files {} with {} vertices, {} edges which took {} seconds", encoded_data_dir.to_str().unwrap(), - g.num_vertices(), - g.num_edges(), + g.count_vertices(), + g.count_edges(), now.elapsed().as_secs() ); @@ -172,15 +156,12 @@ pub fn company_house_graph(path: Option, num_shards: usize) -> Graph { #[cfg(test)] mod company_house_graph_test { use super::*; - use raphtory::db::view_api::{TimeOps, VertexViewOps}; + use crate::db::api::view::{TimeOps, VertexViewOps}; #[test] #[ignore] fn test_ch_load() { - let g = company_house_graph( - None, - 1, - ); + let g = company_house_graph(None); assert_eq!(g.start().unwrap(), 1000); assert_eq!(g.end().unwrap(), 1001); g.window(1000, 1001) diff --git a/raphtory-io/src/graph_loader/example/lotr_graph.rs b/raphtory/src/graph_loader/example/lotr_graph.rs similarity index 75% rename from raphtory-io/src/graph_loader/example/lotr_graph.rs rename to raphtory/src/graph_loader/example/lotr_graph.rs index e33c3d3701..20ff674e49 100644 --- a/raphtory-io/src/graph_loader/example/lotr_graph.rs +++ b/raphtory/src/graph_loader/example/lotr_graph.rs @@ -13,20 +13,18 @@ //! //! Example: //! ```rust -//! use raphtory_io::graph_loader::example::lotr_graph::lotr_graph; -//! use raphtory::db::graph::Graph; -//! use raphtory::db::view_api::*; +//! use raphtory::graph_loader::example::lotr_graph::lotr_graph; +//! use raphtory::prelude::*; //! -//! let graph = lotr_graph(1); +//! let graph = lotr_graph(); //! -//! println!("The graph has {:?} vertices", graph.num_vertices()); -//! println!("The graph has {:?} edges", graph.num_edges()); +//! println!("The graph has {:?} vertices", graph.count_vertices()); +//! println!("The graph has {:?} edges", graph.count_edges()); //! ``` -use raphtory::db::graph::Graph; - -use crate::graph_loader::fetch_file; - -use crate::graph_loader::source::csv_loader::CsvLoader; +use crate::{ + graph_loader::{fetch_file, source::csv_loader::CsvLoader}, + prelude::*, +}; use serde::Deserialize; use std::path::PathBuf; @@ -60,9 +58,9 @@ pub fn lotr_file() -> Result> { /// /// # Returns /// - A Graph containing the LOTR dataset -pub fn lotr_graph(shards: usize) -> Graph { +pub fn lotr_graph() -> Graph { let graph = { - let g = Graph::new(shards); + let g = Graph::new(); CsvLoader::new(lotr_file().unwrap()) .load_into_graph(&g, |lotr: Lotr, g: &Graph| { @@ -70,13 +68,13 @@ pub fn lotr_graph(shards: usize) -> Graph { let dst_id = lotr.dst_id; let time = lotr.time; - g.add_vertex(time, src_id.clone(), &vec![]) + g.add_vertex(time, src_id.clone(), NO_PROPS) .map_err(|err| println!("{:?}", err)) .ok(); - g.add_vertex(time, dst_id.clone(), &vec![]) + g.add_vertex(time, dst_id.clone(), NO_PROPS) .map_err(|err| println!("{:?}", err)) .ok(); - g.add_edge(time, src_id.clone(), dst_id.clone(), &vec![], None) + g.add_edge(time, src_id.clone(), dst_id.clone(), NO_PROPS, None) .expect("Error: Unable to add edge"); }) .expect("Failed to load graph from CSV data files"); diff --git a/raphtory-io/src/graph_loader/example/mod.rs b/raphtory/src/graph_loader/example/mod.rs similarity index 100% rename from raphtory-io/src/graph_loader/example/mod.rs rename to raphtory/src/graph_loader/example/mod.rs diff --git a/raphtory-io/src/graph_loader/example/neo4j_examples.rs b/raphtory/src/graph_loader/example/neo4j_examples.rs similarity index 62% rename from raphtory-io/src/graph_loader/example/neo4j_examples.rs rename to raphtory/src/graph_loader/example/neo4j_examples.rs index 467ae1e426..6bf70d13cf 100644 --- a/raphtory-io/src/graph_loader/example/neo4j_examples.rs +++ b/raphtory/src/graph_loader/example/neo4j_examples.rs @@ -1,7 +1,9 @@ -use crate::graph_loader::source::neo4j_loader::Neo4JConnection; +use crate::{ + db::{api::mutation::AdditionOps, graph::graph as rap}, + graph_loader::source::neo4j_loader::Neo4JConnection, + prelude::{IntoProp, NO_PROPS}, +}; use neo4rs::*; -use raphtory::core::Prop; -use raphtory::db::graph as rap; fn load_movies(row: Row, graph: &rap::Graph) { let film: Node = row.get("film").unwrap(); @@ -17,32 +19,24 @@ fn load_movies(row: Row, graph: &rap::Graph) { let relation_type = relation.typ(); graph - .add_vertex(actor_born, actor_name.clone(), &vec![]) + .add_vertex(actor_born, actor_name.clone(), NO_PROPS) + .unwrap() + .add_constant_properties([("type", "actor")]) .unwrap(); graph - .add_vertex_properties( - actor_name.clone(), - &vec![("type".into(), Prop::Str("actor".into()))], - ) - .unwrap(); - graph - .add_vertex(film_release, film_title.clone(), &vec![]) - .unwrap(); - graph - .add_vertex_properties( - film_title.clone(), - &vec![ - ("type".into(), Prop::Str("film".into())), - ("tagline".into(), Prop::Str(film_tagline)), - ], - ) + .add_vertex(film_release, film_title.clone(), NO_PROPS) + .unwrap() + .add_constant_properties([ + ("type", "film".into_prop()), + ("tagline", film_tagline.into_prop()), + ]) .unwrap(); graph .add_edge( film_release, actor_name, film_title, - &vec![], + NO_PROPS, Some(relation_type.as_str()), ) .unwrap(); @@ -53,9 +47,8 @@ pub async fn neo4j_movie_graph( username: String, password: String, database: String, - shards: usize, ) -> rap::Graph { - let g = rap::Graph::new(shards); + let g = rap::Graph::new(); let neo = Neo4JConnection::new(uri, username, password, database) .await .unwrap(); diff --git a/raphtory-io/src/graph_loader/example/reddit_hyperlinks.rs b/raphtory/src/graph_loader/example/reddit_hyperlinks.rs similarity index 81% rename from raphtory-io/src/graph_loader/example/reddit_hyperlinks.rs rename to raphtory/src/graph_loader/example/reddit_hyperlinks.rs index 597bed9392..e9c18f7395 100644 --- a/raphtory-io/src/graph_loader/example/reddit_hyperlinks.rs +++ b/raphtory/src/graph_loader/example/reddit_hyperlinks.rs @@ -29,25 +29,23 @@ //! //! Example: //! ```no_run -//! use raphtory_io::graph_loader::example::reddit_hyperlinks::reddit_graph; -//! use raphtory::db::graph::Graph; -//! use raphtory::db::view_api::*; +//! use raphtory::graph_loader::example::reddit_hyperlinks::reddit_graph; +//! use raphtory::prelude::*; //! -//! let graph = reddit_graph(1, 120, false); +//! let graph = reddit_graph(120, false); //! -//! println!("The graph has {:?} vertices", graph.num_vertices()); -//! println!("The graph has {:?} edges", graph.num_edges()); +//! println!("The graph has {:?} vertices", graph.count_vertices()); +//! println!("The graph has {:?} edges", graph.count_edges()); //! ``` -use crate::graph_loader::fetch_file; +use crate::{core::Prop, db::api::mutation::AdditionOps, graph_loader::fetch_file, prelude::*}; use chrono::*; use itertools::Itertools; -use raphtory::core::Prop; -use raphtory::db::graph::Graph; -use std::fs::File; -use std::io::{self, BufRead}; -use std::path::Path; -use std::path::PathBuf; +use std::{ + fs::File, + io::{self, BufRead}, + path::{Path, PathBuf}, +}; /// Download the dataset and return the path to the file /// # Arguments @@ -68,7 +66,7 @@ pub fn reddit_file( _ => fetch_file( "reddit-title.tsv", true, - "http://snap.stanford.edu/data/soc-redditHyperlinks-title.tsv", + "http://web.archive.org/web/20201107005944/http://snap.stanford.edu/data/soc-redditHyperlinks-title.tsv", timeout, ), } @@ -87,15 +85,14 @@ where /// /// # Arguments /// -/// * `shards` - The number of shards to use for the graph /// * `timeout` - The timeout in seconds for downloading the dataset /// /// # Returns /// /// * `Graph` - The graph containing the Reddit hyperlinks dataset -pub fn reddit_graph(shards: usize, timeout: u64, test_file: bool) -> Graph { +pub fn reddit_graph(timeout: u64, test_file: bool) -> Graph { let graph = { - let g = Graph::new(shards); + let g = Graph::new(); if let Ok(path) = reddit_file(timeout, Some(test_file)) { if let Ok(lines) = read_lines(path.as_path()) { @@ -114,9 +111,9 @@ pub fn reddit_graph(shards: usize, timeout: u64, test_file: bool) -> Graph { .split(',') .map(|s| s.parse::().unwrap()) .collect(); - let edge_properties = &vec![ + let edge_properties = [ ("post_label".to_string(), Prop::I32(post_label)), - ("post_id".to_string(), Prop::Str(post_id)), + ("post_id".to_string(), Prop::str(post_id)), ("word_count".to_string(), Prop::F64(post_properties[7])), ("long_words".to_string(), Prop::F64(post_properties[9])), ("sentences".to_string(), Prop::F64(post_properties[13])), @@ -134,10 +131,10 @@ pub fn reddit_graph(shards: usize, timeout: u64, test_file: bool) -> Graph { Prop::F64(post_properties[20]), ), ]; - g.add_vertex(time, *src_id, &vec![]) + g.add_vertex(time, *src_id, NO_PROPS) .map_err(|err| println!("{:?}", err)) .ok(); - g.add_vertex(time, *dst_id, &vec![]) + g.add_vertex(time, *dst_id, NO_PROPS) .map_err(|err| println!("{:?}", err)) .ok(); g.add_edge(time, *src_id, *dst_id, edge_properties, None) @@ -158,8 +155,10 @@ pub fn reddit_graph(shards: usize, timeout: u64, test_file: bool) -> Graph { #[cfg(test)] mod reddit_test { - use crate::graph_loader::example::reddit_hyperlinks::{reddit_file, reddit_graph}; - use raphtory::db::view_api::GraphViewOps; + use crate::{ + db::api::view::*, + graph_loader::example::reddit_hyperlinks::{reddit_file, reddit_graph}, + }; #[test] fn check_data() { @@ -169,8 +168,8 @@ mod reddit_test { #[test] fn check_graph() { - let graph = reddit_graph(1, 100, true); - assert_eq!(graph.num_vertices(), 16); - assert_eq!(graph.num_edges(), 9); + let graph = reddit_graph(100, true); + assert_eq!(graph.count_vertices(), 16); + assert_eq!(graph.count_edges(), 9); } } diff --git a/raphtory-io/src/graph_loader/example/stable_coins.rs b/raphtory/src/graph_loader/example/stable_coins.rs similarity index 74% rename from raphtory-io/src/graph_loader/example/stable_coins.rs rename to raphtory/src/graph_loader/example/stable_coins.rs index 920de9250b..4dc3d7c547 100644 --- a/raphtory-io/src/graph_loader/example/stable_coins.rs +++ b/raphtory/src/graph_loader/example/stable_coins.rs @@ -1,19 +1,13 @@ -use crate::graph_loader::source::csv_loader::CsvLoader; +use crate::{ + graph_loader::{fetch_file, source::csv_loader::CsvLoader, unzip_file}, + prelude::*, +}; use chrono::NaiveDateTime; -use raphtory::core::Prop; -use raphtory::db::graph::Graph; -use raphtory::db::view_api::internal::GraphViewInternalOps; -use raphtory::db::view_api::GraphViewOps; -use serde::Deserialize; -use std::collections::HashMap; -use std::fs::File; -use std::io::{copy, Cursor}; -use std::path::{Path, PathBuf}; -use std::time::Duration; -use std::{fs, time::Instant}; use regex::Regex; -use crate::graph_loader::{fetch_file, unzip_file}; +use serde::Deserialize; +use std::{collections::HashMap, fs, path::PathBuf, time::Instant}; +#[allow(dead_code)] #[derive(Deserialize, std::fmt::Debug)] pub struct StableCoin { block_number: String, @@ -25,7 +19,7 @@ pub struct StableCoin { value: f64, } -pub fn stable_coin_graph(path: Option, subset:bool, num_shards: usize) -> Graph { +pub fn stable_coin_graph(path: Option, subset: bool) -> Graph { let data_dir = match path { Some(path) => PathBuf::from(path), None => PathBuf::from("/tmp/stablecoin"), @@ -33,7 +27,7 @@ pub fn stable_coin_graph(path: Option, subset:bool, num_shards: usize) - if !data_dir.join("token_transfers.csv").exists() { let dir_str = data_dir.to_str().unwrap(); - let zip_path =data_dir.join("ERC20-stablecoins.zip"); + let zip_path = data_dir.join("ERC20-stablecoins.zip"); let zip_str = zip_path.to_str().unwrap(); fs::create_dir_all(dir_str).expect(&format!("Failed to create directory {}", dir_str)); fetch_file(zip_str,false,"https://snap.stanford.edu/data/ERC20-stablecoins.zip",600000).expect("Failed to fetch stable coin data: https://snap.stanford.edu/data/ERC20-stablecoins.zip"); @@ -53,11 +47,10 @@ pub fn stable_coin_graph(path: Option, subset:bool, num_shards: usize) - .ok()?; println!( - "Loaded graph with {} shards from encoded data files {} with {} vertices, {} edges which took {} seconds", - g.num_shards(), + "Loaded graph from encoded data files {} with {} vertices, {} edges which took {} seconds", encoded_data_dir.to_str().unwrap(), - g.num_vertices(), - g.num_edges(), + g.count_vertices(), + g.count_edges(), now.elapsed().as_secs() ); @@ -69,7 +62,7 @@ pub fn stable_coin_graph(path: Option, subset:bool, num_shards: usize) - let encoded_data_dir = data_dir.join("graphdb.bincode"); let g = restore_from_bincode(&encoded_data_dir).unwrap_or_else(|| { - let g = Graph::new(num_shards); + let g = Graph::new(); let now = Instant::now(); let contract_addr_labels = HashMap::from([ @@ -83,10 +76,8 @@ pub fn stable_coin_graph(path: Option, subset:bool, num_shards: usize) - let re = if subset { Regex::new(r"token_transfers.csv").unwrap() - } - else{ + } else { Regex::new(r"token_transfers(_V\d+\.\d+\.\d+)?\.csv").unwrap() - }; CsvLoader::new(data_dir) .with_filter(re) @@ -99,7 +90,7 @@ pub fn stable_coin_graph(path: Option, subset:bool, num_shards: usize) - NaiveDateTime::from_timestamp_opt(stablecoin.time_stamp, 0).unwrap(), stablecoin.from_address, stablecoin.to_address, - &vec![("value".into(), Prop::F64(stablecoin.value.into()))], + [("value", stablecoin.value)], Some(label), ) .expect("Failed to add edge"); @@ -107,11 +98,10 @@ pub fn stable_coin_graph(path: Option, subset:bool, num_shards: usize) - .expect("Failed to load graph from CSV data files"); println!( - "Loaded graph with {} shards from CSV data files {} with {} vertices, {} edges which took {} seconds", - g.num_shards(), + "Loaded graph from CSV data files {} with {} vertices, {} edges which took {} seconds", encoded_data_dir.to_str().unwrap(), - g.num_vertices(), - g.num_edges(), + g.count_vertices(), + g.count_edges(), now.elapsed().as_secs() ); diff --git a/raphtory-io/src/graph_loader/example/sx_superuser_graph.rs b/raphtory/src/graph_loader/example/sx_superuser_graph.rs similarity index 76% rename from raphtory-io/src/graph_loader/example/sx_superuser_graph.rs rename to raphtory/src/graph_loader/example/sx_superuser_graph.rs index b163353ace..0b293857d7 100644 --- a/raphtory-io/src/graph_loader/example/sx_superuser_graph.rs +++ b/raphtory/src/graph_loader/example/sx_superuser_graph.rs @@ -36,19 +36,19 @@ //! //! Example: //! ```no_run -//! use raphtory_io::graph_loader::example::sx_superuser_graph::sx_superuser_graph; -//! use raphtory::db::graph::Graph; -//! use raphtory::db::view_api::*; +//! use raphtory::graph_loader::example::sx_superuser_graph::sx_superuser_graph; +//! use raphtory::prelude::*; //! -//! let graph = sx_superuser_graph(1).unwrap(); +//! let graph = sx_superuser_graph().unwrap(); //! -//! println!("The graph has {:?} vertices", graph.num_vertices()); -//! println!("The graph has {:?} edges", graph.num_edges()); +//! println!("The graph has {:?} vertices", graph.count_vertices()); +//! println!("The graph has {:?} edges", graph.count_edges()); //! ``` -use raphtory::db::graph::Graph; - -use crate::graph_loader::{fetch_file, source::csv_loader::CsvLoader}; +use crate::{ + graph_loader::{fetch_file, source::csv_loader::CsvLoader}, + prelude::*, +}; use serde::Deserialize; use std::path::PathBuf; @@ -68,7 +68,7 @@ pub fn sx_superuser_file() -> Result> { fetch_file( "sx-superuser.txt.gz", true, - "https://snap.stanford.edu/data/sx-superuser.txt.gz", + "http://web.archive.org/web/20230309171639/https://snap.stanford.edu/data/sx-superuser.txt.gz", 600, ) } @@ -82,12 +82,12 @@ pub fn sx_superuser_file() -> Result> { /// # Returns /// /// - A Result containing the graph or an error -pub fn sx_superuser_graph(shards: usize) -> Result> { - let graph = Graph::new(shards); +pub fn sx_superuser_graph() -> Result> { + let graph = Graph::new(); CsvLoader::new(sx_superuser_file()?) .set_delimiter(" ") .load_into_graph(&graph, |edge: TEdge, g: &Graph| { - g.add_edge(edge.time, edge.src_id, edge.dst_id, &vec![], None) + g.add_edge(edge.time, edge.src_id, edge.dst_id, NO_PROPS, None) .expect("Error: Unable to add edge"); })?; @@ -99,13 +99,15 @@ mod sx_superuser_test { use crate::graph_loader::example::sx_superuser_graph::{sx_superuser_file, sx_superuser_graph}; #[test] + #[ignore] // don't hit SNAP by default fn test_download_works() { let file = sx_superuser_file().unwrap(); assert!(file.is_file()) } #[test] + #[ignore] // don't hit SNAP by default FIXME: add a truncated test file for this one? fn test_graph_loading_works() { - sx_superuser_graph(2).unwrap(); + sx_superuser_graph().unwrap(); } } diff --git a/raphtory-io/src/graph_loader/mod.rs b/raphtory/src/graph_loader/mod.rs similarity index 64% rename from raphtory-io/src/graph_loader/mod.rs rename to raphtory/src/graph_loader/mod.rs index 737a9f7796..fd0a4b6a74 100644 --- a/raphtory-io/src/graph_loader/mod.rs +++ b/raphtory/src/graph_loader/mod.rs @@ -1,11 +1,87 @@ -//! `GraphLoader` trait and provides some default implementations for loading a graph. +//! Module for loading graphs into raphtory from various sources, like csv, neo4j, etc. +//! +//! Provides the `GraphLoader` trait and some default implementations for loading a graph. //! This base class is used to load in-built graphs such as the LOTR, reddit and StackOverflow. //! It also provides a method to download a CSV file. //! -//! # Example +//! # Examples +//! +//! Load a pre-built graph +//! ```rust +//! use raphtory::algorithms::degree::average_degree; +//! use raphtory::prelude::*; +//! use raphtory::graph_loader::example::lotr_graph::lotr_graph; +//! +//! let graph = lotr_graph(); +//! +//! // Get the in-degree, out-degree of Gandalf +//! // The graph.vertex option returns a result of an option, +//! // so we need to unwrap the result and the option or +//! // we can use this if let instead +//! if let Some(gandalf) = graph.vertex("Gandalf") { +//! println!("Gandalf in degree: {:?}", gandalf.in_degree()); +//! println!("Gandalf out degree: {:?}", gandalf.out_degree()); +//! } +//! +//! // Run an average degree algorithm on the graph +//! println!("Average degree: {:?}", average_degree(&graph)); +//! ``` +//! +//! Load a graph from csv +//! +//! ```no_run +//! use std::time::Instant; +//! use serde::Deserialize; +//! use raphtory::graph_loader::source::csv_loader::CsvLoader; +//! use raphtory::prelude::*; +//! +//! let data_dir = "/tmp/lotr.csv"; +//! +//! #[derive(Deserialize, std::fmt::Debug)] +//! pub struct Lotr { +//! src_id: String, +//! dst_id: String, +//! time: i64, +//! } +//! +//! let g = Graph::new(); +//! let now = Instant::now(); +//! +//! CsvLoader::new(data_dir) +//! .load_into_graph(&g, |lotr: Lotr, g: &Graph| { +//! g.add_vertex( +//! lotr.time, +//! lotr.src_id.clone(), +//! [("type", Prop::str("Character"))], +//! ) +//! .expect("Failed to add vertex"); +//! +//! g.add_vertex( +//! lotr.time, +//! lotr.dst_id.clone(), +//! [("type", Prop::str("Character"))], +//! ) +//! .expect("Failed to add vertex"); +//! +//! g.add_edge( +//! lotr.time, +//! lotr.src_id.clone(), +//! lotr.dst_id.clone(), +//! [( +//! "type", +//! Prop::str("Character Co-occurrence"), +//! )], +//! None, +//! ) +//! .expect("Failed to add edge"); +//! }) +//! .expect("Failed to load graph from CSV data files"); +//! ``` +//! +//! download a file without creating the graph //! //! ```rust -//! use raphtory_io::graph_loader::fetch_file; +//! use raphtory::graph_loader::fetch_file; //! //! let path = fetch_file( //! "lotr.csv", @@ -17,38 +93,36 @@ //! // check if a file exists at the path //! assert!(path.is_ok()); //! ``` -//! -use std::env; -use std::fs::File; -use std::io::{copy, Cursor}; -use std::path::{Path, PathBuf}; -use std::time::Duration; -use std::io::prelude::*; -use zip::read::{ZipArchive, ZipFile}; -use std::fs::*; +use std::{ + env, + fs::{File, *}, + io::{copy, Cursor}, + path::{Path, PathBuf}, + time::Duration, +}; +use zip::read::ZipArchive; pub mod example; pub mod source; pub fn fetch_file( name: &str, - tmp_save:bool, + tmp_save: bool, url: &str, timeout: u64, ) -> Result> { let filepath = if tmp_save { let tmp_dir = env::temp_dir(); tmp_dir.join(name) - } - else { + } else { PathBuf::from(name) }; if !filepath.exists() { let client = reqwest::blocking::Client::builder() .timeout(Duration::from_secs(timeout)) .build()?; - let response = client.get(url).send()?; + let response = client.get(url).send()?.error_for_status()?; let mut content = Cursor::new(response.bytes()?); if !filepath.exists() { let mut file = File::create(&filepath)?; @@ -58,7 +132,6 @@ pub fn fetch_file( Ok(filepath) } - fn unzip_file(zip_file_path: &str, destination_path: &str) -> std::io::Result<()> { let file = File::open(zip_file_path)?; let mut archive = ZipArchive::new(file)?; @@ -73,7 +146,7 @@ fn unzip_file(zip_file_path: &str, destination_path: &str) -> std::io::Result<() } else { if let Some(parent) = Path::new(&dest_path).parent() { if !parent.exists() { - create_dir_all(&parent)?; + create_dir_all(&parent)?; } } let mut output_file = File::create(&dest_path)?; @@ -84,23 +157,10 @@ fn unzip_file(zip_file_path: &str, destination_path: &str) -> std::io::Result<() Ok(()) } - - - #[cfg(test)] mod graph_loader_test { + use crate::{core::utils::hashing, graph_loader::fetch_file, prelude::*}; use csv::StringRecord; - use raphtory::{ - core::{utils, Prop}, - db::{ - graph::Graph, - view_api::{GraphViewOps, TimeOps, VertexViewOps}, - }, - }; - - use crate::graph_loader::{fetch_file, unzip_file}; - use crate::graph_loader::example::stable_coins::stable_coin_graph; - #[test] fn test_fetch_file() { @@ -115,13 +175,13 @@ mod graph_loader_test { #[test] fn test_lotr_load_graph() { - let g = crate::graph_loader::example::lotr_graph::lotr_graph(4); - assert_eq!(g.num_edges(), 701); + let g = crate::graph_loader::example::lotr_graph::lotr_graph(); + assert_eq!(g.count_edges(), 701); } #[test] fn test_graph_at() { - let g = crate::graph_loader::example::lotr_graph::lotr_graph(1); + let g = crate::graph_loader::example::lotr_graph::lotr_graph(); let g_at_empty = g.at(1); let g_at_start = g.at(7059); @@ -129,16 +189,16 @@ mod graph_loader_test { let g_at_max = g.at(i64::MAX); let g_at_min = g.at(i64::MIN); - assert_eq!(g_at_empty.num_vertices(), 0); - assert_eq!(g_at_start.num_vertices(), 70); - assert_eq!(g_at_another.num_vertices(), 123); - assert_eq!(g_at_max.num_vertices(), 139); - assert_eq!(g_at_min.num_vertices(), 0); + assert_eq!(g_at_empty.count_vertices(), 0); + assert_eq!(g_at_start.count_vertices(), 70); + assert_eq!(g_at_another.count_vertices(), 123); + assert_eq!(g_at_max.count_vertices(), 139); + assert_eq!(g_at_min.count_vertices(), 0); } #[test] fn db_lotr() { - let g = Graph::new(4); + let g = Graph::new(); let data_dir = crate::graph_loader::example::lotr_graph::lotr_file() .expect("Failed to get lotr.csv file"); @@ -153,29 +213,18 @@ mod graph_loader_test { if let Ok(mut reader) = csv::Reader::from_path(data_dir) { for rec in reader.records().flatten() { if let Some((src, dst, t)) = parse_record(&rec) { - let src_id = utils::calculate_hash(&src); - let dst_id = utils::calculate_hash(&dst); + let src_id = hashing::calculate_hash(&src); + let dst_id = hashing::calculate_hash(&dst); - g.add_vertex( - t, - src_id, - &vec![("name".to_string(), Prop::Str("Character".to_string()))], - ) - .unwrap(); - g.add_vertex( - t, - dst_id, - &vec![("name".to_string(), Prop::Str("Character".to_string()))], - ) - .unwrap(); + g.add_vertex(t, src_id, [("name", Prop::str("Character"))]) + .unwrap(); + g.add_vertex(t, dst_id, [("name", Prop::str("Character"))]) + .unwrap(); g.add_edge( t, src_id, dst_id, - &vec![( - "name".to_string(), - Prop::Str("Character Co-occurrence".to_string()), - )], + [("name", Prop::str("Character Co-occurrence"))], None, ) .unwrap(); @@ -183,16 +232,16 @@ mod graph_loader_test { } } - let gandalf = utils::calculate_hash(&"Gandalf"); + let gandalf = hashing::calculate_hash(&"Gandalf"); assert!(g.has_vertex(gandalf)); assert!(g.has_vertex("Gandalf")) } #[test] fn test_all_degrees_window() { - let g = crate::graph_loader::example::lotr_graph::lotr_graph(4); + let g = crate::graph_loader::example::lotr_graph::lotr_graph(); - assert_eq!(g.num_edges(), 701); + assert_eq!(g.count_edges(), 701); assert_eq!(g.vertex("Gandalf").unwrap().degree(), 49); assert_eq!( g.vertex("Gandalf").unwrap().window(1356, 24792).degree(), @@ -215,9 +264,9 @@ mod graph_loader_test { #[test] fn test_all_neighbours_window() { - let g = crate::graph_loader::example::lotr_graph::lotr_graph(4); + let g = crate::graph_loader::example::lotr_graph::lotr_graph(); - assert_eq!(g.num_edges(), 701); + assert_eq!(g.count_edges(), 701); assert_eq!(g.vertex("Gandalf").unwrap().neighbours().iter().count(), 49); for v in g @@ -268,9 +317,9 @@ mod graph_loader_test { #[test] fn test_all_edges_window() { - let g = crate::graph_loader::example::lotr_graph::lotr_graph(4); + let g = crate::graph_loader::example::lotr_graph::lotr_graph(); - assert_eq!(g.num_edges(), 701); + assert_eq!(g.count_edges(), 701); assert_eq!(g.vertex("Gandalf").unwrap().edges().count(), 59); assert_eq!( g.vertex("Gandalf") diff --git a/raphtory-io/src/graph_loader/source/csv_loader.rs b/raphtory/src/graph_loader/source/csv_loader.rs similarity index 73% rename from raphtory-io/src/graph_loader/source/csv_loader.rs rename to raphtory/src/graph_loader/source/csv_loader.rs index 2ec34639f1..615eda1134 100644 --- a/raphtory-io/src/graph_loader/source/csv_loader.rs +++ b/raphtory/src/graph_loader/source/csv_loader.rs @@ -4,13 +4,12 @@ //! ```no_run //! use std::path::{Path, PathBuf}; //! use regex::Regex; -//! use raphtory::core::Prop; -//! use raphtory::core::utils::calculate_hash; -//! use raphtory_io::graph_loader::source::csv_loader::CsvLoader; -//! use raphtory::db::graph::Graph; -//! use raphtory_io::graph_loader::example::lotr_graph::Lotr; +//! use raphtory::core::utils::hashing::calculate_hash; +//! use raphtory::graph_loader::source::csv_loader::CsvLoader; +//! use raphtory::graph_loader::example::lotr_graph::Lotr; +//! use raphtory::prelude::*; //! -//! let g = Graph::new(2); +//! let g = Graph::new(); //! let csv_path: PathBuf = [env!("CARGO_MANIFEST_DIR"), "../../resource/"] //! .iter() //! .collect(); @@ -33,14 +32,14 @@ //! g.add_vertex( //! time, //! src_id, -//! &vec![("name".to_string(), Prop::Str("Character".to_string()))], +//! [("name", Prop::str("Character"))], //! ) //! .map_err(|err| println!("{:?}", err)) //! .ok(); //! g.add_vertex( //! time, //! dst_id, -//! &vec![("name".to_string(), Prop::Str("Character".to_string()))], +//! [("name", Prop::str("Character"))], //! ) //! .map_err(|err| println!("{:?}", err)) //! .ok(); @@ -48,9 +47,9 @@ //! time, //! src_id, //! dst_id, -//! &vec![( -//! "name".to_string(), -//! Prop::Str("Character Co-occurrence".to_string()), +//! [( +//! "name", +//! Prop::str("Character Co-occurrence"), //! )], //! None, //! ).expect("Failed to add edge"); @@ -61,18 +60,22 @@ /// Module for loading CSV files into a graph. use bzip2::read::BzDecoder; +use csv::StringRecord; use flate2; // 1.0 use flate2::read::GzDecoder; use rayon::prelude::*; use regex::Regex; use serde::de::DeserializeOwned; -use std::collections::VecDeque; -use std::error::Error; -use std::fmt::{Debug, Display, Formatter}; -use std::fs::File; -use std::io::BufReader; -use std::path::{Path, PathBuf}; -use std::{fs, io}; +use std::{ + collections::VecDeque, + error::Error, + fmt::{Debug, Display, Formatter}, + fs, + fs::File, + io, + io::BufReader, + path::{Path, PathBuf}, +}; #[derive(Debug)] pub enum CsvErr { @@ -123,6 +126,8 @@ pub struct CsvLoader { header: bool, /// The delimiter character used in the CSV file. delimiter: u8, + /// print the name of the file being loaded + print_file_name: bool, } impl CsvLoader { @@ -136,7 +141,7 @@ impl CsvLoader { /// /// ```no_run /// - /// use raphtory_io::graph_loader::source::csv_loader::CsvLoader; + /// use raphtory::graph_loader::source::csv_loader::CsvLoader; /// let loader = CsvLoader::new("/path/to/csv_file.csv"); /// ``` pub fn new>(p: P) -> Self { @@ -145,6 +150,7 @@ impl CsvLoader { regex_filter: None, header: false, delimiter: b',', + print_file_name: false, } } @@ -157,7 +163,7 @@ impl CsvLoader { /// # Example /// /// ```no_run - /// use raphtory_io::graph_loader::source::csv_loader::CsvLoader; + /// use raphtory::graph_loader::source::csv_loader::CsvLoader; /// let loader = CsvLoader::new("/path/to/csv_file.csv").set_header(true); /// ``` pub fn set_header(mut self, h: bool) -> Self { @@ -165,6 +171,22 @@ impl CsvLoader { self } + /// If set to true will print the file name as it reads it + /// + /// # Arguments + /// + /// * `p` - A boolean value indicating whether the CSV file has a header. + /// + /// # Example + /// ```no_run + /// use raphtory::graph_loader::source::csv_loader::CsvLoader; + /// let loader = CsvLoader::new("/path/to/csv_file.csv").set_print_file_name(true); + /// ``` + pub fn set_print_file_name(mut self, p: bool) -> Self { + self.print_file_name = p; + self + } + /// Sets the delimiter character used in the CSV file. /// /// # Arguments @@ -174,7 +196,7 @@ impl CsvLoader { /// # Example /// /// ```no_run - /// use raphtory_io::graph_loader::source::csv_loader::CsvLoader; + /// use raphtory::graph_loader::source::csv_loader::CsvLoader; /// let loader = CsvLoader::new("/path/to/csv_file.csv").set_delimiter("|"); /// ``` pub fn set_delimiter(mut self, d: &str) -> Self { @@ -192,7 +214,7 @@ impl CsvLoader { /// /// ```no_run /// use regex::Regex; - /// use raphtory_io::graph_loader::source::csv_loader::CsvLoader; + /// use raphtory::graph_loader::source::csv_loader::CsvLoader; /// /// let loader = CsvLoader::new("/path/to/csv_files") /// .with_filter(Regex::new(r"file_name_pattern").unwrap()); @@ -268,14 +290,12 @@ impl CsvLoader { while let Some(ref path) = queue.pop_back() { match fs::read_dir(path) { Ok(entries) => { - for entry in entries { - if let Ok(f_path) = entry { - let p = f_path.path(); - if Self::is_dir(&p)? { - queue.push_back(p.clone()) - } else { - self.accept_file(f_path.path(), &mut paths); - } + for f_path in entries.flatten() { + let p = f_path.path(); + if Self::is_dir(&p)? { + queue.push_back(p.clone()) + } else { + self.accept_file(f_path.path(), &mut paths); } } } @@ -321,6 +341,34 @@ impl CsvLoader { Ok(()) } + /// Load data from all CSV files in the directory into a graph. + /// + /// # Arguments + /// + /// * `g` - A reference to the graph object where the data should be loaded. + /// * `loader` - A closure that takes a deserialized record and the graph object as arguments and adds the record to the graph. + /// + /// # Returns + /// + /// A Result containing an empty Ok value if the data is loaded successfully. + /// + /// # Errors + /// + /// An error of type CsvErr is returned if an I/O error occurs while reading the files or parsing the CSV data. + /// + pub fn load_rec_into_graph(&self, g: &G, loader: F) -> Result<(), CsvErr> + where + F: Fn(StringRecord, &G) + Send + Sync, + G: Sync, + { + //FIXME: loader function should return a result for reporting parsing errors + let paths = self.files_vec()?; + paths + .par_iter() + .try_for_each(move |path| self.load_file_into_graph_record(path, g, &loader))?; + Ok(()) + } + /// Loads a CSV file into a graph using the specified loader function. /// /// # Arguments @@ -344,7 +392,9 @@ impl CsvLoader { F: Fn(REC, &G), { let file_path: PathBuf = path.into(); - + if self.print_file_name { + println!("Loading file: {:?}", file_path); + } let mut csv_reader = self.csv_reader(file_path)?; let records_iter = csv_reader.deserialize::(); @@ -357,6 +407,26 @@ impl CsvLoader { Ok(()) } + fn load_file_into_graph_record + Debug, G>( + &self, + path: P, + g: &G, + loader: &F, + ) -> Result<(), CsvErr> + where + F: Fn(StringRecord, &G), + { + let file_path: PathBuf = path.into(); + + let mut csv_reader = self.csv_reader(file_path)?; + for rec in csv_reader.records() { + let record = rec?; + loader(record, g) + } + + Ok(()) + } + /// Returns a `csv::Reader` for the specified file path, automatically detecting and handling gzip and bzip compression. /// /// # Arguments @@ -402,10 +472,11 @@ impl CsvLoader { #[cfg(test)] mod csv_loader_test { - use crate::graph_loader::source::csv_loader::CsvLoader; - use raphtory::core::utils::calculate_hash; - use raphtory::core::Prop; - use raphtory::db::graph::Graph; + use crate::{ + core::utils::hashing::calculate_hash, graph_loader::source::csv_loader::CsvLoader, + prelude::*, + }; + use csv::StringRecord; use regex::Regex; use serde::Deserialize; use std::path::{Path, PathBuf}; @@ -449,28 +520,51 @@ mod csv_loader_test { let dst_id = calculate_hash(&lotr.dst_id); let time = lotr.time; - g.add_vertex( + g.add_vertex(time, src_id, [("name", Prop::str("Character"))]) + .map_err(|err| println!("{:?}", err)) + .ok(); + g.add_vertex(time, dst_id, [("name", Prop::str("Character"))]) + .map_err(|err| println!("{:?}", err)) + .ok(); + g.add_edge( time, src_id, - &vec![("name".to_string(), Prop::Str("Character".to_string()))], - ) - .map_err(|err| println!("{:?}", err)) - .ok(); - g.add_vertex( - time, dst_id, - &vec![("name".to_string(), Prop::Str("Character".to_string()))], + [("name", Prop::str("Character Co-occurrence"))], + None, ) - .map_err(|err| println!("{:?}", err)) - .ok(); + .unwrap(); + }) + .expect("Csv did not parse."); + } + + fn lotr_test_rec(g: Graph, csv_loader: CsvLoader, has_header: bool, delimiter: &str, r: Regex) { + csv_loader + .set_header(has_header) + .set_delimiter(delimiter) + .with_filter(r) + .load_rec_into_graph(&g, |lotr: StringRecord, g: &Graph| { + let src_id = lotr + .get(0) + .map(|s| calculate_hash(&(s.to_owned()))) + .unwrap(); + let dst_id = lotr + .get(1) + .map(|s| calculate_hash(&(s.to_owned()))) + .unwrap(); + let time = lotr.get(2).map(|s| s.parse::().unwrap()).unwrap(); + + g.add_vertex(time, src_id, [("name", Prop::str("Character"))]) + .map_err(|err| println!("{:?}", err)) + .ok(); + g.add_vertex(time, dst_id, [("name", Prop::str("Character"))]) + .map_err(|err| println!("{:?}", err)) + .ok(); g.add_edge( time, src_id, dst_id, - &vec![( - "name".to_string(), - Prop::Str("Character Co-occurrence".to_string()), - )], + [("name", Prop::str("Character Co-occurrence"))], None, ) .unwrap(); @@ -480,7 +574,7 @@ mod csv_loader_test { #[test] fn test_headers_flag_and_delimiter() { - let g = Graph::new(2); + let g = Graph::new(); // todo: move file path to data module let csv_path: PathBuf = [env!("CARGO_MANIFEST_DIR"), "../resource/"] .iter() @@ -492,12 +586,16 @@ mod csv_loader_test { let r = Regex::new(r".+(lotr.csv)").unwrap(); let delimiter = ","; lotr_test(g, csv_loader, has_header, delimiter, r); + let g = Graph::new(); + let csv_loader = CsvLoader::new(Path::new(&csv_path)); + let r = Regex::new(r".+(lotr.csv)").unwrap(); + lotr_test_rec(g, csv_loader, has_header, delimiter, r); } #[test] #[should_panic] fn test_wrong_header_flag_file_with_header() { - let g = Graph::new(2); + let g = Graph::new(); // todo: move file path to data module let csv_path: PathBuf = [env!("CARGO_MANIFEST_DIR"), "../../resource/"] .iter() @@ -512,7 +610,7 @@ mod csv_loader_test { #[test] #[should_panic] fn test_flag_has_header_but_file_has_no_header() { - let g = Graph::new(2); + let g = Graph::new(); // todo: move file path to data module let csv_path: PathBuf = [env!("CARGO_MANIFEST_DIR"), "../../resource/"] .iter() @@ -527,7 +625,7 @@ mod csv_loader_test { #[test] #[should_panic] fn test_wrong_header_names() { - let g = Graph::new(2); + let g = Graph::new(); // todo: move file path to data module let csv_path: PathBuf = [env!("CARGO_MANIFEST_DIR"), "../../resource/"] .iter() @@ -542,7 +640,7 @@ mod csv_loader_test { #[test] #[should_panic] fn test_wrong_delimiter() { - let g = Graph::new(2); + let g = Graph::new(); // todo: move file path to data module let csv_path: PathBuf = [env!("CARGO_MANIFEST_DIR"), "../../resource/"] .iter() diff --git a/raphtory-io/src/graph_loader/source/json_loader.rs b/raphtory/src/graph_loader/source/json_loader.rs similarity index 59% rename from raphtory-io/src/graph_loader/source/json_loader.rs rename to raphtory/src/graph_loader/source/json_loader.rs index 64609b53ae..4162445b5b 100644 --- a/raphtory-io/src/graph_loader/source/json_loader.rs +++ b/raphtory/src/graph_loader/source/json_loader.rs @@ -1,9 +1,21 @@ -use std::{io, path::{PathBuf, Path}, collections::VecDeque, fs, error::Error, fmt::{Display, Formatter}}; - -use raphtory::core::tgraph_shard::errors::GraphError; +use crate::core::utils::errors::GraphError; +use bzip2::read::BzDecoder; +use flate2; // 1.0 +use flate2::read::GzDecoder; +use rayon::prelude::*; use regex::Regex; use serde::de::DeserializeOwned; -use rayon::prelude::*; +use serde_json::{de::IoRead, Deserializer}; +use std::{ + collections::VecDeque, + error::Error, + fmt::{Display, Formatter}, + fs, + fs::File, + io, + io::BufReader, + path::{Path, PathBuf}, +}; #[derive(Debug)] pub enum JsonErr { @@ -12,7 +24,7 @@ pub enum JsonErr { /// A CSV parsing error that occurred while parsing the CSV data. JsonError(serde_json::Error), /// A GraphError that occurred while loading the CSV data into the graph. - GraphError(GraphError) + GraphError(GraphError), } impl From for JsonErr { @@ -60,18 +72,31 @@ pub struct JsonLinesLoader { /// Optional regex filter to select specific CSV files by name. regex_filter: Option, _a: std::marker::PhantomData, + /// print the name of the file being loaded + print_file_name: bool, } -impl JsonLinesLoader { +impl JsonLinesLoader { /// Creates a new CSV loader with the given path. pub fn new(path: PathBuf, regex_filter: Option) -> Self { Self { path, regex_filter, _a: std::marker::PhantomData, + print_file_name: false, } } + /// If set to true will print the file name as it reads it + /// + /// # Arguments + /// + /// * `p` - A boolean value indicating whether the CSV file has a header. + /// + pub fn set_print_file_name(mut self, p: bool) -> Self { + self.print_file_name = p; + self + } /// Check if the provided path is a directory or not. /// @@ -139,14 +164,12 @@ impl JsonLinesLoade while let Some(ref path) = queue.pop_back() { match fs::read_dir(path) { Ok(entries) => { - for entry in entries { - if let Ok(f_path) = entry { - let p = f_path.path(); - if Self::is_dir(&p)? { - queue.push_back(p.clone()) - } else { - self.accept_file(f_path.path(), &mut paths); - } + for f_path in entries.flatten() { + let p = f_path.path(); + if Self::is_dir(&p)? { + queue.push_back(p.clone()) + } else { + self.accept_file(f_path.path(), &mut paths); } } } @@ -181,7 +204,7 @@ impl JsonLinesLoade pub fn load_into_graph(&self, g: &G, loader: F) -> Result<(), JsonErr> where REC: DeserializeOwned + std::fmt::Debug, - F: Fn(REC, &G)->Result<(), GraphError> + Send + Sync, + F: Fn(REC, &G) -> Result<(), GraphError> + Send + Sync, G: Sync, { //FIXME: loader function should return a result for reporting parsing errors @@ -192,6 +215,37 @@ impl JsonLinesLoade Ok(()) } + fn json_reader( + &self, + file_path: PathBuf, + ) -> Result>>>, JsonErr> { + let is_gziped = file_path + .file_name() + .and_then(|name| name.to_str()) + .filter(|name| name.ends_with(".gz")) + .is_some(); + + let is_bziped = file_path + .file_name() + .and_then(|name| name.to_str()) + .filter(|name| name.ends_with(".bz2")) + .is_some(); + + let f = File::open(&file_path)?; + + if is_gziped { + Ok(Deserializer::from_reader(BufReader::new(Box::new( + GzDecoder::new(f), + )))) + } else if is_bziped { + Ok(Deserializer::from_reader(BufReader::new(Box::new( + BzDecoder::new(f), + )))) + } else { + Ok(Deserializer::from_reader(BufReader::new(Box::new(f)))) + } + } + /// Loads a JSON file into a graph using the specified loader function. /// /// # Arguments @@ -214,17 +268,19 @@ impl JsonLinesLoade F: Fn(REC, &G) -> Result<(), GraphError>, { let file_path: PathBuf = path.into(); + if self.print_file_name { + println!("Loading file: {:?}", file_path); + } + + let json_reader = self.json_reader(file_path)?; - let json_reader = serde_json::Deserializer::from_reader(std::io::BufReader::new( - std::fs::File::open(file_path)?, - )); let records_iter = json_reader.into_iter::(); //TODO this needs better error handling for files without perfect data for rec in records_iter { if let Ok(record) = rec { loader(record, g)? - } else{ + } else { println!("Error parsing record: {:?}", rec); } } @@ -232,3 +288,66 @@ impl JsonLinesLoade Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::prelude::*; + use bzip2::{write::BzEncoder, Compression as BzCompression}; + use flate2::{write::GzEncoder, Compression}; + use serde::Deserialize; + use std::{fs::File, io::Write}; + use tempfile::tempdir; + + #[derive(Debug, Deserialize)] + struct TestRecord { + name: String, + time: i64, + } + + fn test_json_rec(g: Graph, loader: JsonLinesLoader) { + loader + .load_into_graph(&g, |testrec: TestRecord, g: &Graph| { + let _ = g.add_vertex(testrec.time.clone(), testrec.name.clone(), NO_PROPS); + Ok(()) + }) + .expect("Unable to add vertex to graph"); + assert_eq!(g.count_vertices(), 3); + assert_eq!(g.count_edges(), 0); + let mut names = g.vertices().into_iter().name().collect::>(); + names.sort(); + assert_eq!(names, vec!["test", "testbz", "testgz"]); + } + + #[test] + fn test_load_into_graph() { + let dir = tempdir().unwrap(); + let plain_file = dir.path().join("test.json"); + let gzip_file = dir.path().join("test.json.gz"); + let bzip_file = dir.path().join("test.json.bz2"); + + // Create plain json file + File::create(&plain_file) + .unwrap() + .write_all(b"{\"name\": \"test\", \"time\": 1}\n") + .expect("unable to make plain file"); + + // Create gzip compressed json file + let f = File::create(&gzip_file).unwrap(); + let mut gz = GzEncoder::new(f, Compression::fast()); + gz.write_all(b"{\"name\": \"testgz\", \"time\": 2}\n") + .expect("unable to write to gz file"); + gz.finish().expect("Unable to write GZ file"); + + // Create bzip2 compressed json file + let f = File::create(&bzip_file).unwrap(); + let mut bz = BzEncoder::new(f, BzCompression::fast()); + bz.write_all(b"{\"name\": \"testbz\", \"time\": 3}\n") + .expect("unable to write to bz file"); + bz.finish().expect("Unable to write BZ file"); + + let g = Graph::new(); + let loader = JsonLinesLoader::::new(dir.path().to_path_buf(), None); + test_json_rec(g, loader); + } +} diff --git a/raphtory-io/src/graph_loader/source/mod.rs b/raphtory/src/graph_loader/source/mod.rs similarity index 73% rename from raphtory-io/src/graph_loader/source/mod.rs rename to raphtory/src/graph_loader/source/mod.rs index 150475e422..ea9fc4dc61 100644 --- a/raphtory-io/src/graph_loader/source/mod.rs +++ b/raphtory/src/graph_loader/source/mod.rs @@ -1,4 +1,3 @@ pub mod csv_loader; pub mod json_loader; pub mod neo4j_loader; -pub mod polars_loader; diff --git a/raphtory-io/src/graph_loader/source/neo4j_loader.rs b/raphtory/src/graph_loader/source/neo4j_loader.rs similarity index 78% rename from raphtory-io/src/graph_loader/source/neo4j_loader.rs rename to raphtory/src/graph_loader/source/neo4j_loader.rs index 406659e0df..f8646117e5 100644 --- a/raphtory-io/src/graph_loader/source/neo4j_loader.rs +++ b/raphtory/src/graph_loader/source/neo4j_loader.rs @@ -1,5 +1,5 @@ +use crate::db::graph::graph as rap; use neo4rs::*; -use raphtory::db::graph as rap; /// A struct that defines the Neo4J loader with configurable options. pub struct Neo4JConnection { // The created graph object given the arguments @@ -13,7 +13,7 @@ impl Neo4JConnection { password: String, database: String, ) -> Result { - let config = config() + let config = ConfigBuilder::default() .uri(uri.as_str()) .user(username.as_str()) .password(password.as_str()) @@ -24,11 +24,11 @@ impl Neo4JConnection { } pub async fn run(&self, query: Query) -> Result<()> { - Ok(self.neo_graph.run(query).await?) + self.neo_graph.run(query).await } pub async fn execute(&self, query: Query) -> Result { - Ok(self.neo_graph.execute(query).await?) + self.neo_graph.execute(query).await } pub async fn load_query_into_graph( @@ -48,11 +48,15 @@ impl Neo4JConnection { #[cfg(test)] mod neo_loader_test { - use crate::graph_loader::source::neo4j_loader::Neo4JConnection; + use crate::{ + db::{ + api::{mutation::AdditionOps, view::GraphViewOps}, + graph::graph as rap, + }, + graph_loader::source::neo4j_loader::Neo4JConnection, + prelude::{IntoProp, NO_PROPS}, + }; use neo4rs::*; - use raphtory::core::Prop; - use raphtory::db::graph as rap; - use raphtory::db::view_api::GraphViewOps; fn load_movies(row: Row, graph: &rap::Graph) { let film: Node = row.get("film").unwrap(); @@ -68,32 +72,24 @@ mod neo_loader_test { let relation_type = relation.typ(); graph - .add_vertex(actor_born, actor_name.clone(), &vec![]) + .add_vertex(actor_born, actor_name.clone(), NO_PROPS) + .unwrap() + .add_constant_properties([("type", "actor")]) .unwrap(); graph - .add_vertex_properties( - actor_name.clone(), - &vec![("type".into(), Prop::Str("actor".into()))], - ) - .unwrap(); - graph - .add_vertex(film_release, film_title.clone(), &vec![]) - .unwrap(); - graph - .add_vertex_properties( - film_title.clone(), - &vec![ - ("type".into(), Prop::Str("film".into())), - ("tagline".into(), Prop::Str(film_tagline)), - ], - ) + .add_vertex(film_release, film_title.clone(), NO_PROPS) + .unwrap() + .add_constant_properties([ + ("type", "film".into_prop()), + ("tagline", film_tagline.into_prop()), + ]) .unwrap(); graph .add_edge( film_release, actor_name, film_title, - &vec![], + NO_PROPS, Some(relation_type.as_str()), ) .unwrap(); @@ -110,7 +106,7 @@ mod neo_loader_test { ) .await .unwrap(); - let doc_graph = rap::Graph::new(1); + let doc_graph = rap::Graph::new(); neo.load_query_into_graph( &doc_graph, diff --git a/raphtory/src/graphgen/preferential_attachment.rs b/raphtory/src/graphgen/preferential_attachment.rs index 5a528d9b55..f1164ffdf8 100644 --- a/raphtory/src/graphgen/preferential_attachment.rs +++ b/raphtory/src/graphgen/preferential_attachment.rs @@ -5,15 +5,20 @@ //! # Examples //! //! ``` -//! use raphtory::db::graph::Graph; +//! use raphtory::prelude::*; //! use raphtory::graphgen::preferential_attachment::ba_preferential_attachment; //! -//! let graph = Graph::new(2); +//! let graph = Graph::new(); //! ba_preferential_attachment(&graph, 1000, 10); //! ``` -use crate::db::graph::Graph; -use crate::db::view_api::*; +use crate::{ + db::{ + api::{mutation::AdditionOps, view::*}, + graph::graph::Graph, + }, + prelude::NO_PROPS, +}; use rand::prelude::*; use std::collections::HashSet; @@ -37,10 +42,10 @@ use std::collections::HashSet; /// # Examples /// /// ``` -/// use raphtory::db::graph::Graph; +/// use raphtory::prelude::*; /// use raphtory::graphgen::preferential_attachment::ba_preferential_attachment; /// -/// let graph = Graph::new(2); +/// let graph = Graph::new(); /// ba_preferential_attachment(&graph, 1000, 10); /// ``` pub fn ba_preferential_attachment(graph: &Graph, vertices_to_add: usize, edges_per_step: usize) { @@ -60,17 +65,17 @@ pub fn ba_preferential_attachment(graph: &Graph, vertices_to_add: usize, edges_p while ids.len() < edges_per_step { max_id += 1; graph - .add_vertex(latest_time, max_id, &vec![]) + .add_vertex(latest_time, max_id, NO_PROPS) .map_err(|err| println!("{:?}", err)) .ok(); degrees.push(0); ids.push(max_id); } - if graph.num_edges() < edges_per_step { + if graph.count_edges() < edges_per_step { for pos in 1..ids.len() { graph - .add_edge(latest_time, ids[pos], ids[pos - 1], &vec![], None) + .add_edge(latest_time, ids[pos], ids[pos - 1], NO_PROPS, None) .expect("Not able to add edge"); edge_count += 2; degrees[pos] += 1; @@ -102,7 +107,7 @@ pub fn ba_preferential_attachment(graph: &Graph, vertices_to_add: usize, edges_p let dst = ids[pos]; degrees[pos] += 1; graph - .add_edge(latest_time, max_id, dst, &vec![], None) + .add_edge(latest_time, max_id, dst, NO_PROPS, None) .expect("Not able to add edge"); } ids.push(max_id); @@ -118,33 +123,33 @@ mod preferential_attachment_tests { use crate::graphgen::random_attachment::random_attachment; #[test] fn blank_graph() { - let graph = Graph::new(2); + let graph = Graph::new(); ba_preferential_attachment(&graph, 1000, 10); - assert_eq!(graph.num_edges(), 10009); - assert_eq!(graph.num_vertices(), 1010); + assert_eq!(graph.count_edges(), 10009); + assert_eq!(graph.count_vertices(), 1010); } #[test] fn only_nodes() { - let graph = Graph::new(2); + let graph = Graph::new(); for i in 0..10 { graph - .add_vertex(i, i as u64, &vec![]) + .add_vertex(i, i as u64, NO_PROPS) .map_err(|err| println!("{:?}", err)) .ok(); } ba_preferential_attachment(&graph, 1000, 5); - assert_eq!(graph.num_edges(), 5009); - assert_eq!(graph.num_vertices(), 1010); + assert_eq!(graph.count_edges(), 5009); + assert_eq!(graph.count_vertices(), 1010); } #[test] fn prior_graph() { - let graph = Graph::new(2); + let graph = Graph::new(); random_attachment(&graph, 1000, 3); ba_preferential_attachment(&graph, 500, 4); - assert_eq!(graph.num_edges(), 5000); - assert_eq!(graph.num_vertices(), 1503); + assert_eq!(graph.count_edges(), 5000); + assert_eq!(graph.count_vertices(), 1503); } } diff --git a/raphtory/src/graphgen/random_attachment.rs b/raphtory/src/graphgen/random_attachment.rs index 8e7e074ad4..c458ac355d 100644 --- a/raphtory/src/graphgen/random_attachment.rs +++ b/raphtory/src/graphgen/random_attachment.rs @@ -7,14 +7,19 @@ //! # Examples //! //! ``` -//! use raphtory::db::graph::Graph; +//! use raphtory::prelude::*; //! use raphtory::graphgen::random_attachment::random_attachment; -//! let graph = Graph::new(2); +//! let graph = Graph::new(); //! random_attachment(&graph, 1000, 10); //! ``` -use crate::db::graph::Graph; -use crate::db::view_api::*; +use crate::{ + db::{ + api::{mutation::AdditionOps, view::*}, + graph::graph::Graph, + }, + prelude::NO_PROPS, +}; use rand::seq::SliceRandom; /// Given a graph this function will add a user defined number of vertices, each with a @@ -34,9 +39,9 @@ use rand::seq::SliceRandom; /// # Examples /// /// ``` -/// use raphtory::db::graph::Graph; +/// use raphtory::prelude::*; /// use raphtory::graphgen::random_attachment::random_attachment; -/// let graph = Graph::new(2); +/// let graph = Graph::new(); /// random_attachment(&graph, 1000, 10); /// ``` pub fn random_attachment(graph: &Graph, vertices_to_add: usize, edges_per_step: usize) { @@ -49,7 +54,7 @@ pub fn random_attachment(graph: &Graph, vertices_to_add: usize, edges_per_step: max_id += 1; latest_time += 1; graph - .add_vertex(latest_time, max_id, &vec![]) + .add_vertex(latest_time, max_id, NO_PROPS) .map_err(|err| println!("{:?}", err)) .ok(); ids.push(max_id); @@ -61,7 +66,7 @@ pub fn random_attachment(graph: &Graph, vertices_to_add: usize, edges_per_step: latest_time += 1; edges.for_each(|neighbour| { graph - .add_edge(latest_time, max_id, *neighbour, &vec![], None) + .add_edge(latest_time, max_id, *neighbour, NO_PROPS, None) .expect("Not able to add edge"); }); ids.push(max_id); @@ -74,33 +79,33 @@ mod random_graph_test { use crate::graphgen::preferential_attachment::ba_preferential_attachment; #[test] fn blank_graph() { - let graph = Graph::new(2); + let graph = Graph::new(); random_attachment(&graph, 100, 20); - assert_eq!(graph.num_edges(), 2000); - assert_eq!(graph.num_vertices(), 120); + assert_eq!(graph.count_edges(), 2000); + assert_eq!(graph.count_vertices(), 120); } #[test] fn only_nodes() { - let graph = Graph::new(2); + let graph = Graph::new(); for i in 0..10 { graph - .add_vertex(i, i as u64, &vec![]) + .add_vertex(i, i as u64, NO_PROPS) .map_err(|err| println!("{:?}", err)) .ok(); } random_attachment(&graph, 1000, 5); - assert_eq!(graph.num_edges(), 5000); - assert_eq!(graph.num_vertices(), 1010); + assert_eq!(graph.count_edges(), 5000); + assert_eq!(graph.count_vertices(), 1010); } #[test] fn prior_graph() { - let graph = Graph::new(2); + let graph = Graph::new(); ba_preferential_attachment(&graph, 300, 7); random_attachment(&graph, 4000, 12); - assert_eq!(graph.num_edges(), 50106); - assert_eq!(graph.num_vertices(), 4307); + assert_eq!(graph.count_edges(), 50106); + assert_eq!(graph.count_vertices(), 4307); } } diff --git a/raphtory/src/lib.rs b/raphtory/src/lib.rs index 688915dc8c..9e53a8eac3 100644 --- a/raphtory/src/lib.rs +++ b/raphtory/src/lib.rs @@ -23,65 +23,42 @@ //! - **Fast** - raphtory is fast, and can process large amounts of data in a short amount of time. //! - **Open Source** - raphtory is open source, and is available on Github under a GPL-3.0 license. //! -//! ### Shards -//! -//! The sub module `Core` contains the underlying implementation of the graph. -//! Users interact with the graph via the `DB` submodule. -//! -//! The sub module `DB` is the overarching manager for the graph. A GraphDB instance can have N number of shards. -//! These shards (also called TemporalGraphParts) store fragments of a graph. -//! Each shard contains a part of a graph, similar to how data is partitioned. -//! -//! When an edge or node is added to the graph, GraphDB will search for an appropriate -//! place inside a shard to place these. -//! -//! For example, if your graph has 4 shards, altogether they make up the entire temporal graph. -//! Vertices and Edges will be spread across the varying shards. -//! -//! Shards are used for performance and distribution reasons. Having multiple shards running in -//! parallel increases the overall speed. In a matter of seconds, you are able to see your -//! results from your temporal graph analysis. Furthermore, you can run your analysis across -//! multiple machines (e.g. one shard per machine). -//! //! ## Example //! //! Create your own graph below //! ``` -//! use raphtory::db::graph::Graph; -//! use raphtory::core::Direction; -//! use raphtory::core::Prop; -//! use raphtory::db::view_api::*; +//! use raphtory::prelude::*; //! //! // Create your GraphDB object and state the number of shards you would like, here we have 2 -//! let graph = Graph::new(2); +//! let graph = Graph::new(); //! //! // Add vertex and edges to your graph with the respective properties //! graph.add_vertex( //! 1, //! "Gandalf", -//! &vec![("type".to_string(), Prop::Str("Character".to_string()))], -//! ); +//! [("type", Prop::str("Character"))], +//! ).unwrap(); //! //! graph.add_vertex( //! 2, //! "Frodo", -//! &vec![("type".to_string(), Prop::Str("Character".to_string()))], -//! ); +//! [("type", Prop::str("Character"))], +//! ).unwrap(); //! //! graph.add_edge( //! 3, //! "Gandalf", //! "Frodo", -//! &vec![( -//! "meeting".to_string(), -//! Prop::Str("Character Co-occurrence".to_string()), +//! [( +//! "meeting", +//! Prop::str("Character Co-occurrence"), //! )], //! None, -//! ); +//! ).unwrap(); //! //! // Get the in-degree, out-degree and degree of Gandalf -//! println!("Number of vertices {:?}", graph.num_vertices()); -//! println!("Number of Edges {:?}", graph.num_edges()); +//! println!("Number of vertices {:?}", graph.count_vertices()); +//! println!("Number of Edges {:?}", graph.count_edges()); //! ``` //! //! ## Supported Operating Systems @@ -112,3 +89,32 @@ pub mod algorithms; pub mod core; pub mod db; pub mod graphgen; + +#[cfg(feature = "python")] +pub mod python; + +#[cfg(feature = "io")] +pub mod graph_loader; + +#[cfg(feature = "search")] +pub mod search; + +#[cfg(feature = "vectors")] +pub mod vectors; + +pub mod prelude { + pub const NO_PROPS: [(&str, Prop); 0] = []; + pub use crate::{ + core::{IntoProp, Prop, PropUnwrap}, + db::{ + api::{ + mutation::{AdditionOps, DeletionOps, PropertyAdditionOps}, + view::{ + EdgeListOps, EdgeViewOps, GraphViewOps, Layer, LayerOps, TimeOps, + VertexListOps, VertexViewOps, + }, + }, + graph::graph::Graph, + }, + }; +} diff --git a/raphtory/src/python/graph/algorithm_result.rs b/raphtory/src/python/graph/algorithm_result.rs new file mode 100644 index 0000000000..dcf67888dc --- /dev/null +++ b/raphtory/src/python/graph/algorithm_result.rs @@ -0,0 +1,177 @@ +use ordered_float::OrderedFloat; +use pyo3::prelude::*; + +/// Create a macro for py_algorithm_result +macro_rules! py_algorithm_result { + ($name:ident, $rustKey:ty, $rustValue:ty, $rustSortValue:ty) => { + #[pyclass] + pub struct $name( + $crate::algorithms::algorithm_result::AlgorithmResult< + $rustKey, + $rustValue, + $rustSortValue, + >, + ); + + impl pyo3::IntoPy + for $crate::algorithms::algorithm_result::AlgorithmResult< + $rustKey, + $rustValue, + $rustSortValue, + > + { + fn into_py(self, py: Python<'_>) -> pyo3::PyObject { + $name(self).into_py(py) + } + } + }; + + ($name:ident, $rustKey:ty, $rustValue:ty) => { + py_algorithm_result!($name, $rustKey, $rustValue, $rustValue); + }; +} + +#[macro_export] +macro_rules! py_algorithm_result_base { + ($name:ident, $rustKey:ty, $rustValue:ty) => { + #[pymethods] + impl $name { + /// Returns a reference to the entire `result` hashmap. + fn get_all(&self) -> std::collections::HashMap<$rustKey, $rustValue> { + self.0.get_all().clone() + } + + /// Returns the value corresponding to the provided key in the `result` hashmap. + /// + /// # Arguments + /// + /// * `key`: The key of type `H` for which the value is to be retrieved. + fn get(&self, key: $rustKey) -> Option<$rustValue> { + self.0.get(&key).cloned() + } + + /// Creates a dataframe from the result + /// + /// # Returns + /// + /// A `pandas.DataFrame` containing the result + pub fn to_df(&self) -> PyResult { + let hashmap = &self.0.result; + let mut keys = Vec::new(); + let mut values = Vec::new(); + Python::with_gil(|py| { + for (key, value) in hashmap.iter() { + keys.push(key.to_object(py)); + values.push(value.to_object(py)); + } + let dict = pyo3::types::PyDict::new(py); + dict.set_item("Key", pyo3::types::PyList::new(py, keys.as_slice()))?; + dict.set_item("Value", pyo3::types::PyList::new(py, values.as_slice()))?; + let pandas = pyo3::types::PyModule::import(py, "pandas")?; + let df: &PyAny = pandas.getattr("DataFrame")?.call1((dict,))?; + Ok(df.to_object(py)) + }) + } + } + }; +} + +#[macro_export] +macro_rules! py_algorithm_result_partial_ord { + ($name:ident, $rustKey:ty, $rustValue:ty) => { + #[pymethods] + impl $name { + /// Sorts the `AlgorithmResult` by its values in ascending or descending order. + /// + /// # Arguments + /// + /// * `reverse`: If `true`, sorts the result in descending order; otherwise, sorts in ascending order. + /// + /// # Returns + /// + /// A sorted vector of tuples containing keys of type `H` and values of type `Y`. + #[pyo3(signature = (reverse=true))] + fn sort_by_value(&self, reverse: bool) -> Vec<($rustKey, $rustValue)> { + self.0.sort_by_value(reverse) + } + + /// Sorts the `AlgorithmResult` by its keys in ascending or descending order. + /// + /// # Arguments + /// + /// * `reverse`: If `true`, sorts the result in descending order; otherwise, sorts in ascending order. + /// + /// # Returns + /// + /// A sorted vector of tuples containing keys of type `H` and values of type `Y`. + #[pyo3(signature = (reverse=true))] + fn sort_by_key(&self, reverse: bool) -> Vec<($rustKey, $rustValue)> { + self.0.sort_by_key(reverse) + } + + /// Retrieves the top-k elements from the `AlgorithmResult` based on its values. + /// + /// # Arguments + /// + /// * `k`: The number of elements to retrieve. + /// * `percentage`: If `true`, the `k` parameter is treated as a percentage of total elements. + /// * `reverse`: If `true`, retrieves the elements in descending order; otherwise, in ascending order. + /// + /// # Returns + /// + /// An `Option` containing a vector of tuples with keys of type `H` and values of type `Y`. + /// If `percentage` is `true`, the returned vector contains the top `k` percentage of elements. + /// If `percentage` is `false`, the returned vector contains the top `k` elements. + /// Returns `None` if the result is empty or if `k` is 0. + #[pyo3(signature = (k, percentage=false, reverse=true))] + fn top_k( + &self, + k: usize, + percentage: bool, + reverse: bool, + ) -> Vec<($rustKey, $rustValue)> { + self.0.top_k(k, percentage, reverse) + } + } + py_algorithm_result_base!($name, $rustKey, $rustValue); + }; +} + +#[macro_export] +macro_rules! py_algorithm_result_ord_hash_eq { + ($name:ident, $rustKey:ty, $rustValue:ty) => { + #[pymethods] + impl $name { + /// Groups the `AlgorithmResult` by its values. + /// + /// # Returns + /// + /// A `HashMap` where keys are unique values from the `AlgorithmResult` and values are vectors + /// containing keys of type `H` that share the same value. + fn group_by(&self) -> std::collections::HashMap<$rustValue, Vec<$rustKey>> { + self.0.group_by() + } + } + py_algorithm_result_partial_ord!($name, $rustKey, $rustValue); + }; +} + +py_algorithm_result!(AlgorithmResultStrU64, String, u64); +py_algorithm_result_ord_hash_eq!(AlgorithmResultStrU64, String, u64); + +py_algorithm_result!( + AlgorithmResultStrTupleF32F32, + String, + (f32, f32), + (OrderedFloat, OrderedFloat) +); +py_algorithm_result_partial_ord!(AlgorithmResultStrTupleF32F32, String, (f32, f32)); + +py_algorithm_result!(AlgorithmResultStrVecI64Str, String, Vec<(i64, String)>); +py_algorithm_result_ord_hash_eq!(AlgorithmResultStrVecI64Str, String, Vec<(i64, String)>); + +py_algorithm_result!(AlgorithmResultU64VecUsize, u64, Vec); +py_algorithm_result_ord_hash_eq!(AlgorithmResultU64VecUsize, u64, Vec); + +py_algorithm_result!(AlgorithmResultStrF64, String, f64, OrderedFloat); +py_algorithm_result_partial_ord!(AlgorithmResultStrF64, String, f64); diff --git a/raphtory/src/python/graph/edge.rs b/raphtory/src/python/graph/edge.rs new file mode 100644 index 0000000000..1f2a4ca29f --- /dev/null +++ b/raphtory/src/python/graph/edge.rs @@ -0,0 +1,754 @@ +//! The edge module contains the PyEdge class, which is used to represent edges in the graph and +//! provides access to the edge's properties and vertices. +//! +//! The PyEdge class also provides access to the perspective APIs, which allow the user to view the +//! edge as it existed at a particular point in time, or as it existed over a particular time range. +//! +use crate::{ + core::{ + utils::{errors::GraphError, time::error::ParseTimeError}, + ArcStr, Direction, + }, + db::{ + api::{ + properties::Properties, + view::{ + internal::{DynamicGraph, Immutable, IntoDynamic, MaterializedGraph}, + BoxedIter, WindowSet, + }, + }, + graph::{ + edge::EdgeView, + views::{ + deletion_graph::GraphWithDeletions, layer_graph::LayeredGraph, + window_graph::WindowedGraph, + }, + }, + }, + prelude::*, + python::{ + graph::{ + properties::{PyNestedPropsIterable, PyPropsList}, + vertex::{PyNestedVertexIterable, PyVertex, PyVertexIterable}, + }, + types::{ + repr::{iterator_repr, Repr}, + wrappers::iterators::{ + NestedOptionI64Iterable, NestedU64U64Iterable, OptionI64Iterable, + }, + }, + utils::{PyGenericIterable, PyGenericIterator, PyInterval, PyTime}, + }, +}; +use chrono::NaiveDateTime; +use itertools::Itertools; +use pyo3::{prelude::*, pyclass::CompareOp}; +use std::{ + collections::{hash_map::DefaultHasher, HashMap}, + hash::{Hash, Hasher}, + ops::Deref, + sync::Arc, +}; + +/// PyEdge is a Python class that represents an edge in the graph. +/// An edge is a directed connection between two vertices. +#[pyclass(name = "Edge", subclass)] +pub struct PyEdge { + pub(crate) edge: EdgeView, +} + +#[pyclass(name="MutableEdge", extends=PyEdge)] +pub struct PyMutableEdge { + edge: EdgeView, +} + +impl From> for PyEdge { + fn from(value: EdgeView) -> Self { + Self { + edge: EdgeView { + graph: value.graph.clone().into_dynamic(), + edge: value.edge, + }, + } + } +} + +impl + GraphViewOps> From> for PyMutableEdge { + fn from(value: EdgeView) -> Self { + let edge = EdgeView { + edge: value.edge, + graph: value.graph.into(), + }; + + Self { edge } + } +} + +impl IntoPy for EdgeView { + fn into_py(self, py: Python<'_>) -> PyObject { + let py_version: PyEdge = self.into(); + py_version.into_py(py) + } +} + +impl IntoPy for EdgeView { + fn into_py(self, py: Python<'_>) -> PyObject { + let graph: MaterializedGraph = self.graph.into(); + let edge = self.edge; + let vertex = EdgeView { graph, edge }; + vertex.into_py(py) + } +} + +impl IntoPy for EdgeView { + fn into_py(self, py: Python<'_>) -> PyObject { + let graph: MaterializedGraph = self.graph.into(); + let edge = self.edge; + let vertex = EdgeView { graph, edge }; + vertex.into_py(py) + } +} + +impl IntoPy for EdgeView { + fn into_py(self, py: Python<'_>) -> PyObject { + Py::new(py, (PyMutableEdge::from(self.clone()), PyEdge::from(self))) + .unwrap() // I think this only fails if we are out of memory? Seems to be unavoidable! + .into_py(py) + } +} + +impl IntoPy for ArcStr { + fn into_py(self, py: Python<'_>) -> PyObject { + self.0.into_py(py) + } +} + +impl<'source> FromPyObject<'source> for ArcStr { + fn extract(ob: &'source PyAny) -> PyResult { + ob.extract::().map(|v| v.into()) + } +} + +/// PyEdge is a Python class that represents an edge in the graph. +/// An edge is a directed connection between two vertices. +#[pymethods] +impl PyEdge { + /// Rich Comparison for Vertex objects + pub fn __richcmp__(&self, other: PyRef, op: CompareOp) -> Py { + let py = other.py(); + match op { + CompareOp::Eq => (self.edge.id() == other.id()).into_py(py), + CompareOp::Ne => (self.edge.id() != other.id()).into_py(py), + _ => py.NotImplemented(), + } + } + + /// Returns the hash of the edge and edge properties. + /// + /// Returns: + /// A hash of the edge. + pub fn __hash__(&self) -> u64 { + let mut s = DefaultHasher::new(); + self.edge.id().hash(&mut s); + s.finish() + } + + /// The id of the edge. + #[getter] + pub fn id(&self) -> (u64, u64) { + self.edge.id() + } + + pub fn __getitem__(&self, name: &str) -> Option { + self.edge.properties().get(name) + } + + /// Returns a list of timestamps of when an edge is added or change to an edge is made. + /// + /// Returns: + /// A list of timestamps. + /// + pub fn history(&self) -> Vec { + self.edge.history() + } + + /// Returns a view of the properties of the edge. + #[getter] + pub fn properties(&self) -> Properties> { + self.edge.properties() + } + + /// Get the source vertex of the Edge. + /// + /// Returns: + /// The source vertex of the Edge. + #[getter] + fn src(&self) -> PyVertex { + self.edge.src().into() + } + + /// Get the destination vertex of the Edge. + /// + /// Returns: + /// The destination vertex of the Edge. + #[getter] + fn dst(&self) -> PyVertex { + self.edge.dst().into() + } + + //****** Perspective APIS ******// + + /// Get the start time of the Edge. + /// + /// Returns: + /// The start time of the Edge. + #[getter] + pub fn start(&self) -> Option { + self.edge.start() + } + + /// Get the start datetime of the Edge. + /// + /// Returns: + /// the start datetime of the Edge. + #[getter] + pub fn start_date_time(&self) -> Option { + let start_time = self.edge.start()?; + NaiveDateTime::from_timestamp_millis(start_time) + } + + /// Get the end time of the Edge. + /// + /// Returns: + /// The end time of the Edge. + #[getter] + pub fn end(&self) -> Option { + self.edge.end() + } + + /// Get the end datetime of the Edge. + /// + /// Returns: + /// The end datetime of the Edge + #[getter] + pub fn end_date_time(&self) -> Option { + let end_time = self.edge.end()?; + NaiveDateTime::from_timestamp_millis(end_time) + } + + /// Get the duration of the Edge. + /// + /// Arguments: + /// step (int): The step size to use when calculating the duration. + /// + /// Returns: + /// A set of windows containing edges that fall in the time period + #[pyo3(signature = (step))] + fn expanding( + &self, + step: PyInterval, + ) -> Result>, ParseTimeError> { + self.edge.expanding(step) + } + + /// Get a set of Edge windows for a given window size, step, start time + /// and end time using rolling window. + /// A rolling window is a window that moves forward by `step` size at each iteration. + /// + /// Arguments: + /// window (int | str): The size of the window. + /// step (int | str): The step size to use when calculating the duration. + /// + /// Returns: + /// A set of windows containing edges that fall in the time period + fn rolling( + &self, + window: PyInterval, + step: Option, + ) -> Result>, ParseTimeError> { + self.edge.rolling(window, step) + } + + /// Get a new Edge with the properties of this Edge within the specified time window. + /// + /// Arguments: + /// t_start (int | str): The start time of the window (optional). + /// t_end (int | str): The end time of the window (optional). + /// + /// Returns: + /// A new Edge with the properties of this Edge within the specified time window. + #[pyo3(signature = (t_start = None, t_end = None))] + pub fn window( + &self, + t_start: Option, + t_end: Option, + ) -> EdgeView> { + self.edge + .window(t_start.unwrap_or(PyTime::MIN), t_end.unwrap_or(PyTime::MAX)) + } + + /// Get a new Edge with the properties of this Edge within the specified layers. + /// + /// Arguments: + /// layer_names ([str]): Layers to be included in the new edge. + /// + /// Returns: + /// A new Edge with the properties of this Edge within the specified time window. + #[pyo3(signature = (layer_names))] + pub fn layers( + &self, + layer_names: Vec, + ) -> PyResult>> { + if let Some(edge) = self.edge.layer(layer_names.clone()) { + Ok(edge) + } else { + let available_layers: Vec<_> = self.edge.layer_names().collect(); + Err(PyErr::new::( + format!("Layers {layer_names:?} not available for edge, available layers: {available_layers:?}"), + )) + } + } + + /// Get a new Edge with the properties of this Edge at a specified time. + /// + /// Arguments: + /// end (int): The time to get the properties at. + /// + /// Returns: + /// A new Edge with the properties of this Edge at a specified time. + #[pyo3(signature = (end))] + pub fn at(&self, end: PyTime) -> EdgeView> { + self.edge.at(end) + } + + /// Explodes an Edge into a list of PyEdges. This is useful when you want to iterate over + /// the properties of an Edge at every single point in time. This will return a seperate edge + /// each time a property had been changed. + /// + /// Returns: + /// A list of PyEdges + pub fn explode(&self) -> PyEdges { + let edge = self.edge.clone(); + (move || edge.explode()).into() + } + + /// Explodes an Edge into a list of PyEdges, one for each layer the edge is part of. This is useful when you want to iterate over + /// the properties of an Edge for every layer. + /// + /// Returns: + /// A list of PyEdges + pub fn explode_layers(&self) -> PyEdges { + let edge = self.edge.clone(); + (move || edge.explode_layers()).into() + } + + /// Gets the earliest time of an edge. + /// + /// Returns: + /// (int) The earliest time of an edge + #[getter] + pub fn earliest_time(&self) -> Option { + self.edge.earliest_time() + } + + /// Gets of earliest datetime of an edge. + /// + /// Returns: + /// the earliest datetime of an edge + #[getter] + pub fn earliest_date_time(&self) -> Option { + NaiveDateTime::from_timestamp_millis(self.edge.earliest_time()?) + } + + /// Gets the latest time of an edge. + /// + /// Returns: + /// (int) The latest time of an edge + #[getter] + pub fn latest_time(&self) -> Option { + self.edge.latest_time() + } + + /// Gets of latest datetime of an edge. + /// + /// Returns: + /// the latest datetime of an edge + #[getter] + pub fn latest_date_time(&self) -> Option { + let latest_time = self.edge.latest_time()?; + NaiveDateTime::from_timestamp_millis(latest_time) + } + + /// Gets the time of an exploded edge. + /// + /// Returns: + /// (int) The time of an exploded edge + #[getter] + pub fn time(&self) -> Option { + self.edge.time() + } + + /// Gets the names of the layers this edge belongs to + /// + /// Returns: + /// ([str]) The name of the layer + #[getter] + pub fn layer_names(&self) -> Vec { + self.edge.layer_names().collect() + } + + /// Gets the name of the layer this edge belongs to - assuming it only belongs to one layer + /// + /// Returns: + /// ([str]) The name of the layer + #[getter] + pub fn layer_name(&self) -> Option { + self.edge.layer_name().map(|v| v.clone()) + } + + /// Gets the datetime of an exploded edge. + /// + /// Returns: + /// (datetime) the datetime of an exploded edge + #[getter] + pub fn date_time(&self) -> Option { + let date_time = self.edge.time()?; + NaiveDateTime::from_timestamp_millis(date_time) + } + + /// Displays the Edge as a string. + pub fn __repr__(&self) -> String { + self.repr() + } +} + +impl Repr for PyEdge { + fn repr(&self) -> String { + self.edge.repr() + } +} + +impl Repr for EdgeView { + fn repr(&self) -> String { + let properties: String = self + .properties() + .iter() + .map(|(k, v)| format!("{}: {}", k.deref(), v)) + .join(", "); + + let source = self.src().name(); + let target = self.dst().name(); + let earliest_time = self.earliest_time().repr(); + let latest_time = self.latest_time().repr(); + if properties.is_empty() { + format!( + "Edge(source={}, target={}, earliest_time={}, latest_time={})", + source.trim_matches('"'), + target.trim_matches('"'), + earliest_time, + latest_time, + ) + } else { + format!( + "Edge(source={}, target={}, earliest_time={}, latest_time={}, properties={})", + source.trim_matches('"'), + target.trim_matches('"'), + earliest_time, + latest_time, + format!("{{{properties}}}") + ) + } + } +} + +impl Repr for PyMutableEdge { + fn repr(&self) -> String { + self.edge.repr() + } +} +#[pymethods] +impl PyMutableEdge { + fn add_updates( + &self, + t: PyTime, + properties: Option>, + layer: Option<&str>, + ) -> Result<(), GraphError> { + self.edge + .add_updates(t, properties.unwrap_or_default(), layer) + } + + fn add_constant_properties( + &self, + properties: HashMap, + layer: Option<&str>, + ) -> Result<(), GraphError> { + self.edge.add_constant_properties(properties, layer) + } + + fn __repr__(&self) -> String { + self.repr() + } +} + +/// A list of edges that can be iterated over. +#[pyclass(name = "Edges")] +pub struct PyEdges { + builder: Arc BoxedIter> + Send + Sync + 'static>, +} + +impl PyEdges { + /// an iterable that can be used in rust + fn iter(&self) -> BoxedIter> { + (self.builder)() + } + + /// returns an iterable used in python + fn py_iter(&self) -> BoxedIter { + Box::new(self.iter().map(|e| e.into())) + } +} + +#[pymethods] +impl PyEdges { + fn __iter__(&self) -> PyGenericIterator { + self.py_iter().into() + } + + /// Returns all source vertices of the Edges as an iterable. + /// + /// Returns: + /// The source vertices of the Edges as an iterable. + #[getter] + fn src(&self) -> PyVertexIterable { + let builder = self.builder.clone(); + (move || builder().src()).into() + } + + /// Returns all destination vertices as an iterable + #[getter] + fn dst(&self) -> PyVertexIterable { + let builder = self.builder.clone(); + (move || builder().dst()).into() + } + + /// Returns all edges as a list + fn collect(&self) -> Vec { + self.py_iter().collect() + } + + /// Returns the number of edges + fn count(&self) -> usize { + self.py_iter().count() + } + + /// Explodes the edges into a list of edges. This is useful when you want to iterate over + /// the properties of an Edge at every single point in time. This will return a seperate edge + /// each time a property had been changed. + fn explode(&self) -> PyEdges { + let builder = self.builder.clone(); + (move || { + let iter: BoxedIter> = + Box::new(builder().flat_map(|e| e.explode())); + iter + }) + .into() + } + + /// Explodes each edge into a list of edges, one for each layer the edge is part of. This is useful when you want to iterate over + /// the properties of an Edge for every layer. + fn explode_layers(&self) -> PyEdges { + let builder = self.builder.clone(); + (move || { + let iter: BoxedIter> = + Box::new(builder().flat_map(|e| e.explode_layers())); + iter + }) + .into() + } + + /// Returns the earliest time of the edges. + #[getter] + fn earliest_time(&self) -> OptionI64Iterable { + let edges: Arc< + dyn Fn() -> Box> + Send> + Send + Sync, + > = self.builder.clone(); + (move || edges().earliest_time()).into() + } + + /// Returns the latest time of the edges. + #[getter] + fn latest_time(&self) -> OptionI64Iterable { + let edges: Arc< + dyn Fn() -> Box> + Send> + Send + Sync, + > = self.builder.clone(); + (move || edges().latest_time()).into() + } + + /// Returns all properties of the edges + #[getter] + fn properties(&self) -> PyPropsList { + let builder = self.builder.clone(); + (move || builder().properties()).into() + } + + /// Returns all ids of the edges. + #[getter] + fn id(&self) -> PyGenericIterable { + let edges = self.builder.clone(); + (move || edges().id()).into() + } + + fn __repr__(&self) -> String { + self.repr() + } +} + +impl Repr for PyEdges { + fn repr(&self) -> String { + format!("Edges({})", iterator_repr(self.iter())) + } +} + +impl BoxedIter> + Send + Sync + 'static> From for PyEdges { + fn from(value: F) -> Self { + Self { + builder: Arc::new(value), + } + } +} + +py_nested_iterable!(PyNestedEdges, EdgeView); + +#[pymethods] +impl PyNestedEdges { + /// Returns all source vertices of the Edges as an iterable. + /// + /// Returns: + /// The source verticeÍs of the Edges as an iterable. + #[getter] + fn src(&self) -> PyNestedVertexIterable { + let builder = self.builder.clone(); + (move || builder().src()).into() + } + + /// Returns all destination vertices as an iterable + #[getter] + fn dst(&self) -> PyNestedVertexIterable { + let builder = self.builder.clone(); + (move || builder().dst()).into() + } + + /// Returns the earliest time of the edges. + #[getter] + fn earliest_time(&self) -> NestedOptionI64Iterable { + let edges = self.builder.clone(); + (move || edges().earliest_time()).into() + } + + /// Returns the latest time of the edges. + #[getter] + fn latest_time(&self) -> NestedOptionI64Iterable { + let edges = self.builder.clone(); + (move || edges().latest_time()).into() + } + + // FIXME: needs a view that allows indexing into the properties + /// Returns all properties of the edges + #[getter] + fn properties(&self) -> PyNestedPropsIterable { + let builder = self.builder.clone(); + (move || builder().properties()).into() + } + + /// Returns all ids of the edges. + #[getter] + fn id(&self) -> NestedU64U64Iterable { + let edges = self.builder.clone(); + (move || edges().id()).into() + } + + /// Explode each edge, creating a separate edge instance for each edge event + fn explode(&self) -> PyNestedEdges { + let builder = self.builder.clone(); + (move || { + let iter: BoxedIter>> = Box::new(builder().map(|e| { + let inner_box: BoxedIter> = + Box::new(e.flat_map(|e| e.explode())); + inner_box + })); + iter + }) + .into() + } + + /// Explode each edge over layers, creating a separate edge instance for each layer the edge is part of + fn explode_layers(&self) -> PyNestedEdges { + let builder = self.builder.clone(); + (move || { + let iter: BoxedIter>> = Box::new(builder().map(|e| { + let inner_box: BoxedIter> = + Box::new(e.flat_map(|e| e.explode_layers())); + inner_box + })); + iter + }) + .into() + } +} + +#[pyclass] +#[derive(Clone)] +pub struct PyDirection { + inner: Direction, +} + +#[pymethods] +impl PyDirection { + #[new] + pub fn new(direction: &str) -> Self { + match direction { + "OUT" => PyDirection { + inner: Direction::OUT, + }, + "IN" => PyDirection { + inner: Direction::IN, + }, + "BOTH" => PyDirection { + inner: Direction::BOTH, + }, + _ => panic!("Invalid direction"), + } + } + + fn as_str(&self) -> &str { + match self.inner { + Direction::OUT => "OUT", + Direction::IN => "IN", + Direction::BOTH => "BOTH", + } + } +} + +impl Into for PyDirection { + fn into(self) -> Direction { + self.inner + } +} + +impl From for PyDirection { + fn from(s: String) -> Self { + match s.to_uppercase().as_str() { + "OUT" => PyDirection { + inner: Direction::OUT, + }, + "IN" => PyDirection { + inner: Direction::IN, + }, + "BOTH" => PyDirection { + inner: Direction::BOTH, + }, + _ => panic!("Invalid direction string"), + } + } +} diff --git a/raphtory/src/python/graph/graph.rs b/raphtory/src/python/graph/graph.rs new file mode 100644 index 0000000000..4741c7dbd8 --- /dev/null +++ b/raphtory/src/python/graph/graph.rs @@ -0,0 +1,460 @@ +//! Defines the `Graph` struct, which represents a raphtory graph in memory. +//! +//! This is the base class used to create a temporal graph, add vertices and edges, +//! create windows, and query the graph with a variety of algorithms. +//! It is a wrapper around a set of shards, which are the actual graph data structures. +//! In Python, this class wraps around the rust graph. +use crate::{ + core::utils::errors::GraphError, + db::api::view::internal::MaterializedGraph, + prelude::*, + python::{ + graph::{graph_with_deletions::PyGraphWithDeletions, views::graph_view::PyGraphView}, + utils::{PyInputVertex, PyTime}, + }, +}; +use pyo3::prelude::*; + +use crate::{ + core::entities::vertices::vertex_ref::VertexRef, + db::{ + api::view::internal::{DynamicGraph, IntoDynamic}, + graph::{edge::EdgeView, vertex::VertexView}, + }, + python::graph::pandas::{load_edges_props_from_df, load_vertex_props_from_df}, +}; +use pyo3::types::{IntoPyDict, PyBytes}; +use std::{ + collections::HashMap, + fmt::{Debug, Formatter}, + path::{Path, PathBuf}, +}; + +use super::pandas::{ + load_edges_from_df, load_vertices_from_df, process_pandas_py_df, GraphLoadException, +}; + +/// A temporal graph. +#[derive(Clone)] +#[pyclass(name="Graph", extends=PyGraphView)] +pub struct PyGraph { + pub graph: Graph, +} + +impl Debug for PyGraph { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.graph) + } +} + +impl From for PyGraph { + fn from(value: Graph) -> Self { + Self { graph: value } + } +} + +impl From for Graph { + fn from(value: PyGraph) -> Self { + value.graph + } +} + +impl From for DynamicGraph { + fn from(value: PyGraph) -> Self { + value.graph.into_dynamic() + } +} + +impl<'source> FromPyObject<'source> for MaterializedGraph { + fn extract(graph: &'source PyAny) -> PyResult { + if let Ok(graph) = graph.extract::>() { + Ok(graph.graph.clone().into()) + } else if let Ok(graph) = graph.extract::>() { + Ok(graph.graph.clone().into()) + } else { + Err(PyErr::new::(format!( + "Incorrect type, object is not a PyGraph or PyGraphWithDeletions" + ))) + } + } +} + +impl IntoPy for Graph { + fn into_py(self, py: Python<'_>) -> PyObject { + Py::new(py, (PyGraph::from(self.clone()), PyGraphView::from(self))) + .unwrap() // I think this only fails if we are out of memory? Seems to be unavoidable if we want to create an actual graph. + .into_py(py) + } +} + +impl<'source> FromPyObject<'source> for Graph { + fn extract(ob: &'source PyAny) -> PyResult { + let g: PyRef = ob.extract()?; + Ok(g.graph.clone()) + } +} + +impl PyGraph { + pub fn py_from_db_graph(db_graph: Graph) -> PyResult> { + Python::with_gil(|py| { + Py::new( + py, + (PyGraph::from(db_graph.clone()), PyGraphView::from(db_graph)), + ) + }) + } +} + +/// A temporal graph. +#[pymethods] +impl PyGraph { + #[new] + pub fn py_new() -> (Self, PyGraphView) { + let graph = Graph::new(); + ( + Self { + graph: graph.clone(), + }, + PyGraphView::from(graph), + ) + } + + /// Adds a new vertex with the given id and properties to the graph. + /// + /// Arguments: + /// timestamp (int, str, or datetime(utc)): The timestamp of the vertex. + /// id (str or int): The id of the vertex. + /// properties (dict): The properties of the vertex. + /// + /// Returns: + /// None + #[pyo3(signature = (timestamp, id, properties=None))] + pub fn add_vertex( + &self, + timestamp: PyTime, + id: PyInputVertex, + properties: Option>, + ) -> Result, GraphError> { + self.graph + .add_vertex(timestamp, id, properties.unwrap_or_default()) + } + + /// Adds properties to the graph. + /// + /// Arguments: + /// timestamp (int, str, or datetime(utc)): The timestamp of the temporal property. + /// properties (dict): The temporal properties of the graph. + /// + /// Returns: + /// None + pub fn add_property( + &self, + timestamp: PyTime, + properties: HashMap, + ) -> Result<(), GraphError> { + self.graph.add_properties(timestamp, properties) + } + + /// Adds static properties to the graph. + /// + /// Arguments: + /// properties (dict): The static properties of the graph. + /// + /// Returns: + /// None + pub fn add_constant_properties( + &self, + properties: HashMap, + ) -> Result<(), GraphError> { + self.graph.add_constant_properties(properties) + } + + /// Adds a new edge with the given source and destination vertices and properties to the graph. + /// + /// Arguments: + /// timestamp (int): The timestamp of the edge. + /// src (str or int): The id of the source vertex. + /// dst (str or int): The id of the destination vertex. + /// properties (dict): The properties of the edge, as a dict of string and properties + /// layer (str): The layer of the edge. + /// + /// Returns: + /// None + #[pyo3(signature = (timestamp, src, dst, properties=None, layer=None))] + pub fn add_edge( + &self, + timestamp: PyTime, + src: PyInputVertex, + dst: PyInputVertex, + properties: Option>, + layer: Option<&str>, + ) -> Result, GraphError> { + self.graph + .add_edge(timestamp, src, dst, properties.unwrap_or_default(), layer) + } + + //FIXME: This is reimplemented here to get mutable views. If we switch the underlying graph to enum dispatch, this won't be necessary! + /// Gets the vertex with the specified id + /// + /// Arguments: + /// id (str or int): the vertex id + /// + /// Returns: + /// the vertex with the specified id, or None if the vertex does not exist + pub fn vertex(&self, id: VertexRef) -> Option> { + self.graph.vertex(id) + } + + //FIXME: This is reimplemented here to get mutable views. If we switch the underlying graph to enum dispatch, this won't be necessary! + /// Gets the edge with the specified source and destination vertices + /// + /// Arguments: + /// src (str or int): the source vertex id + /// dst (str or int): the destination vertex id + /// layer (str): the edge layer (optional) + /// + /// Returns: + /// the edge with the specified source and destination vertices, or None if the edge does not exist + #[pyo3(signature = (src, dst))] + pub fn edge(&self, src: VertexRef, dst: VertexRef) -> Option> { + self.graph.edge(src, dst) + } + + //****** Saving And Loading ******// + + // Alternative constructors are tricky, see: https://gist.github.com/redshiftzero/648e4feeff3843ffd9924f13625f839c + + /// Loads a graph from the given path. + /// + /// Arguments: + /// path (str): The path to the graph. + /// + /// Returns: + /// Graph: The loaded graph. + #[staticmethod] + pub fn load_from_file(path: &str) -> Result { + let file_path: PathBuf = [env!("CARGO_MANIFEST_DIR"), path].iter().collect(); + Graph::load_from_file(file_path) + } + + /// Saves the graph to the given path. + /// + /// Arguments: + /// path (str): The path to the graph. + /// + /// Returns: + /// None + pub fn save_to_file(&self, path: &str) -> Result<(), GraphError> { + self.graph.save_to_file(Path::new(path)) + } + + /// Get bincode encoded graph + pub fn bincode<'py>(&'py self, py: Python<'py>) -> Result<&'py PyBytes, GraphError> { + let bytes = MaterializedGraph::from(self.graph.clone()).bincode()?; + Ok(PyBytes::new(py, &bytes)) + } + + #[staticmethod] + #[pyo3(signature = (edges_df, src = "source", dst = "destination", time = "time", props = None, const_props=None,shared_const_props=None,layer = None, layer_in_df = None, vertex_df = None, vertex_col = None, vertex_time_col = None, vertex_props = None, vertex_const_props = None, vertex_shared_const_props = None))] + fn load_from_pandas( + edges_df: &PyAny, + src: &str, + dst: &str, + time: &str, + props: Option>, + const_props: Option>, + shared_const_props: Option>, + layer: Option<&str>, + layer_in_df: Option<&str>, + vertex_df: Option<&PyAny>, + vertex_col: Option<&str>, + vertex_time_col: Option<&str>, + vertex_props: Option>, + vertex_const_props: Option>, + vertex_shared_const_props: Option>, + ) -> Result { + let graph = PyGraph { + graph: Graph::new(), + }; + graph.load_edges_from_pandas( + edges_df, + src, + dst, + time, + props, + const_props, + shared_const_props, + layer, + layer_in_df, + )?; + if let (Some(vertex_df), Some(vertex_col), Some(vertex_time_col)) = + (vertex_df, vertex_col, vertex_time_col) + { + graph.load_vertices_from_pandas( + vertex_df, + vertex_col, + vertex_time_col, + vertex_props, + vertex_const_props, + vertex_shared_const_props, + )?; + } + Ok(graph.graph) + } + + #[pyo3(signature = (vertices_df, vertex_col = "id", time_col = "time", props = None, const_props = None, shared_const_props = None))] + fn load_vertices_from_pandas( + &self, + vertices_df: &PyAny, + vertex_col: &str, + time_col: &str, + props: Option>, + const_props: Option>, + shared_const_props: Option>, + ) -> Result<(), GraphError> { + let graph = &self.graph; + Python::with_gil(|py| { + let size: usize = py + .eval( + "index.__len__()", + Some([("index", vertices_df.getattr("index")?)].into_py_dict(py)), + None, + )? + .extract()?; + let df = process_pandas_py_df(vertices_df, py, size)?; + load_vertices_from_df( + &df, + size, + vertex_col, + time_col, + props, + const_props, + shared_const_props, + graph, + ) + .map_err(|e| GraphLoadException::new_err(format!("{:?}", e)))?; + + Ok::<(), PyErr>(()) + }) + .map_err(|e| GraphError::LoadFailure(format!("Failed to load graph {e:?}")))?; + Ok(()) + } + + #[pyo3(signature = (edge_df, src_col = "source", dst_col = "destination", time_col = "time", props = None, const_props=None,shared_const_props=None,layer=None,layer_in_df=None))] + fn load_edges_from_pandas( + &self, + edge_df: &PyAny, + src_col: &str, + dst_col: &str, + time_col: &str, + props: Option>, + const_props: Option>, + shared_const_props: Option>, + layer: Option<&str>, + layer_in_df: Option<&str>, + ) -> Result<(), GraphError> { + let graph = &self.graph; + Python::with_gil(|py| { + let size: usize = py + .eval( + "index.__len__()", + Some([("index", edge_df.getattr("index")?)].into_py_dict(py)), + None, + )? + .extract()?; + let df = process_pandas_py_df(edge_df, py, size)?; + load_edges_from_df( + &df, + size, + src_col, + dst_col, + time_col, + props, + const_props, + shared_const_props, + layer, + layer_in_df, + graph, + ) + .map_err(|e| GraphLoadException::new_err(format!("{:?}", e)))?; + + Ok::<(), PyErr>(()) + }) + .map_err(|e| GraphError::LoadFailure(format!("Failed to load graph {e:?}")))?; + Ok(()) + } + + #[pyo3(signature = (vertices_df, vertex_col = "id", const_props = None, shared_const_props = None))] + fn load_vertex_props_from_pandas( + &self, + vertices_df: &PyAny, + vertex_col: &str, + const_props: Option>, + shared_const_props: Option>, + ) -> Result<(), GraphError> { + let graph = &self.graph; + Python::with_gil(|py| { + let size: usize = py + .eval( + "index.__len__()", + Some([("index", vertices_df.getattr("index")?)].into_py_dict(py)), + None, + )? + .extract()?; + let df = process_pandas_py_df(vertices_df, py, size)?; + load_vertex_props_from_df( + &df, + size, + vertex_col, + const_props, + shared_const_props, + graph, + ) + .map_err(|e| GraphLoadException::new_err(format!("{:?}", e)))?; + + Ok::<(), PyErr>(()) + }) + .map_err(|e| GraphError::LoadFailure(format!("Failed to load graph {e:?}")))?; + Ok(()) + } + + #[pyo3(signature = (edge_df, src_col = "source", dst_col = "destination", const_props=None,shared_const_props=None,layer=None,layer_in_df=None))] + fn load_edge_props_from_pandas( + &self, + edge_df: &PyAny, + src_col: &str, + dst_col: &str, + const_props: Option>, + shared_const_props: Option>, + layer: Option<&str>, + layer_in_df: Option<&str>, + ) -> Result<(), GraphError> { + let graph = &self.graph; + Python::with_gil(|py| { + let size: usize = py + .eval( + "index.__len__()", + Some([("index", edge_df.getattr("index")?)].into_py_dict(py)), + None, + )? + .extract()?; + let df = process_pandas_py_df(edge_df, py, size)?; + load_edges_props_from_df( + &df, + size, + src_col, + dst_col, + const_props, + shared_const_props, + layer, + layer_in_df, + graph, + ) + .map_err(|e| GraphLoadException::new_err(format!("{:?}", e)))?; + + Ok::<(), PyErr>(()) + }) + .map_err(|e| GraphError::LoadFailure(format!("Failed to load graph {e:?}")))?; + Ok(()) + } +} diff --git a/raphtory/src/python/graph/graph_with_deletions.rs b/raphtory/src/python/graph/graph_with_deletions.rs new file mode 100644 index 0000000000..970331fb38 --- /dev/null +++ b/raphtory/src/python/graph/graph_with_deletions.rs @@ -0,0 +1,245 @@ +//! Defines the `GraphWithDeletions` class, which represents a raphtory graph in memory. +//! Unlike in the `Graph` which has event semantics, `GraphWithDeletions` has edges that persist until explicitly deleted. +//! +//! This is the base class used to create a temporal graph, add vertices and edges, +//! create windows, and query the graph with a variety of algorithms. +//! It is a wrapper around a set of shards, which are the actual graph data structures. +//! In Python, this class wraps around the rust graph. +use crate::{ + core::{entities::vertices::vertex_ref::VertexRef, utils::errors::GraphError, Prop}, + db::{ + api::{ + mutation::{AdditionOps, PropertyAdditionOps}, + view::internal::MaterializedGraph, + }, + graph::{edge::EdgeView, vertex::VertexView, views::deletion_graph::GraphWithDeletions}, + }, + prelude::{DeletionOps, GraphViewOps}, + python::{ + graph::views::graph_view::PyGraphView, + utils::{PyInputVertex, PyTime}, + }, +}; +use pyo3::{prelude::*, types::PyBytes}; +use std::{ + collections::HashMap, + fmt::{Debug, Formatter}, + path::{Path, PathBuf}, +}; + +/// A temporal graph. +#[derive(Clone)] +#[pyclass(name="GraphWithDeletions", extends=PyGraphView)] +pub struct PyGraphWithDeletions { + pub(crate) graph: GraphWithDeletions, +} + +impl Debug for PyGraphWithDeletions { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.graph) + } +} + +impl From for PyGraphWithDeletions { + fn from(value: GraphWithDeletions) -> Self { + Self { graph: value } + } +} + +impl IntoPy for GraphWithDeletions { + fn into_py(self, py: Python<'_>) -> PyObject { + Py::new( + py, + ( + PyGraphWithDeletions::from(self.clone()), + PyGraphView::from(self), + ), + ) + .unwrap() // I think this only fails if we are out of memory? Seems to be unavoidable if we want to create an actual graph. + .into_py(py) + } +} + +impl PyGraphWithDeletions { + pub fn py_from_db_graph(db_graph: GraphWithDeletions) -> PyResult> { + Python::with_gil(|py| { + Py::new( + py, + ( + PyGraphWithDeletions::from(db_graph.clone()), + PyGraphView::from(db_graph), + ), + ) + }) + } +} + +/// A temporal graph. +#[pymethods] +impl PyGraphWithDeletions { + #[new] + pub fn py_new() -> (Self, PyGraphView) { + let graph = GraphWithDeletions::new(); + ( + Self { + graph: graph.clone(), + }, + PyGraphView::from(graph), + ) + } + + /// Adds a new vertex with the given id and properties to the graph. + /// + /// Arguments: + /// timestamp (int, str, or datetime(utc)): The timestamp of the vertex. + /// id (str or int): The id of the vertex. + /// properties (dict): The properties of the vertex. + /// + /// Returns: + /// None + #[pyo3(signature = (timestamp, id, properties=None))] + pub fn add_vertex( + &self, + timestamp: PyTime, + id: PyInputVertex, + properties: Option>, + ) -> Result, GraphError> { + self.graph + .add_vertex(timestamp, id, properties.unwrap_or_default()) + } + + /// Adds properties to the graph. + /// + /// Arguments: + /// timestamp (int, str, or datetime(utc)): The timestamp of the temporal property. + /// properties (dict): The temporal properties of the graph. + /// + /// Returns: + /// None + pub fn add_property( + &self, + timestamp: PyTime, + properties: HashMap, + ) -> Result<(), GraphError> { + self.graph.add_properties(timestamp, properties) + } + + /// Adds static properties to the graph. + /// + /// Arguments: + /// properties (dict): The static properties of the graph. + /// + /// Returns: + /// None + pub fn add_constant_properties( + &self, + properties: HashMap, + ) -> Result<(), GraphError> { + self.graph.add_constant_properties(properties) + } + + /// Adds a new edge with the given source and destination vertices and properties to the graph. + /// + /// Arguments: + /// timestamp (int): The timestamp of the edge. + /// src (str or int): The id of the source vertex. + /// dst (str or int): The id of the destination vertex. + /// properties (dict): The properties of the edge, as a dict of string and properties + /// layer (str): The layer of the edge. + /// + /// Returns: + /// None + #[pyo3(signature = (timestamp, src, dst, properties=None, layer=None))] + pub fn add_edge( + &self, + timestamp: PyTime, + src: PyInputVertex, + dst: PyInputVertex, + properties: Option>, + layer: Option<&str>, + ) -> Result, GraphError> { + self.graph + .add_edge(timestamp, src, dst, properties.unwrap_or_default(), layer) + } + + /// Deletes an edge given the timestamp, src and dst vertices and layer (optional) + /// + /// Arguments: + /// timestamp (int): The timestamp of the edge. + /// src (str or int): The id of the source vertex. + /// dst (str or int): The id of the destination vertex. + /// layer (str): The layer of the edge. (optional) + /// + /// Returns: + /// None or a GraphError if the edge could not be deleted + pub fn delete_edge( + &self, + timestamp: PyTime, + src: PyInputVertex, + dst: PyInputVertex, + layer: Option<&str>, + ) -> Result<(), GraphError> { + self.graph.delete_edge(timestamp, src, dst, layer) + } + + //FIXME: This is reimplemented here to get mutable views. If we switch the underlying graph to enum dispatch, this won't be necessary! + /// Gets the vertex with the specified id + /// + /// Arguments: + /// id (str or int): the vertex id + /// + /// Returns: + /// the vertex with the specified id, or None if the vertex does not exist + pub fn vertex(&self, id: VertexRef) -> Option> { + self.graph.vertex(id) + } + + //FIXME: This is reimplemented here to get mutable views. If we switch the underlying graph to enum dispatch, this won't be necessary! + /// Gets the edge with the specified source and destination vertices + /// + /// Arguments: + /// src (str or int): the source vertex id + /// dst (str or int): the destination vertex id + /// layer (str): the edge layer (optional) + /// + /// Returns: + /// the edge with the specified source and destination vertices, or None if the edge does not exist + #[pyo3(signature = (src, dst))] + pub fn edge(&self, src: VertexRef, dst: VertexRef) -> Option> { + self.graph.edge(src, dst) + } + + //****** Saving And Loading ******// + + // Alternative constructors are tricky, see: https://gist.github.com/redshiftzero/648e4feeff3843ffd9924f13625f839c + + /// Loads a graph from the given path. + /// + /// Arguments: + /// path (str): The path to the graph. + /// + /// Returns: + /// Graph: The loaded graph. + #[staticmethod] + pub fn load_from_file(path: &str) -> Result { + let file_path: PathBuf = [env!("CARGO_MANIFEST_DIR"), path].iter().collect(); + GraphWithDeletions::load_from_file(file_path) + } + + /// Saves the graph to the given path. + /// + /// Arguments: + /// path (str): The path to the graph. + /// + /// Returns: + /// None + pub fn save_to_file(&self, path: &str) -> Result<(), GraphError> { + self.graph.save_to_file(Path::new(path)) + } + + /// Get bincode encoded graph + pub fn bincode<'py>(&'py self, py: Python<'py>) -> Result<&'py PyBytes, GraphError> { + let bytes = MaterializedGraph::from(self.graph.clone()).bincode()?; + Ok(PyBytes::new(py, &bytes)) + } +} diff --git a/raphtory/src/python/graph/mod.rs b/raphtory/src/python/graph/mod.rs new file mode 100644 index 0000000000..f9b42d9891 --- /dev/null +++ b/raphtory/src/python/graph/mod.rs @@ -0,0 +1,8 @@ +pub mod algorithm_result; +pub mod edge; +pub mod graph; +pub mod graph_with_deletions; +pub mod pandas; +pub mod properties; +pub mod vertex; +pub mod views; diff --git a/raphtory/src/python/graph/pandas.rs b/raphtory/src/python/graph/pandas.rs new file mode 100644 index 0000000000..1b15982231 --- /dev/null +++ b/raphtory/src/python/graph/pandas.rs @@ -0,0 +1,843 @@ +use crate::{core::utils::errors::GraphError, prelude::*}; +use arrow2::{ + array::{Array, BooleanArray, PrimitiveArray, Utf8Array}, + ffi, + types::{NativeType, Offset}, +}; +use kdam::tqdm; +use pyo3::{ + create_exception, exceptions::PyException, ffi::Py_uintptr_t, prelude::*, types::PyDict, +}; +use std::collections::HashMap; + +fn i64_opt_into_u64_opt(x: Option<&i64>) -> Option { + x.map(|x| (*x).try_into().unwrap()) +} + +pub(crate) fn process_pandas_py_df(df: &PyAny, py: Python, size: usize) -> PyResult { + let globals = PyDict::new(py); + globals.set_item("df", df)?; + let module = py.import("pyarrow")?; + let pa_table = module.getattr("Table")?; + + let table = pa_table.call_method("from_pandas", (df,), None)?; + + let rb = table.call_method0("to_batches")?.extract::>()?; + let names = if let Some(batch0) = rb.get(0) { + let schema = batch0.getattr("schema")?; + schema.getattr("names")?.extract::>()? + } else { + vec![] + }; + + let arrays = tqdm!( + rb.iter().map(|rb| { + (0..names.len()) + .map(|i| { + let array = rb.call_method1("column", (i,))?; + let arr = array_to_rust(array)?; + Ok::, PyErr>(arr) + }) + .collect::, PyErr>>() + }), + desc = "Converting dataframe to Arrow", + total = size, + animation = kdam::Animation::FillUp, + unit_scale = true + ) + .collect::, PyErr>>()?; + + let df = PretendDF { names, arrays }; + Ok(df) +} + +pub(crate) fn load_vertices_from_df<'a>( + df: &'a PretendDF, + size: usize, + vertex_id: &str, + time: &str, + props: Option>, + const_props: Option>, + shared_const_props: Option>, + graph: &Graph, +) -> Result<(), GraphError> { + let prop_iter = props + .unwrap_or_default() + .into_iter() + .map(|name| lift_property(name, &df)) + .reduce(combine_prop_iters) + .unwrap_or_else(|| Box::new(std::iter::repeat(vec![]))); + + let const_prop_iter = const_props + .unwrap_or_default() + .into_iter() + .map(|name| lift_property(name, &df)) + .reduce(combine_prop_iters) + .unwrap_or_else(|| Box::new(std::iter::repeat(vec![]))); + + if let (Some(vertex_id), Some(time)) = (df.iter_col::(vertex_id), df.iter_col::(time)) + { + let iter = vertex_id.map(|i| i.copied()).zip(time); + load_vertices_from_num_iter( + graph, + size, + iter, + prop_iter, + const_prop_iter, + shared_const_props, + )?; + } else if let (Some(vertex_id), Some(time)) = + (df.iter_col::(vertex_id), df.iter_col::(time)) + { + let iter = vertex_id.map(i64_opt_into_u64_opt).zip(time); + load_vertices_from_num_iter( + graph, + size, + iter, + prop_iter, + const_prop_iter, + shared_const_props, + )?; + } else if let (Some(vertex_id), Some(time)) = + (df.utf8::(vertex_id), df.iter_col::(time)) + { + let iter = vertex_id.into_iter().zip(time); + for (((vertex_id, time), props), const_props) in tqdm!( + iter.zip(prop_iter).zip(const_prop_iter), + desc = "Loading vertices", + total = size, + animation = kdam::Animation::FillUp, + unit_scale = true + ) { + if let (Some(vertex_id), Some(time)) = (vertex_id, time) { + let v = graph.add_vertex(*time, vertex_id, props)?; + v.add_constant_properties(const_props)?; + if let Some(shared_const_props) = &shared_const_props { + v.add_constant_properties(shared_const_props.iter())?; + } + } + } + } else if let (Some(vertex_id), Some(time)) = + (df.utf8::(vertex_id), df.iter_col::(time)) + { + let iter = vertex_id.into_iter().zip(time); + for (((vertex_id, time), props), const_props) in tqdm!( + iter.zip(prop_iter).zip(const_prop_iter), + desc = "Loading vertices", + total = size, + animation = kdam::Animation::FillUp, + unit_scale = true + ) { + if let (Some(vertex_id), Some(time)) = (vertex_id, time) { + let v = graph.add_vertex(*time, vertex_id, props)?; + v.add_constant_properties(const_props)?; + if let Some(shared_const_props) = &shared_const_props { + v.add_constant_properties(shared_const_props)?; + } + } + } + } else { + return Err(GraphError::LoadFailure( + "vertex id column must be either u64 or text, time column must be i64".to_string(), + )); + } + + Ok(()) +} + +pub(crate) fn load_edges_from_df<'a, S: AsRef>( + df: &'a PretendDF, + size: usize, + src: &str, + dst: &str, + time: &str, + props: Option>, + const_props: Option>, + shared_const_props: Option>, + layer: Option, + layer_in_df: Option, + graph: &Graph, +) -> Result<(), GraphError> { + let prop_iter = props + .unwrap_or_default() + .into_iter() + .map(|name| lift_property(name, &df)) + .reduce(combine_prop_iters) + .unwrap_or_else(|| Box::new(std::iter::repeat(vec![]))); + + let const_prop_iter = const_props + .unwrap_or_default() + .into_iter() + .map(|name| lift_property(name, &df)) + .reduce(combine_prop_iters) + .unwrap_or_else(|| Box::new(std::iter::repeat(vec![]))); + + let layer = lift_layer(layer, layer_in_df, df); + + if let (Some(src), Some(dst), Some(time)) = ( + df.iter_col::(src), + df.iter_col::(dst), + df.iter_col::(time), + ) { + let triplets = src + .map(|i| i.copied()) + .zip(dst.map(|i| i.copied())) + .zip(time); + load_edges_from_num_iter( + &graph, + size, + triplets, + prop_iter, + const_prop_iter, + shared_const_props, + layer, + )?; + } else if let (Some(src), Some(dst), Some(time)) = ( + df.iter_col::(src), + df.iter_col::(dst), + df.iter_col::(time), + ) { + let triplets = src + .map(i64_opt_into_u64_opt) + .zip(dst.map(i64_opt_into_u64_opt)) + .zip(time); + load_edges_from_num_iter( + &graph, + size, + triplets, + prop_iter, + const_prop_iter, + shared_const_props, + layer, + )?; + } else if let (Some(src), Some(dst), Some(time)) = ( + df.utf8::(src), + df.utf8::(dst), + df.iter_col::(time), + ) { + let triplets = src.into_iter().zip(dst.into_iter()).zip(time.into_iter()); + + for (((((src, dst), time), props), const_props), layer) in tqdm!( + triplets.zip(prop_iter).zip(const_prop_iter).zip(layer), + desc = "Loading edges", + total = size, + animation = kdam::Animation::FillUp, + unit_scale = true + ) { + if let (Some(src), Some(dst), Some(time)) = (src, dst, time) { + let e = graph.add_edge(*time, src, dst, props, layer.as_deref())?; + e.add_constant_properties(const_props, layer.as_deref())?; + if let Some(shared_const_props) = &shared_const_props { + e.add_constant_properties(shared_const_props.iter(), layer.as_deref())?; + } + } + } + } else if let (Some(src), Some(dst), Some(time)) = ( + df.utf8::(src), + df.utf8::(dst), + df.iter_col::(time), + ) { + let triplets = src.into_iter().zip(dst.into_iter()).zip(time.into_iter()); + for (((((src, dst), time), props), const_props), layer) in tqdm!( + triplets.zip(prop_iter).zip(const_prop_iter).zip(layer), + desc = "Loading edges", + total = size, + animation = kdam::Animation::FillUp, + unit_scale = true + ) { + if let (Some(src), Some(dst), Some(time)) = (src, dst, time) { + let e = graph.add_edge(*time, src, dst, props, layer.as_deref())?; + e.add_constant_properties(const_props, layer.as_deref())?; + if let Some(shared_const_props) = &shared_const_props { + e.add_constant_properties(shared_const_props.iter(), layer.as_deref())?; + } + } + } + } else { + return Err(GraphError::LoadFailure( + "source and target columns must be either u64 or text, time column must be i64" + .to_string(), + )); + } + Ok(()) +} + +pub(crate) fn load_vertex_props_from_df<'a>( + df: &'a PretendDF, + size: usize, + vertex_id: &str, + const_props: Option>, + shared_const_props: Option>, + graph: &Graph, +) -> Result<(), GraphError> { + let const_prop_iter = const_props + .unwrap_or_default() + .into_iter() + .map(|name| lift_property(name, &df)) + .reduce(combine_prop_iters) + .unwrap_or_else(|| Box::new(std::iter::repeat(vec![]))); + + if let Some(vertex_id) = df.iter_col::(vertex_id) { + let iter = vertex_id.map(|i| i.copied()); + for (vertex_id, const_props) in tqdm!( + iter.zip(const_prop_iter), + desc = "Loading vertex properties", + total = size, + animation = kdam::Animation::FillUp, + unit_scale = true + ) { + if let Some(vertex_id) = vertex_id { + let v = graph + .vertex(vertex_id) + .ok_or(GraphError::VertexIdError(vertex_id))?; + v.add_constant_properties(const_props)?; + if let Some(shared_const_props) = &shared_const_props { + v.add_constant_properties(shared_const_props.iter())?; + } + } + } + } else if let Some(vertex_id) = df.iter_col::(vertex_id) { + let iter = vertex_id.map(i64_opt_into_u64_opt); + for (vertex_id, const_props) in tqdm!( + iter.zip(const_prop_iter), + desc = "Loading vertex properties", + total = size, + animation = kdam::Animation::FillUp, + unit_scale = true + ) { + if let Some(vertex_id) = vertex_id { + let v = graph + .vertex(vertex_id) + .ok_or(GraphError::VertexIdError(vertex_id))?; + v.add_constant_properties(const_props)?; + if let Some(shared_const_props) = &shared_const_props { + v.add_constant_properties(shared_const_props.iter())?; + } + } + } + } else if let Some(vertex_id) = df.utf8::(vertex_id) { + let iter = vertex_id.into_iter(); + for (vertex_id, const_props) in tqdm!( + iter.zip(const_prop_iter), + desc = "Loading vertex properties", + total = size, + animation = kdam::Animation::FillUp, + unit_scale = true + ) { + if let Some(vertex_id) = vertex_id { + let v = graph + .vertex(vertex_id) + .ok_or_else(|| GraphError::VertexNameError(vertex_id.to_owned()))?; + v.add_constant_properties(const_props)?; + if let Some(shared_const_props) = &shared_const_props { + v.add_constant_properties(shared_const_props.iter())?; + } + } + } + } else if let Some(vertex_id) = df.utf8::(vertex_id) { + let iter = vertex_id.into_iter(); + for (vertex_id, const_props) in tqdm!( + iter.zip(const_prop_iter), + desc = "Loading vertex properties", + total = size, + animation = kdam::Animation::FillUp, + unit_scale = true + ) { + if let Some(vertex_id) = vertex_id { + let v = graph + .vertex(vertex_id) + .ok_or_else(|| GraphError::VertexNameError(vertex_id.to_owned()))?; + v.add_constant_properties(const_props)?; + if let Some(shared_const_props) = &shared_const_props { + v.add_constant_properties(shared_const_props.iter())?; + } + } + } + } else { + return Err(GraphError::LoadFailure( + "vertex id column must be either u64 or text, time column must be i64".to_string(), + )); + } + Ok(()) +} + +pub(crate) fn load_edges_props_from_df<'a, S: AsRef>( + df: &'a PretendDF, + size: usize, + src: &str, + dst: &str, + const_props: Option>, + shared_const_props: Option>, + layer: Option, + layer_in_df: Option, + graph: &Graph, +) -> Result<(), GraphError> { + let const_prop_iter = const_props + .unwrap_or_default() + .into_iter() + .map(|name| lift_property(name, &df)) + .reduce(combine_prop_iters) + .unwrap_or_else(|| Box::new(std::iter::repeat(vec![]))); + + let layer = lift_layer(layer, layer_in_df, df); + + if let (Some(src), Some(dst)) = (df.iter_col::(src), df.iter_col::(dst)) { + let triplets = src.map(|i| i.copied()).zip(dst.map(|i| i.copied())); + + for (((src, dst), const_props), layer) in tqdm!( + triplets.zip(const_prop_iter).zip(layer), + desc = "Loading edge properties", + total = size, + animation = kdam::Animation::FillUp, + unit_scale = true + ) { + if let (Some(src), Some(dst)) = (src, dst) { + let e = graph + .edge(src, dst) + .ok_or(GraphError::EdgeIdError { src, dst })?; + e.add_constant_properties(const_props, layer.as_deref())?; + if let Some(shared_const_props) = &shared_const_props { + e.add_constant_properties(shared_const_props.iter(), layer.as_deref())?; + } + } + } + } else if let (Some(src), Some(dst)) = (df.iter_col::(src), df.iter_col::(dst)) { + let triplets = src + .map(i64_opt_into_u64_opt) + .zip(dst.map(i64_opt_into_u64_opt)); + for (((src, dst), const_props), layer) in tqdm!( + triplets.zip(const_prop_iter).zip(layer), + desc = "Loading edge properties", + total = size, + animation = kdam::Animation::FillUp, + unit_scale = true + ) { + if let (Some(src), Some(dst)) = (src, dst) { + let e = graph + .edge(src, dst) + .ok_or(GraphError::EdgeIdError { src, dst })?; + e.add_constant_properties(const_props, layer.as_deref())?; + if let Some(shared_const_props) = &shared_const_props { + e.add_constant_properties(shared_const_props.iter(), layer.as_deref())?; + } + } + } + } else if let (Some(src), Some(dst)) = (df.utf8::(src), df.utf8::(dst)) { + let triplets = src.into_iter().zip(dst.into_iter()); + for (((src, dst), const_props), layer) in tqdm!( + triplets.zip(const_prop_iter).zip(layer), + desc = "Loading edge properties", + total = size, + animation = kdam::Animation::FillUp, + unit_scale = true + ) { + if let (Some(src), Some(dst)) = (src, dst) { + let e = graph + .edge(src, dst) + .ok_or_else(|| GraphError::EdgeNameError { + src: src.to_owned(), + dst: dst.to_owned(), + })?; + e.add_constant_properties(const_props, layer.as_deref())?; + if let Some(shared_const_props) = &shared_const_props { + e.add_constant_properties(shared_const_props.iter(), layer.as_deref())?; + } + } + } + } else if let (Some(src), Some(dst)) = (df.utf8::(src), df.utf8::(dst)) { + let triplets = src.into_iter().zip(dst.into_iter()); + for (((src, dst), const_props), layer) in tqdm!( + triplets.zip(const_prop_iter).zip(layer), + desc = "Loading edge properties", + total = size, + animation = kdam::Animation::FillUp, + unit_scale = true + ) { + if let (Some(src), Some(dst)) = (src, dst) { + let e = graph + .edge(src, dst) + .ok_or_else(|| GraphError::EdgeNameError { + src: src.to_owned(), + dst: dst.to_owned(), + })?; + e.add_constant_properties(const_props, layer.as_deref())?; + if let Some(shared_const_props) = &shared_const_props { + e.add_constant_properties(shared_const_props.iter(), layer.as_deref())?; + } + } + } + } else { + return Err(GraphError::LoadFailure( + "source and target columns must be either u64 or text, time column must be i64" + .to_string(), + )); + } + Ok(()) +} + +fn lift_property<'a: 'b, 'b>( + name: &'a str, + df: &'b PretendDF, +) -> Box> + 'b> { + if let Some(col) = df.iter_col::(name) { + iter_as_prop(name, col) + } else if let Some(col) = df.iter_col::(name) { + iter_as_prop(name, col) + } else if let Some(col) = df.iter_col::(name) { + iter_as_prop(name, col) + } else if let Some(col) = df.iter_col::(name) { + iter_as_prop(name, col) + } else if let Some(col) = df.iter_col::(name) { + iter_as_prop(name, col) + } else if let Some(col) = df.iter_col::(name) { + iter_as_prop(name, col) + } else if let Some(col) = df.bool(name) { + Box::new(col.map(move |val| { + val.into_iter() + .map(|v| (name, Prop::Bool(v))) + .collect::>() + })) + } else if let Some(col) = df.utf8::(name) { + Box::new(col.map(move |val| { + val.into_iter() + .map(|v| (name, Prop::str(v))) + .collect::>() + })) + } else if let Some(col) = df.utf8::(name) { + Box::new(col.map(move |val| { + val.into_iter() + .map(|v| (name, Prop::str(v))) + .collect::>() + })) + } else { + Box::new(std::iter::repeat(Vec::with_capacity(0))) + } +} + +fn lift_layer<'a, S: AsRef>( + layer: Option, + layer_in_df: Option, + df: &'a PretendDF, +) -> Box> + 'a> { + if let Some(layer) = layer { + //Prioritise the explicit layer set by the user + Box::new(std::iter::repeat(Some(layer.as_ref().to_string()))) + } else if let Some(name) = layer_in_df { + if let Some(col) = df.utf8::(name.as_ref()) { + Box::new(col.map(|v| v.map(|v| v.to_string()))) + } else if let Some(col) = df.utf8::(name.as_ref()) { + Box::new(col.map(|v| v.map(|v| v.to_string()))) + } else { + Box::new(std::iter::repeat(None)) + } + } else { + Box::new(std::iter::repeat(None)) + } +} + +fn iter_as_prop< + 'a: 'b, + 'b, + T: Into + Copy + 'static, + I: Iterator> + 'a, +>( + name: &'a str, + is: I, +) -> Box> + '_> { + Box::new(is.map(move |val| { + val.into_iter() + .map(|v| (name, (*v).into())) + .collect::>() + })) +} + +fn combine_prop_iters< + 'a, + I1: Iterator> + 'a, + I2: Iterator> + 'a, +>( + i1: I1, + i2: I2, +) -> Box> + 'a> { + Box::new(i1.zip(i2).map(|(mut v1, v2)| { + v1.extend(v2); + v1 + })) +} + +fn load_edges_from_num_iter< + 'a, + S: AsRef, + I: Iterator, Option), Option<&'a i64>)>, + PI: Iterator>, + IL: Iterator>, +>( + graph: &Graph, + size: usize, + edges: I, + props: PI, + const_props: PI, + shared_const_props: Option>, + layer: IL, +) -> Result<(), GraphError> { + for (((((src, dst), time), edge_props), const_props), layer) in tqdm!( + edges.zip(props).zip(const_props).zip(layer), + desc = "Loading edges", + total = size, + animation = kdam::Animation::FillUp, + unit_scale = true + ) { + if let (Some(src), Some(dst), Some(time)) = (src, dst, time) { + let e = graph.add_edge(*time, src, dst, edge_props, layer.as_deref())?; + e.add_constant_properties(const_props, layer.as_deref())?; + if let Some(shared_const_props) = &shared_const_props { + e.add_constant_properties(shared_const_props.iter(), layer.as_deref())?; + } + } + } + Ok(()) +} + +fn load_vertices_from_num_iter< + 'a, + S: AsRef, + I: Iterator, Option<&'a i64>)>, + PI: Iterator>, +>( + graph: &Graph, + size: usize, + vertices: I, + props: PI, + const_props: PI, + shared_const_props: Option>, +) -> Result<(), GraphError> { + for (((vertex, time), props), const_props) in tqdm!( + vertices.zip(props).zip(const_props), + desc = "Loading vertices", + total = size, + animation = kdam::Animation::FillUp, + unit_scale = true + ) { + if let (Some(v), Some(t), props, const_props) = (vertex, time, props, const_props) { + let v = graph.add_vertex(*t, v, props)?; + v.add_constant_properties(const_props)?; + + if let Some(shared_const_props) = &shared_const_props { + v.add_constant_properties(shared_const_props.iter())?; + } + } + } + Ok(()) +} + +pub(crate) struct PretendDF { + names: Vec, + arrays: Vec>>, +} + +impl PretendDF { + fn iter_col(&self, name: &str) -> Option> + '_> { + let idx = self.names.iter().position(|n| n == name)?; + + let _ = (&self.arrays[0])[idx] + .as_any() + .downcast_ref::>()?; + + let iter = self.arrays.iter().flat_map(move |arr| { + let arr = &arr[idx]; + let arr = arr.as_any().downcast_ref::>().unwrap(); + arr.iter() + }); + + Some(iter) + } + + fn utf8(&self, name: &str) -> Option> + '_> { + let idx = self.names.iter().position(|n| n == name)?; + // test that it's actually a utf8 array + let _ = (&self.arrays[0])[idx] + .as_any() + .downcast_ref::>()?; + + let iter = self.arrays.iter().flat_map(move |arr| { + let arr = &arr[idx]; + let arr = arr.as_any().downcast_ref::>().unwrap(); + arr.iter() + }); + + Some(iter) + } + + fn bool(&self, name: &str) -> Option> + '_> { + let idx = self.names.iter().position(|n| n == name)?; + + let _ = (&self.arrays[0])[idx] + .as_any() + .downcast_ref::()?; + + let iter = self.arrays.iter().flat_map(move |arr| { + let arr = &arr[idx]; + let arr = arr.as_any().downcast_ref::().unwrap(); + arr.iter() + }); + + Some(iter) + } +} + +pub fn array_to_rust(obj: &PyAny) -> PyResult { + // prepare a pointer to receive the Array struct + let array = Box::new(ffi::ArrowArray::empty()); + let schema = Box::new(ffi::ArrowSchema::empty()); + + let array_ptr = &*array as *const ffi::ArrowArray; + let schema_ptr = &*schema as *const ffi::ArrowSchema; + + // make the conversion through PyArrow's private API + // this changes the pointer's memory and is thus unsafe. In particular, `_export_to_c` can go out of bounds + obj.call_method1( + "_export_to_c", + (array_ptr as Py_uintptr_t, schema_ptr as Py_uintptr_t), + )?; + + unsafe { + let field = ffi::import_field_from_c(schema.as_ref()) + .map_err(|e| ArrowErrorException::new_err(format!("{:?}", e)))?; + let array = ffi::import_array_from_c(*array, field.data_type) + .map_err(|e| ArrowErrorException::new_err(format!("{:?}", e)))?; + Ok(array) + } +} + +pub type ArrayRef = Box; + +create_exception!(exceptions, ArrowErrorException, PyException); +create_exception!(exceptions, GraphLoadException, PyException); + +#[cfg(test)] +mod test { + use crate::{prelude::*, python::graph::pandas::load_vertices_from_df}; + + use super::{load_edges_from_df, PretendDF}; + use arrow2::array::{PrimitiveArray, Utf8Array}; + + #[test] + fn load_edges_from_pretend_df() { + let df = PretendDF { + names: vec!["src", "dst", "time", "prop1", "prop2"] + .iter() + .map(|s| s.to_string()) + .collect(), + arrays: vec![ + vec![ + Box::new(PrimitiveArray::::from(vec![Some(1)])), + Box::new(PrimitiveArray::::from(vec![Some(2)])), + Box::new(PrimitiveArray::::from(vec![Some(1)])), + Box::new(PrimitiveArray::::from(vec![Some(1.0)])), + Box::new(Utf8Array::::from(vec![Some("a")])), + ], + vec![ + Box::new(PrimitiveArray::::from(vec![Some(2), Some(3)])), + Box::new(PrimitiveArray::::from(vec![Some(3), Some(4)])), + Box::new(PrimitiveArray::::from(vec![Some(2), Some(3)])), + Box::new(PrimitiveArray::::from(vec![Some(2.0), Some(3.0)])), + Box::new(Utf8Array::::from(vec![Some("b"), Some("c")])), + ], + ], + }; + let graph = Graph::new(); + let layer: Option<&str> = None; + let layer_in_df: Option<&str> = None; + load_edges_from_df( + &df, + 5, + "src", + "dst", + "time", + Some(vec!["prop1", "prop2"]), + None, + None, + layer, + layer_in_df, + &graph, + ) + .expect("failed to load edges from pretend df"); + + let actual = graph + .edges() + .map(|e| { + ( + e.src().id(), + e.dst().id(), + e.latest_time(), + e.properties() + .temporal() + .get("prop1") + .and_then(|v| v.latest()), + e.properties() + .temporal() + .get("prop2") + .and_then(|v| v.latest()), + ) + }) + .collect::>(); + + assert_eq!( + actual, + vec![ + (1, 2, Some(1), Some(Prop::F64(1.0)), Some(Prop::str("a"))), + (2, 3, Some(2), Some(Prop::F64(2.0)), Some(Prop::str("b"))), + (3, 4, Some(3), Some(Prop::F64(3.0)), Some(Prop::str("c"))), + ] + ); + } + + #[test] + fn load_vertices_from_pretend_df() { + let df = PretendDF { + names: vec!["id", "name", "time"] + .iter() + .map(|s| s.to_string()) + .collect(), + arrays: vec![ + vec![ + Box::new(PrimitiveArray::::from(vec![Some(1)])), + Box::new(Utf8Array::::from(vec![Some("a")])), + Box::new(PrimitiveArray::::from(vec![Some(1)])), + ], + vec![ + Box::new(PrimitiveArray::::from(vec![Some(2)])), + Box::new(Utf8Array::::from(vec![Some("b")])), + Box::new(PrimitiveArray::::from(vec![Some(2)])), + ], + ], + }; + let graph = Graph::new(); + + load_vertices_from_df(&df, 3, "id", "time", Some(vec!["name"]), None, None, &graph) + .expect("failed to load vertices from pretend df"); + + let actual = graph + .vertices() + .iter() + .map(|v| { + ( + v.id(), + v.latest_time(), + v.properties() + .temporal() + .get("name") + .and_then(|v| v.latest()), + ) + }) + .collect::>(); + + assert_eq!( + actual, + vec![ + (1, Some(1), Some(Prop::str("a"))), + (2, Some(2), Some(Prop::str("b"))), + ] + ); + } +} diff --git a/raphtory/src/python/graph/properties/constant_props.rs b/raphtory/src/python/graph/properties/constant_props.rs new file mode 100644 index 0000000000..ca353e1b0e --- /dev/null +++ b/raphtory/src/python/graph/properties/constant_props.rs @@ -0,0 +1,255 @@ +use crate::{ + core::{ArcStr, Prop}, + db::api::{ + properties::{internal::PropertiesOps, ConstProperties}, + view::internal::Static, + }, + python::{ + graph::properties::{ + props::PyPropsComp, DynProps, PyConstPropsListListCmp, PyPropValueList, + PyPropValueListList, PyPropsListCmp, + }, + types::repr::{iterator_dict_repr, Repr}, + utils::PyGenericIterator, + }, +}; +use itertools::Itertools; +use pyo3::{ + exceptions::{PyKeyError, PyTypeError}, + prelude::*, +}; +use std::{collections::HashMap, sync::Arc}; + +pub type DynConstProperties = ConstProperties; + +impl From> + for DynConstProperties +{ + fn from(value: ConstProperties

) -> Self { + ConstProperties { + props: Arc::new(value.props), + } + } +} + +impl IntoPy for ConstProperties

{ + fn into_py(self, py: Python<'_>) -> PyObject { + PyConstProperties::from(self).into_py(py) + } +} + +impl Repr for ConstProperties

{ + fn repr(&self) -> String { + format!("StaticProperties({{{}}})", iterator_dict_repr(self.iter())) + } +} + +/// A view of constant properties of an entity +#[pyclass(name = "ConstProperties")] +pub struct PyConstProperties { + props: DynConstProperties, +} + +py_eq!(PyConstProperties, PyPropsComp); + +#[pymethods] +impl PyConstProperties { + /// keys() -> list[str] + /// + /// lists the available property keys + pub fn keys(&self) -> Vec { + self.props.keys() + } + + /// values() -> list[Any] + /// + /// lists the property values + pub fn values(&self) -> Vec { + self.props.values() + } + + /// items() -> list[tuple[str, Any]] + /// + /// lists the property keys together with the corresponding value + pub fn items(&self) -> Vec<(ArcStr, Prop)> { + self.props.iter().collect() + } + + /// __getitem__(key: str) -> Any + /// + /// get property value by key + /// + /// Raises: + /// KeyError: if property `key` does not exist + pub fn __getitem__(&self, key: &str) -> PyResult { + self.props + .get(key) + .ok_or(PyKeyError::new_err("No such property")) + } + + /// get(key: str) -> Any | None + /// + /// Arguments: + /// key: the name of the property + /// + /// get property value by key (returns `None` if key does not exist) + pub fn get(&self, key: &str) -> Option { + // Fixme: Add option to specify default? + self.props.get(key) + } + + /// as_dict() -> dict[str, Any] + /// + /// convert the properties view to a python dict + pub fn as_dict(&self) -> HashMap { + self.props.as_map() + } + + /// __iter__() -> Iterator[str] + /// + /// iterate over property keys + pub fn __iter__(&self) -> PyGenericIterator { + self.keys().into_iter().into() + } + + /// __contains__(key: str) -> bool + /// + /// check if property `key` exists + pub fn __contains__(&self, key: &str) -> bool { + self.props.contains(key) + } + + /// __len__() -> int + /// + /// the number of properties + pub fn __len__(&self) -> usize { + self.keys().len() + } + + pub fn __repr__(&self) -> String { + self.repr() + } +} + +impl From> for PyConstProperties { + fn from(value: ConstProperties

) -> Self { + PyConstProperties { + props: ConstProperties::new(Arc::new(value.props)), + } + } +} + +impl Repr for PyConstProperties { + fn repr(&self) -> String { + self.props.repr() + } +} + +py_iterable_base!(PyConstPropsList, DynConstProperties, PyConstProperties); +py_eq!(PyConstPropsList, PyPropsListCmp); + +#[pymethods] +impl PyConstPropsList { + pub fn keys(&self) -> Vec { + self.iter().map(|p| p.keys()).kmerge().dedup().collect() + } + + pub fn values(&self) -> Vec { + self.keys() + .into_iter() + .map(|k| self.get(k).expect("key exists")) + .collect() + } + pub fn items(&self) -> Vec<(ArcStr, PyPropValueList)> { + self.keys().into_iter().zip(self.values()).collect() + } + + pub fn __getitem__(&self, key: ArcStr) -> PyResult { + self.get(key).ok_or(PyKeyError::new_err("No such property")) + } + + pub fn get(&self, key: ArcStr) -> Option { + self.__contains__(&key).then(|| { + let builder = self.builder.clone(); + let key = key.clone(); + (move || { + let key = key.clone(); + builder().map(move |p| p.get(&key)) + }) + .into() + }) + } + + pub fn __contains__(&self, key: &str) -> bool { + self.iter().any(|p| p.contains(key)) + } + + pub fn __iter__(&self) -> PyGenericIterator { + self.keys().into_iter().into() + } + + pub fn as_dict(&self) -> HashMap>> { + self.items() + .into_iter() + .map(|(k, v)| (k, v.collect())) + .collect() + } +} + +py_nested_iterable_base!(PyConstPropsListList, DynConstProperties, PyConstProperties); +py_eq!(PyConstPropsListList, PyConstPropsListListCmp); + +#[pymethods] +impl PyConstPropsListList { + pub fn keys(&self) -> Vec { + self.iter() + .flat_map(|it| it.map(|p| p.keys())) + .kmerge() + .dedup() + .collect() + } + + pub fn values(&self) -> Vec { + self.keys() + .into_iter() + .map(|k| self.get(k).expect("key exists")) + .collect() + } + pub fn items(&self) -> Vec<(ArcStr, PyPropValueListList)> { + self.keys().into_iter().zip(self.values()).collect() + } + + pub fn __getitem__(&self, key: ArcStr) -> PyResult { + self.get(key).ok_or(PyKeyError::new_err("No such property")) + } + + pub fn __iter__(&self) -> PyGenericIterator { + self.keys().into_iter().into() + } + + pub fn get(&self, key: ArcStr) -> Option { + self.__contains__(&key).then(|| { + let builder = self.builder.clone(); + let key = key.clone(); + (move || { + let key = key.clone(); + builder().map(move |it| { + let key = key.clone(); + it.map(move |p| p.get(&key)) + }) + }) + .into() + }) + } + + pub fn __contains__(&self, key: &str) -> bool { + self.iter().any(|mut it| it.any(|p| p.contains(key))) + } + + pub fn as_dict(&self) -> HashMap>>> { + self.items() + .into_iter() + .map(|(k, v)| (k, v.collect())) + .collect() + } +} diff --git a/raphtory/src/python/graph/properties/mod.rs b/raphtory/src/python/graph/properties/mod.rs new file mode 100644 index 0000000000..14f69c7425 --- /dev/null +++ b/raphtory/src/python/graph/properties/mod.rs @@ -0,0 +1,13 @@ +use crate::db::api::properties::internal::{InheritPropertiesOps, PropertiesOps}; +use std::sync::Arc; + +mod constant_props; +mod props; +mod temporal_props; + +pub type DynProps = Arc; +impl InheritPropertiesOps for DynProps {} + +pub use constant_props::*; +pub use props::*; +pub use temporal_props::*; diff --git a/raphtory/src/python/graph/properties/props.rs b/raphtory/src/python/graph/properties/props.rs new file mode 100644 index 0000000000..335551bd34 --- /dev/null +++ b/raphtory/src/python/graph/properties/props.rs @@ -0,0 +1,478 @@ +use crate::{ + core::{ArcStr, Prop}, + db::api::{ + properties::{internal::PropertiesOps, Properties}, + view::internal::{DynamicGraph, Static}, + }, + python::{ + graph::properties::{ + DynConstProperties, DynProps, DynTemporalProperties, PyConstProperties, + PyConstPropsList, PyConstPropsListList, PyTemporalPropsList, PyTemporalPropsListList, + }, + types::{ + repr::{iterator_dict_repr, Repr}, + wrappers::prop::PropValue, + }, + utils::PyGenericIterator, + }, +}; +use itertools::Itertools; +use pyo3::{ + exceptions::{PyKeyError, PyTypeError}, + prelude::*, +}; +use std::{collections::HashMap, ops::Deref, sync::Arc}; + +pub type DynProperties = Properties>; + +#[derive(PartialEq, Clone)] +pub struct PyPropsComp(HashMap); + +impl<'source> FromPyObject<'source> for PyPropsComp { + fn extract(ob: &'source PyAny) -> PyResult { + if let Ok(sp) = ob.extract::>() { + Ok(sp.deref().into()) + } else if let Ok(p) = ob.extract::>() { + Ok(p.deref().into()) + } else if let Ok(m) = ob.extract::>() { + Ok(PyPropsComp(m)) + } else { + Err(PyTypeError::new_err("not comparable with properties")) + } + } +} + +impl From<&PyConstProperties> for PyPropsComp { + fn from(value: &PyConstProperties) -> Self { + Self(value.as_dict()) + } +} + +impl From<&PyProperties> for PyPropsComp { + fn from(value: &PyProperties) -> Self { + Self(value.as_dict()) + } +} + +impl From for PyPropsComp { + fn from(value: DynConstProperties) -> Self { + Self(value.as_map()) + } +} + +impl From for PyPropsComp { + fn from(value: DynProperties) -> Self { + Self(value.as_map()) + } +} + +/// A view of the properties of an entity +#[pyclass(name = "Properties")] +pub struct PyProperties { + props: DynProperties, +} + +py_eq!(PyProperties, PyPropsComp); + +#[pymethods] +impl PyProperties { + /// Get property value. + /// + /// First searches temporal properties and returns latest value if it exists. + /// If not, it falls back to static properties. + pub fn get(&self, key: &str) -> Option { + self.props.get(key) + } + + /// Check if property `key` exists. + pub fn __contains__(&self, key: &str) -> bool { + self.props.contains(key) + } + + /// gets property value if it exists, otherwise raises `KeyError` + fn __getitem__(&self, key: &str) -> PyResult { + self.props + .get(key) + .ok_or(PyKeyError::new_err("No such property")) + } + + /// iterate over property keys + fn __iter__(&self) -> PyGenericIterator { + self.keys().into_iter().into() + } + + /// number of properties + fn __len__(&self) -> usize { + self.keys().len() + } + + /// Get the names for all properties (includes temporal and static properties) + pub fn keys(&self) -> Vec { + self.props.keys().map(|k| k.clone()).collect() + } + + /// Get the values of the properties + /// + /// If a property exists as both temporal and static, temporal properties take priority with + /// fallback to the static property if the temporal value does not exist. + pub fn values(&self) -> Vec { + self.props.values().collect() + } + + /// Get a list of key-value pairs + pub fn items(&self) -> Vec<(ArcStr, Prop)> { + self.props.as_vec() + } + + /// Get a view of the temporal properties only. + #[getter] + pub fn temporal(&self) -> DynTemporalProperties { + self.props.temporal() + } + + /// Get a view of the constant properties (meta-data) only. + #[getter] + pub fn constant(&self) -> DynConstProperties { + self.props.constant() + } + + /// Convert properties view to a dict + pub fn as_dict(&self) -> HashMap { + self.props.as_map() + } +} + +impl From> + for DynProperties +{ + fn from(value: Properties

) -> Self { + Properties::new(Arc::new(value.props)) + } +} + +impl From> for DynProperties { + fn from(value: Properties) -> Self { + let props: DynProps = Arc::new(value.props); + Properties::new(props) + } +} + +impl> From

for PyProperties { + fn from(value: P) -> Self { + Self { + props: value.into(), + } + } +} + +impl IntoPy for Properties

{ + fn into_py(self, py: Python<'_>) -> PyObject { + PyProperties::from(self).into_py(py) + } +} + +impl IntoPy for Properties { + fn into_py(self, py: Python<'_>) -> PyObject { + PyProperties::from(self).into_py(py) + } +} + +impl IntoPy for DynProperties { + fn into_py(self, py: Python<'_>) -> PyObject { + PyProperties::from(self).into_py(py) + } +} + +impl Repr for Properties

{ + fn repr(&self) -> String { + format!("Properties({{{}}})", iterator_dict_repr(self.iter())) + } +} + +impl Repr for PyProperties { + fn repr(&self) -> String { + self.props.repr() + } +} + +#[derive(PartialEq, Clone)] +pub struct PyPropsListCmp(HashMap); + +impl<'source> FromPyObject<'source> for PyPropsListCmp { + fn extract(ob: &'source PyAny) -> PyResult { + if let Ok(sp) = ob.extract::>() { + Ok(sp.deref().into()) + } else if let Ok(p) = ob.extract::>() { + Ok(p.deref().into()) + } else if let Ok(m) = ob.extract::>() { + Ok(Self(m)) + } else { + Err(PyTypeError::new_err("not comparable with properties")) + } + } +} + +impl From<&PyConstPropsList> for PyPropsListCmp { + fn from(value: &PyConstPropsList) -> Self { + Self( + value + .items() + .into_iter() + .map(|(k, v)| (k, v.into())) + .collect(), + ) + } +} + +impl From<&PyPropsList> for PyPropsListCmp { + fn from(value: &PyPropsList) -> Self { + Self( + value + .items() + .into_iter() + .map(|(k, v)| (k, v.into())) + .collect(), + ) + } +} + +py_iterable_base!(PyPropsList, DynProperties, PyProperties); +py_eq!(PyPropsList, PyPropsListCmp); + +#[pymethods] +impl PyPropsList { + /// Get property value. + /// + /// First searches temporal properties and returns latest value if it exists. + /// If not, it falls back to constant properties. + pub fn get(&self, key: &str) -> Option { + self.__contains__(key).then(|| { + let builder = self.builder.clone(); + let key = Arc::new(key.to_owned()); + (move || { + let key = key.clone(); + builder().map(move |p| p.get(key.as_ref())) + }) + .into() + }) + } + + pub fn __iter__(&self) -> PyGenericIterator { + self.keys().into_iter().into() + } + + /// Check if property `key` exists. + pub fn __contains__(&self, key: &str) -> bool { + self.iter().any(|p| p.contains(key)) + } + + fn __getitem__(&self, key: &str) -> PyResult { + self.get(key).ok_or(PyKeyError::new_err("No such property")) + } + + /// Get the names for all properties (includes temporal and constant properties) + pub fn keys(&self) -> Vec { + self.iter() + // FIXME: Still have to clone all those strings which sucks + .map(|p| p.keys().map(|k| k.clone()).collect_vec()) + .kmerge() + .dedup() + .collect() + } + + /// Get the values of the properties + /// + /// If a property exists as both temporal and constant, temporal properties take priority with + /// fallback to the constant property if the temporal value does not exist. + pub fn values(&self) -> PyPropValueListList { + let builder = self.builder.clone(); + let keys = Arc::new(self.keys()); + (move || { + let builder = builder.clone(); + let keys = keys.clone(); + (0..keys.len()).map(move |index| { + let builder = builder.clone(); + let keys = keys.clone(); + builder().map(move |p| { + let key = &keys[index]; + p.get(key) + }) + }) + }) + .into() + } + + /// Get a list of key-value pairs + pub fn items(&self) -> Vec<(ArcStr, PyPropValueList)> { + self.keys() + .into_iter() + .flat_map(|k| self.get(&k).map(|v| (k, v))) + .collect() + } + + /// Get a view of the temporal properties only. + #[getter] + pub fn temporal(&self) -> PyTemporalPropsList { + let builder = self.builder.clone(); + (move || builder().map(|p| p.temporal())).into() + } + + /// Get a view of the constant properties (meta-data) only. + #[getter] + pub fn constant(&self) -> PyConstPropsList { + let builder = self.builder.clone(); + (move || builder().map(|p| p.constant())).into() + } + + /// Convert properties view to a dict + pub fn as_dict(&self) -> HashMap>> { + self.items() + .into_iter() + .map(|(k, v)| (k, v.collect())) + .collect() + } + + pub fn __repr__(&self) -> String { + format!( + "Properties({{{}}})", + iterator_dict_repr(self.items().into_iter()) + ) + } +} + +py_nested_iterable_base!(PyNestedPropsIterable, DynProperties, PyProperties); +py_eq!(PyNestedPropsIterable, PyConstPropsListListCmp); + +#[derive(PartialEq, Clone)] +pub struct PyConstPropsListListCmp(HashMap); + +impl<'source> FromPyObject<'source> for PyConstPropsListListCmp { + fn extract(ob: &'source PyAny) -> PyResult { + if let Ok(sp) = ob.extract::>() { + Ok(sp.deref().into()) + } else if let Ok(p) = ob.extract::>() { + Ok(p.deref().into()) + } else if let Ok(m) = ob.extract::>() { + Ok(Self(m)) + } else { + Err(PyTypeError::new_err("not comparable with properties")) + } + } +} + +impl From<&PyConstPropsListList> for PyConstPropsListListCmp { + fn from(value: &PyConstPropsListList) -> Self { + Self( + value + .items() + .into_iter() + .map(|(k, v)| (k, v.into())) + .collect(), + ) + } +} + +impl From<&PyNestedPropsIterable> for PyConstPropsListListCmp { + fn from(value: &PyNestedPropsIterable) -> Self { + Self( + value + .items() + .into_iter() + .map(|(k, v)| (k, v.into())) + .collect(), + ) + } +} + +#[pymethods] +impl PyNestedPropsIterable { + /// Get property value. + /// + /// First searches temporal properties and returns latest value if it exists. + /// If not, it falls back to constant properties. + pub fn get(&self, key: &str) -> Option { + self.__contains__(key).then(|| { + let builder = self.builder.clone(); + let key = Arc::new(key.to_owned()); + (move || { + let key = key.clone(); + builder().map(move |it| { + let key = key.clone(); + it.map(move |p| p.get(key.clone().as_ref())) + }) + }) + .into() + }) + } + + /// Check if property `key` exists. + pub fn __contains__(&self, key: &str) -> bool { + self.iter().any(|mut it| it.any(|p| p.contains(key))) + } + + fn __getitem__(&self, key: &str) -> Result { + self.get(key).ok_or(PyKeyError::new_err("No such property")) + } + + /// Get the names for all properties (includes temporal and constant properties) + pub fn keys(&self) -> Vec { + self.iter() + // FIXME: Still have to clone all those strings which sucks + .flat_map(|it| it.map(|p| p.keys().map(|k| k.clone()).collect_vec())) + .kmerge() + .dedup() + .collect() + } + + pub fn __iter__(&self) -> PyGenericIterator { + self.keys().into_iter().into() + } + + /// Get the values of the properties + /// + /// If a property exists as both temporal and constant, temporal properties take priority with + /// fallback to the constant property if the temporal value does not exist. + pub fn values(&self) -> Vec { + self.keys() + .into_iter() + .flat_map(|key| self.get(&key)) + .collect() + } + + /// Get a list of key-value pairs + pub fn items(&self) -> Vec<(ArcStr, PyPropValueListList)> { + self.keys().into_iter().zip(self.values()).collect() + } + + /// Get a view of the temporal properties only. + #[getter] + pub fn temporal(&self) -> PyTemporalPropsListList { + let builder = self.builder.clone(); + (move || builder().map(|it| it.map(|p| p.temporal()))).into() + } + + /// Get a view of the constant properties (meta-data) only. + #[getter] + pub fn constant(&self) -> PyConstPropsListList { + let builder = self.builder.clone(); + (move || builder().map(|it| it.map(|p| p.constant()))).into() + } + + /// Convert properties view to a dict + pub fn as_dict(&self) -> HashMap>>> { + self.items() + .into_iter() + .map(|(k, v)| (k, v.collect())) + .collect() + } +} + +py_iterable!(PyPropValueList, PropValue, PropValue); +py_iterable_comp!(PyPropValueList, PropValue, PyPropValueListCmp); + +py_nested_iterable!(PyPropValueListList, PropValue, PropValue); +py_iterable_comp!( + PyPropValueListList, + PyPropValueListCmp, + PyPropValueListListCmp +); diff --git a/raphtory/src/python/graph/properties/temporal_props.rs b/raphtory/src/python/graph/properties/temporal_props.rs new file mode 100644 index 0000000000..232a1f193d --- /dev/null +++ b/raphtory/src/python/graph/properties/temporal_props.rs @@ -0,0 +1,1268 @@ +use crate::{ + core::{utils::time::IntoTime, ArcStr, Prop}, + db::api::{ + properties::{internal::PropertiesOps, TemporalProperties, TemporalPropertyView}, + view::internal::{DynamicGraph, Static}, + }, + python::{ + graph::properties::{DynProps, PyPropValueList, PyPropValueListList}, + types::{ + repr::{iterator_dict_repr, iterator_repr, Repr}, + wrappers::{ + iterators::{NestedUsizeIterable, PropIterable, UsizeIterable}, + prop::{PropHistItems, PropValue}, + }, + }, + utils::{PyGenericIterator, PyTime}, + }, +}; +use itertools::Itertools; +use pyo3::{ + exceptions::{PyKeyError, PyTypeError}, + prelude::*, +}; +use std::{collections::HashMap, ops::Deref, sync::Arc}; + +pub type DynTemporalProperties = TemporalProperties; +pub type DynTemporalProperty = TemporalPropertyView; + +impl From> + for DynTemporalProperties +{ + fn from(value: TemporalProperties

) -> Self { + TemporalProperties::new(Arc::new(value.props)) + } +} + +impl From> for DynTemporalProperties { + fn from(value: TemporalProperties) -> Self { + let props: Arc = Arc::new(value.props); + TemporalProperties::new(props) + } +} + +impl> From

for PyTemporalProperties { + fn from(value: P) -> Self { + Self { + props: value.into(), + } + } +} + +#[derive(PartialEq)] +pub struct PyTemporalPropsCmp(HashMap); + +impl From> for PyTemporalPropsCmp { + fn from(value: HashMap) -> Self { + Self(value) + } +} + +impl From<&PyTemporalProperties> for PyTemporalPropsCmp { + fn from(value: &PyTemporalProperties) -> Self { + Self( + value + .histories() + .into_iter() + .map(|(k, v)| (k, v.into())) + .collect(), + ) + } +} + +impl<'source> FromPyObject<'source> for PyTemporalPropsCmp { + fn extract(ob: &'source PyAny) -> PyResult { + if let Ok(v) = ob.extract::>() { + Ok(PyTemporalPropsCmp::from(v.deref())) + } else if let Ok(v) = ob.extract::>() { + Ok(PyTemporalPropsCmp::from(v)) + } else { + Err(PyTypeError::new_err("cannot compare")) + } + } +} + +/// A view of the temporal properties of an entity +#[pyclass(name = "TemporalProperties")] +pub struct PyTemporalProperties { + props: DynTemporalProperties, +} + +py_eq!(PyTemporalProperties, PyTemporalPropsCmp); + +#[pymethods] +impl PyTemporalProperties { + /// List the available property keys + fn keys(&self) -> Vec { + self.props.keys().map(|k| k.clone()).collect() + } + + /// List the values of the properties + /// + /// Returns: + /// list[TemporalProp]: the list of property views + fn values(&self) -> Vec { + self.props.values().collect() + } + + /// List the property keys together with the corresponding values + fn items(&self) -> Vec<(ArcStr, DynTemporalProperty)> { + self.props.iter().map(|(k, v)| (k.clone(), v)).collect() + } + + /// Get the latest value of all properties + /// + /// Returns: + /// dict[str, Any]: the mapping of property keys to latest values + fn latest(&self) -> HashMap { + self.props + .iter_latest() + .map(|(k, v)| (k.clone(), v)) + .collect() + } + + /// Get the histories of all properties + /// + /// Returns: + /// dict[str, list[(int, Any)]]: the mapping of property keys to histories + fn histories(&self) -> HashMap> { + self.props + .iter() + .map(|(k, v)| (k.clone(), v.iter().collect())) + .collect() + } + + /// __getitem__(key: str) -> TemporalProp + /// + /// Get property value for `key` + /// + /// Returns: + /// the property view + /// + /// Raises: + /// KeyError: if property `key` does not exist + fn __getitem__(&self, key: &str) -> PyResult { + self.get(key).ok_or(PyKeyError::new_err("No such property")) + } + + /// get(key: str) -> Optional[TemporalProp] + /// + /// Get property value for `key` if it exists + /// + /// Returns: + /// the property view if it exists, otherwise `None` + fn get(&self, key: &str) -> Option { + // Fixme: Add option to specify default? + self.props.get(key) + } + + /// Iterator over property keys + fn __iter__(&self) -> PyGenericIterator { + self.keys().into_iter().into() + } + + /// Check if property `key` exists + fn __contains__(&self, key: &str) -> bool { + self.props.contains(key) + } + + /// The number of properties + fn __len__(&self) -> usize { + self.keys().len() + } + + fn __repr__(&self) -> String { + self.props.repr() + } +} + +/// A view of a temporal property +#[pyclass(name = "TemporalProp")] +pub struct PyTemporalProp { + prop: DynTemporalProperty, +} + +#[derive(PartialEq, Clone)] +pub struct PyTemporalPropCmp(Vec<(i64, Prop)>); + +impl<'source> FromPyObject<'source> for PyTemporalPropCmp { + fn extract(ob: &'source PyAny) -> PyResult { + if let Ok(sp) = ob.extract::>() { + Ok(sp.deref().into()) + } else if let Ok(m) = ob.extract::>() { + Ok(Self(m)) + } else { + Err(PyTypeError::new_err("not comparable")) + } + } +} + +impl From<&PyTemporalProp> for PyTemporalPropCmp { + fn from(value: &PyTemporalProp) -> Self { + Self(value.items()) + } +} + +impl From> for PyTemporalPropCmp { + fn from(value: Vec<(i64, Prop)>) -> Self { + Self(value) + } +} + +impl From for PyTemporalPropCmp { + fn from(value: DynTemporalProperty) -> Self { + PyTemporalPropCmp(value.iter().collect()) + } +} + +py_eq!(PyTemporalProp, PyTemporalPropCmp); + +#[pymethods] +impl PyTemporalProp { + /// Get the timestamps at which the property was updated + pub fn history(&self) -> Vec { + self.prop.history() + } + + /// Get the property values for each update + pub fn values(&self) -> Vec { + self.prop.values() + } + + /// List update timestamps and corresponding property values + pub fn items(&self) -> Vec<(i64, Prop)> { + self.prop.iter().collect() + } + + /// Iterate over `items` + pub fn __iter__(&self) -> PyGenericIterator { + self.prop.iter().into() + } + /// Get the value of the property at time `t` + pub fn at(&self, t: PyTime) -> Option { + self.prop.at(t.into_time()) + } + /// Get the latest value of the property + pub fn value(&self) -> Option { + self.prop.latest() + } + + pub fn sum(&self) -> Prop { + let mut it_iter = self.prop.iter(); + let first = it_iter.next().unwrap(); + it_iter.fold(first.1, |acc, elem| acc.add(elem.1).unwrap()) + } + + pub fn min(&self) -> (i64, Prop) { + let mut it_iter = self.prop.iter(); + let first = it_iter.next().unwrap(); + it_iter.fold(first, |acc, elem| if acc.1 <= elem.1 { acc } else { elem }) + } + + pub fn max(&self) -> (i64, Prop) { + let mut it_iter = self.prop.iter(); + let first = it_iter.next().unwrap(); + it_iter.fold(first, |acc, elem| if acc.1 >= elem.1 { acc } else { elem }) + } + + pub fn count(&self) -> usize { + self.prop.iter().count() + } + + pub fn average(&self) -> Option { + self.mean() + } + + pub fn mean(&self) -> Option { + let sum: Prop = self.sum(); + let count: usize = self.count(); + if count == 0 { + return None; + } + match sum { + Prop::I32(s) => Some(Prop::F32(s as f32 / count as f32)), + Prop::I64(s) => Some(Prop::F64(s as f64 / count as f64)), + Prop::U32(s) => Some(Prop::F32(s as f32 / count as f32)), + Prop::U8(s) => Some(Prop::F64(s as f64 / count as f64)), // needs a test + Prop::U16(s) => Some(Prop::F64(s as f64 / count as f64)), // needs a test + Prop::U64(s) => Some(Prop::F64(s as f64 / count as f64)), + Prop::F32(s) => Some(Prop::F32(s / count as f32)), + Prop::F64(s) => Some(Prop::F64(s / count as f64)), + _ => None, + } + } + + pub fn median(&self) -> Option<(i64, Prop)> { + let it_iter = self.prop.iter(); + let mut vec: Vec<(i64, Prop)> = it_iter.collect_vec(); + // let mut vec: Vec<(i64, Prop)> = it_iter.map(|(t, v)| (t, v.clone())).collect(); + vec.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap()); + let len = vec.len(); + if len == 0 { + return None; + } + if len % 2 == 0 { + return Some(vec[len / 2 - 1].clone()); + } + Some(vec[len / 2].clone()) + } + + pub fn __repr__(&self) -> String { + self.prop.repr() + } +} + +impl From> for PyTemporalProp { + fn from(value: TemporalPropertyView

) -> Self { + Self { + prop: TemporalPropertyView { + id: value.id, + props: Arc::new(value.props), + }, + } + } +} + +impl IntoPy + for TemporalProperties

+{ + fn into_py(self, py: Python<'_>) -> PyObject { + PyTemporalProperties::from(self).into_py(py) + } +} + +impl IntoPy for TemporalProperties { + fn into_py(self, py: Python<'_>) -> PyObject { + PyTemporalProperties::from(self).into_py(py) + } +} + +impl IntoPy for DynTemporalProperties { + fn into_py(self, py: Python<'_>) -> PyObject { + PyTemporalProperties::from(self).into_py(py) + } +} + +impl Repr for TemporalProperties

{ + fn repr(&self) -> String { + format!( + "TemporalProperties({{{}}})", + iterator_dict_repr(self.iter()) + ) + } +} + +impl Repr for TemporalPropertyView

{ + fn repr(&self) -> String { + format!("TemporalProp({})", iterator_repr(self.iter())) + } +} + +impl Repr for PyTemporalProp { + fn repr(&self) -> String { + self.prop.repr() + } +} + +impl Repr for PyTemporalProperties { + fn repr(&self) -> String { + self.props.repr() + } +} + +impl IntoPy for TemporalPropertyView

{ + fn into_py(self, py: Python<'_>) -> PyObject { + PyTemporalProp::from(self).into_py(py) + } +} + +py_iterable_base!( + PyTemporalPropsList, + DynTemporalProperties, + PyTemporalProperties +); + +#[derive(PartialEq)] +pub struct PyTemporalPropsListCmp(HashMap); + +impl From<&PyTemporalPropsList> for PyTemporalPropsListCmp { + fn from(value: &PyTemporalPropsList) -> Self { + Self( + value + .items() + .into_iter() + .map(|(k, v)| (k, v.into())) + .collect(), + ) + } +} + +impl From> for PyTemporalPropsListCmp { + fn from(value: HashMap) -> Self { + Self(value) + } +} + +impl<'source> FromPyObject<'source> for PyTemporalPropsListCmp { + fn extract(ob: &'source PyAny) -> PyResult { + if let Ok(v) = ob.extract::>() { + Ok(PyTemporalPropsListCmp::from(v.deref())) + } else if let Ok(v) = ob.extract::>() { + Ok(PyTemporalPropsListCmp::from(v)) + } else { + Err(PyTypeError::new_err("cannot compare")) + } + } +} + +py_eq!(PyTemporalPropsList, PyTemporalPropsListCmp); + +#[pymethods] +impl PyTemporalPropsList { + fn keys(&self) -> Vec { + self.iter() + // FIXME: Still have to clone all those strings which sucks + .map(|p| p.keys().map(|k| k.clone()).collect_vec()) + .kmerge() + .dedup() + .collect() + } + fn values(&self) -> Vec { + self.keys() + .into_iter() + .map(|k| self.get(k).expect("key exists")) + .collect() + } + fn items(&self) -> Vec<(ArcStr, PyTemporalPropList)> { + self.keys().into_iter().zip(self.values()).collect() + } + + fn latest(&self) -> HashMap { + let builder = self.builder.clone(); + self.keys() + .into_iter() + .map(move |k| { + let builder = builder.clone(); + let nk = k.clone(); + ( + k, + (move || { + let nk = nk.clone(); + builder().map(move |p| p.get(nk.as_ref()).and_then(|v| v.latest())) + }) + .into(), + ) + }) + .collect() + } + + fn histories(&self) -> HashMap { + self.keys() + .into_iter() + .map(|k| { + let kk = k.clone(); + let builder = self.builder.clone(); + let v = (move || { + let kk = kk.clone(); + builder().map(move |p| { + p.get(kk.as_ref()) + .map(|v| v.iter().collect::>()) + .unwrap_or_default() + }) + }) + .into(); + (k, v) + }) + .collect() + } + + fn __getitem__(&self, key: ArcStr) -> PyResult { + self.get(key).ok_or(PyKeyError::new_err("unknown property")) + } + + fn __contains__(&self, key: &str) -> bool { + self.iter().any(|p| p.contains(key)) + } + + fn __iter__(&self) -> PyGenericIterator { + self.keys().into_iter().into() + } + + fn get(&self, key: ArcStr) -> Option { + self.__contains__(&key).then(|| { + let builder = self.builder.clone(); + let key = key.clone(); + (move || { + let key = key.clone(); + builder().map(move |p| p.get(&key)) + }) + .into() + }) + } +} + +pub struct OptionPyTemporalProp(Option); + +#[derive(PartialEq, FromPyObject, Clone)] +pub struct OptionPyTemporalPropCmp(Option); + +impl From> for OptionPyTemporalPropCmp { + fn from(value: Option) -> Self { + OptionPyTemporalPropCmp(value.map(|v| v.into())) + } +} + +impl Repr for OptionPyTemporalProp { + fn repr(&self) -> String { + self.0.repr() + } +} + +impl IntoPy for OptionPyTemporalProp { + fn into_py(self, py: Python<'_>) -> PyObject { + self.0.into_py(py) + } +} + +impl From> for OptionPyTemporalProp { + fn from(value: Option) -> Self { + Self(value.map(|v| v.into())) + } +} + +py_iterable!( + PyTemporalPropList, + Option, + OptionPyTemporalProp +); + +py_iterable_comp!( + PyTemporalPropList, + OptionPyTemporalPropCmp, + PyTemporalPropListCmp +); + +#[pymethods] +impl PyTemporalPropList { + #[getter] + pub fn history(&self) -> PyPropHistList { + let builder = self.builder.clone(); + (move || builder().map(|p| p.map(|v| v.history()).unwrap_or_default())).into() + } + + pub fn values(&self) -> PyPropHistValueList { + let builder = self.builder.clone(); + (move || builder().map(|p| p.map(|v| v.values()).unwrap_or_default())).into() + } + + pub fn items(&self) -> PyPropHistItemsList { + let builder = self.builder.clone(); + (move || builder().map(|p| p.map(|v| v.iter().collect::>()).unwrap_or_default())) + .into() + } + + pub fn at(&self, t: PyTime) -> PyPropValueList { + let t = t.into_time(); + let builder = self.builder.clone(); + (move || builder().map(move |p| p.and_then(|v| v.at(t)))).into() + } + + pub fn value(&self) -> PyPropValueList { + let builder = self.builder.clone(); + (move || builder().map(|p| p.and_then(|v| v.latest()))).into() + } +} + +py_nested_iterable_base!( + PyTemporalPropsListList, + DynTemporalProperties, + PyTemporalProperties +); + +#[derive(PartialEq)] +pub struct PyTemporalPropsListListCmp(HashMap); + +impl From<&PyTemporalPropsListList> for PyTemporalPropsListListCmp { + fn from(value: &PyTemporalPropsListList) -> Self { + Self( + value + .items() + .into_iter() + .map(|(k, v)| (k, v.into())) + .collect(), + ) + } +} + +impl From> for PyTemporalPropsListListCmp { + fn from(value: HashMap) -> Self { + Self(value) + } +} + +impl<'source> FromPyObject<'source> for PyTemporalPropsListListCmp { + fn extract(ob: &'source PyAny) -> PyResult { + if let Ok(v) = ob.extract::>() { + Ok(Self::from(v.deref())) + } else if let Ok(v) = ob.extract::>() { + Ok(Self::from(v)) + } else { + Err(PyTypeError::new_err("cannot compare")) + } + } +} + +py_eq!(PyTemporalPropsListList, PyTemporalPropsListListCmp); + +#[pymethods] +impl PyTemporalPropsListList { + fn keys(&self) -> Vec { + self.iter() + .flat_map( + |it| // FIXME: Still have to clone all those strings which sucks + it.map(|p| p.keys().map(|k| k.clone()).collect_vec()), + ) + .kmerge() + .dedup() + .collect() + } + fn values(&self) -> Vec { + self.keys() + .into_iter() + .map(|k| self.get(k).expect("key exists")) + .collect() + } + fn items(&self) -> Vec<(ArcStr, PyTemporalPropListList)> { + self.keys().into_iter().zip(self.values()).collect() + } + + fn latest(&self) -> HashMap { + let builder = self.builder.clone(); + self.keys() + .into_iter() + .map(move |k| { + let builder = builder.clone(); + let nk = k.clone(); + ( + k, + (move || { + let nk = nk.clone(); + builder().map(move |it| { + let nk = nk.clone(); + it.map(move |p| p.get(nk.as_ref()).and_then(|v| v.latest())) + }) + }) + .into(), + ) + }) + .collect() + } + + fn histories(&self) -> HashMap { + let builder = self.builder.clone(); + self.keys() + .into_iter() + .map(move |k| { + let builder = builder.clone(); + let kk = k.clone(); + let v = (move || { + let kk = kk.clone(); + builder().map(move |it| { + let kk = kk.clone(); + it.map(move |p| { + p.get(kk.as_ref()) + .map(|v| v.iter().collect::>()) + .unwrap_or_default() + }) + }) + }) + .into(); + (k, v) + }) + .collect() + } + + fn __getitem__(&self, key: ArcStr) -> PyResult { + self.get(key).ok_or(PyKeyError::new_err("unknown property")) + } + + fn __contains__(&self, key: &str) -> bool { + self.iter().any(|mut it| it.any(|p| p.contains(key))) + } + + fn __iter__(&self) -> PyGenericIterator { + self.keys().into_iter().into() + } + + fn get(&self, key: ArcStr) -> Option { + self.__contains__(&key).then(|| { + let builder = self.builder.clone(); + let key = key.clone(); + (move || { + let key = key.clone(); + builder().map(move |it| { + let key = key.clone(); + it.map(move |p| p.get(&key)) + }) + }) + .into() + }) + } +} + +py_nested_iterable!( + PyTemporalPropListList, + Option, + OptionPyTemporalProp +); + +py_iterable_comp!( + PyTemporalPropListList, + PyTemporalPropListCmp, + PyTemporalPropListListCmp +); + +#[pymethods] +impl PyTemporalPropListList { + #[getter] + pub fn history(&self) -> PyPropHistListList { + let builder = self.builder.clone(); + (move || builder().map(|it| it.map(|p| p.map(|v| v.history()).unwrap_or_default()))).into() + } + + pub fn values(&self) -> PyPropHistValueListList { + let builder = self.builder.clone(); + (move || builder().map(|it| it.map(|p| p.map(|v| v.values()).unwrap_or_default()))).into() + } + + pub fn items(&self) -> PyPropHistItemsListList { + let builder = self.builder.clone(); + (move || { + builder() + .map(|it| it.map(|p| p.map(|v| v.iter().collect::>()).unwrap_or_default())) + }) + .into() + } + + pub fn at(&self, t: PyTime) -> PyPropValueListList { + let t = t.into_time(); + let builder = self.builder.clone(); + (move || builder().map(move |it| it.map(move |p| p.and_then(|v| v.at(t))))).into() + } + + pub fn value(&self) -> PyPropValueListList { + let builder = self.builder.clone(); + (move || builder().map(|it| it.map(|p| p.and_then(|v| v.latest())))).into() + } + + pub fn flatten(&self) -> PyTemporalPropList { + let builder = self.builder.clone(); + (move || builder().flatten()).into() + } +} + +#[pymethods] +impl PyPropHistValueListList { + pub fn flatten(&self) -> PyPropHistValueList { + let builder = self.builder.clone(); + (move || builder().flatten()).into() + } + + pub fn count(&self) -> NestedUsizeIterable { + let builder = self.builder.clone(); + (move || builder().map(|it| it.map(|itit| itit.len()))).into() + } + + pub fn median(&self) -> PyPropValueListList { + let builder = self.builder.clone(); + (move || { + builder().map(|it| { + it.map(|itit| { + let mut sorted: Vec = itit.into_iter().collect(); + sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + let len = sorted.len(); + match len { + 0 => None, + 1 => Some(sorted[0].clone()), + _ => { + let a = &sorted[len / 2]; + Some(a.clone()) + } + } + }) + }) + }) + .into() + } + + pub fn sum(&self) -> PyPropValueListList { + let builder = self.builder.clone(); + (move || { + builder().map(|it| { + it.map(|itit| { + let mut itit_iter = itit.into_iter(); + let first = itit_iter.next(); + itit_iter.clone().fold(first, |acc, elem| match acc { + Some(a) => a.add(elem), + _ => None, + }) + }) + }) + }) + .into() + } + + pub fn mean(&self) -> PyPropValueListList { + let builder = self.builder.clone(); + (move || { + builder().map(|it| { + it.map(|itit| { + let mut itit_iter = itit.into_iter(); + let first = itit_iter.next(); + let sum = itit_iter.clone().fold(first, |acc, elem| match acc { + Some(a) => a.add(elem), + _ => Some(elem), + }); + let count = itit_iter.count(); + if count == 0 { + return None; + } + match sum { + Some(Prop::U8(s)) => Some(Prop::U8(s / count as u8)), + Some(Prop::U16(s)) => Some(Prop::U16(s / count as u16)), + Some(Prop::I32(s)) => Some(Prop::I32(s / count as i32)), + Some(Prop::I64(s)) => Some(Prop::I64(s / count as i64)), + Some(Prop::U32(s)) => Some(Prop::U32(s / count as u32)), + Some(Prop::U64(s)) => Some(Prop::U64(s / count as u64)), + Some(Prop::F32(s)) => Some(Prop::F32(s / count as f32)), + Some(Prop::F64(s)) => Some(Prop::F64(s / count as f64)), + _ => None, + } + }) + }) + }) + .into() + } +} + +#[pymethods] +impl PropIterable { + pub fn sum(&self) -> PropValue { + let mut it_iter = self.iter(); + let first = it_iter.next(); + it_iter.fold(first, |acc, elem| acc.and_then(|val| val.add(elem))) + } + + pub fn median(&self) -> PropValue { + let mut sorted: Vec = self.iter().collect(); + sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + let len = sorted.len(); + match len { + 0 => None, + 1 => Some(sorted[0].clone()), + _ => { + let a = &sorted[len / 2]; + Some(a.clone()) + } + } + } + + pub fn count(&self) -> usize { + self.iter().count() + } + + pub fn min(&self) -> PropValue { + let mut sorted: Vec = self.iter().collect(); + sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + let len = sorted.len(); + match len { + 0 => None, + _ => { + let a = &sorted[0]; + Some(a.clone()) + } + } + } + + pub fn max(&self) -> PropValue { + let mut sorted: Vec = self.iter().collect(); + sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + let len = sorted.len(); + match len { + 0 => None, + _ => { + let a = &sorted[len - 1]; + Some(a.clone()) + } + } + } + + pub fn average(&self) -> PropValue { + self.mean() + } + + pub fn mean(&self) -> PropValue { + let sum: PropValue = self.sum(); + let count: usize = self.iter().collect::>().len(); + if count == 0 { + return None; + } + match sum { + Some(Prop::U8(s)) => Some(Prop::F64(s as f64 / count as f64)), + Some(Prop::U16(s)) => Some(Prop::F64(s as f64 / count as f64)), + Some(Prop::I32(s)) => Some(Prop::F32(s as f32 / count as f32)), + Some(Prop::I64(s)) => Some(Prop::F64(s as f64 / count as f64)), + Some(Prop::U32(s)) => Some(Prop::F32(s as f32 / count as f32)), + Some(Prop::U64(s)) => Some(Prop::F64(s as f64 / count as f64)), + Some(Prop::F32(s)) => Some(Prop::F32(s / count as f32)), + Some(Prop::F64(s)) => Some(Prop::F64(s / count as f64)), + _ => None, + } + } +} + +#[pymethods] +impl PyPropHistValueList { + pub fn sum(&self) -> PyPropValueList { + let builder = self.builder.clone(); + (move || { + builder().map(|it| { + let mut it_iter = it.into_iter(); + let first = it_iter.next(); + it_iter.fold(first, |acc, elem| acc.and_then(|val| val.add(elem))) + }) + }) + .into() + } + + pub fn min(&self) -> PyPropValueList { + let builder = self.builder.clone(); + (move || { + builder().map(|it| { + let mut it_iter = it.into_iter(); + let first = it_iter.next(); + it_iter.fold(first, |a, b| { + match PartialOrd::partial_cmp(&a, &Some(b.clone())) { + Some(std::cmp::Ordering::Less) => a, + _ => Some(b), + } + }) + }) + }) + .into() + } + + pub fn max(&self) -> PyPropValueList { + let builder = self.builder.clone(); + (move || { + builder().map(|it| { + let mut it_iter = it.into_iter(); + let first = it_iter.next(); + it_iter.fold(first, |a, b| { + match PartialOrd::partial_cmp(&a, &Some(b.clone())) { + Some(std::cmp::Ordering::Greater) => a, + _ => Some(b), + } + }) + }) + }) + .into() + } + + pub fn median(&self) -> PyPropValueList { + let builder = self.builder.clone(); + (move || { + builder().map(|it| { + let mut sorted: Vec = it.clone(); + sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + let len = sorted.len(); + match len { + 0 => None, + 1 => Some(sorted[0].clone()), + _ => { + let a = &sorted[len / 2]; + Some(a.clone()) + } + } + }) + }) + .into() + } + + pub fn average(&self) -> PyPropValueList { + self.mean() + } + + pub fn mean(&self) -> PyPropValueList { + let builder = self.builder.clone(); + (move || { + builder().map(|it| { + let mut it_iter = it.clone().into_iter(); + let first = it_iter.next(); + let sum = it_iter.fold(first, |acc, elem| acc.and_then(|val| val.add(elem))); + let count = it.len(); + if count == 0 { + return None; + } + match sum { + Some(Prop::U8(s)) => Some(Prop::F64(s as f64 / count as f64)), + Some(Prop::U16(s)) => Some(Prop::F64(s as f64 / count as f64)), + Some(Prop::I32(s)) => Some(Prop::F32(s as f32 / count as f32)), + Some(Prop::I64(s)) => Some(Prop::F64(s as f64 / count as f64)), + Some(Prop::U32(s)) => Some(Prop::F32(s as f32 / count as f32)), + Some(Prop::U64(s)) => Some(Prop::F64(s as f64 / count as f64)), + Some(Prop::F32(s)) => Some(Prop::F32(s / count as f32)), + Some(Prop::F64(s)) => Some(Prop::F64(s / count as f64)), + _ => None, + } + }) + }) + .into() + } + + pub fn count(&self) -> UsizeIterable { + let builder = self.builder.clone(); + (move || builder().map(|it| it.len())).into() + } + + pub fn flatten(&self) -> PropIterable { + let builder = self.builder.clone(); + (move || builder().flatten()).into() + } +} + +#[pymethods] +impl PyPropValueList { + pub fn sum(&self) -> Option { + self.iter() + .reduce(|acc, elem| match (acc, elem) { + (Some(a), Some(b)) => a.add(b), + (Some(a), None) => Some(a), + (None, Some(b)) => Some(b), + _ => None, + }) + .flatten() + } + + pub fn count(&self) -> usize { + self.iter().count() + } + + pub fn min(&self) -> PropValue { + let mut sorted: Vec = self.iter().collect(); + sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + let len = sorted.len(); + match len { + 0 => None, + _ => { + let a = &sorted[0]; + a.clone() + } + } + } + + pub fn max(&self) -> PropValue { + let mut sorted: Vec = self.iter().collect(); + sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + let len = sorted.len(); + match len { + 0 => None, + _ => { + let a = &sorted[len - 1]; + a.clone() + } + } + } + + pub fn drop_none(&self) -> PyPropValueList { + let builder = self.builder.clone(); + (move || builder().filter(|x| x.is_some())).into() + } + + pub fn median(&self) -> PropValue { + let mut sorted: Vec = self.iter().collect(); + sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + let len = sorted.len(); + match len { + 0 => None, + 1 => sorted[0].clone(), + _ => { + let a = &sorted[len / 2]; + a.clone() + } + } + } + + pub fn mean(&self) -> PropValue { + let sum: PropValue = self.sum(); + let count: usize = self.iter().collect::>().len(); + if count == 0 { + return None; + } + match sum { + Some(Prop::U8(s)) => Some(Prop::F64(s as f64 / count as f64)), + Some(Prop::U16(s)) => Some(Prop::F64(s as f64 / count as f64)), + Some(Prop::I32(s)) => Some(Prop::F32(s as f32 / count as f32)), + Some(Prop::I64(s)) => Some(Prop::F64(s as f64 / count as f64)), + Some(Prop::U32(s)) => Some(Prop::F32(s as f32 / count as f32)), + Some(Prop::U64(s)) => Some(Prop::F64(s as f64 / count as f64)), + Some(Prop::F32(s)) => Some(Prop::F32(s / count as f32)), + Some(Prop::F64(s)) => Some(Prop::F64(s / count as f64)), + _ => None, + } + } + + pub fn average(&self) -> PropValue { + self.mean() + } +} + +#[pymethods] +impl PyPropValueListList { + pub fn sum(&self) -> PyPropValueList { + let builder = self.builder.clone(); + (move || { + builder().map(|it| { + let mut it_iter = it.into_iter(); + let first = it_iter.next().flatten(); + it_iter.fold(first, |acc, elem| match (acc, elem) { + (Some(a), Some(b)) => a.add(b), + (Some(a), None) => Some(a), + (None, Some(b)) => Some(b), + _ => None, + }) + }) + }) + .into() + } + + pub fn min(&self) -> PyPropValueList { + let builder = self.builder.clone(); + (move || { + builder().map(|it| { + let mut it_iter = it.into_iter(); + let first = it_iter.next().unwrap(); + it_iter.fold(first, |a, b| { + match PartialOrd::partial_cmp(&a, &Some(b.clone().unwrap())) { + Some(std::cmp::Ordering::Less) => a, + _ => Some(b.clone().unwrap()), + } + }) + }) + }) + .into() + } + + pub fn max(&self) -> PyPropValueList { + let builder = self.builder.clone(); + (move || { + builder().map(|it| { + let mut it_iter = it.into_iter(); + let first = it_iter.next().unwrap(); + it_iter.fold(first, |a, b| { + match PartialOrd::partial_cmp(&a, &Some(b.clone().unwrap())) { + Some(std::cmp::Ordering::Greater) => a, + _ => Some(b.clone().unwrap()), + } + }) + }) + }) + .into() + } + + pub fn average(&self) -> PyPropValueList { + self.mean() + } + + pub fn mean(&self) -> PyPropValueList { + let builder = self.builder.clone(); + (move || { + builder().map(|mut it| { + let mut count: usize = 1; + let first = it.next().flatten(); + let sum = it.fold(first, |acc, elem| { + count += 1; + match (acc, elem) { + (Some(a), Some(b)) => a.add(b), + (Some(a), None) => Some(a), + (None, Some(b)) => Some(b), + _ => None, + } + }); + if count == 0 { + return None; + } + match sum { + Some(Prop::U8(s)) => Some(Prop::F64(s as f64 / count as f64)), + Some(Prop::U16(s)) => Some(Prop::F64(s as f64 / count as f64)), + Some(Prop::I32(s)) => Some(Prop::F32(s as f32 / count as f32)), + Some(Prop::I64(s)) => Some(Prop::F64(s as f64 / count as f64)), + Some(Prop::U32(s)) => Some(Prop::F32(s as f32 / count as f32)), + Some(Prop::U64(s)) => Some(Prop::F64(s as f64 / count as f64)), + Some(Prop::F32(s)) => Some(Prop::F32(s / count as f32)), + Some(Prop::F64(s)) => Some(Prop::F64(s / count as f64)), + _ => None, + } + }) + }) + .into() + } + + pub fn median(&self) -> PyPropValueList { + let builder = self.builder.clone(); + (move || { + builder().map(|it| { + let mut sorted: Vec = it.into_iter().collect(); + sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)); + let len = sorted.len(); + match len { + 0 => None, + 1 => sorted[0].clone(), + _ => { + let a = &sorted[len / 2]; + a.clone() + } + } + }) + }) + .into() + } + + pub fn flatten(&self) -> PyPropValueList { + let builder = self.builder.clone(); + (move || builder().flatten()).into() + } + + pub fn count(&self) -> UsizeIterable { + let builder = self.builder.clone(); + (move || builder().map(|it| it.count())).into() + } + + pub fn drop_none(&self) -> PyPropValueListList { + let builder = self.builder.clone(); + (move || builder().map(|it| it.filter(|x| x.is_some()))).into() + } +} + +py_iterable!(PyPropHistList, Vec); +py_iterable_comp!(PyPropHistList, Vec, PyPropHistListCmp); +py_nested_iterable!(PyPropHistListList, Vec); +py_iterable_comp!(PyPropHistListList, PyPropHistListCmp, PyPropHistListListCmp); + +py_iterable!(PyPropHistValueList, Vec); +py_iterable_comp!(PyPropHistValueList, Vec, PyPropHistValueListCmp); +py_nested_iterable!(PyPropHistValueListList, Vec); +py_iterable_comp!( + PyPropHistValueListList, + PyPropHistValueListCmp, + PropHistValueListListCmp +); + +py_iterable!(PyPropHistItemsList, PropHistItems); +py_iterable_comp!(PyPropHistItemsList, PropHistItems, PyPropHistItemsListCmp); +py_nested_iterable!(PyPropHistItemsListList, PropHistItems); +py_iterable_comp!( + PyPropHistItemsListList, + PyPropHistItemsListCmp, + PyPropHistItemsListListCmp +); diff --git a/py-raphtory/src/vertex.rs b/raphtory/src/python/graph/vertex.rs similarity index 59% rename from py-raphtory/src/vertex.rs rename to raphtory/src/python/graph/vertex.rs index 841da8fd8d..0cee2bcbb8 100644 --- a/py-raphtory/src/vertex.rs +++ b/raphtory/src/python/graph/vertex.rs @@ -1,32 +1,55 @@ //! Defines the `Vertex`, which represents a vertex in the graph. //! A vertex is a node in the graph, and can have properties and edges. //! It can also be used to navigate the graph. -use crate::dynamic::{DynamicGraph, IntoDynamic}; -use crate::edge::{PyEdges, PyNestedEdges}; -use crate::types::repr::{iterator_repr, Repr}; -use crate::utils::{ - at_impl, expanding_impl, extract_vertex_ref, rolling_impl, window_impl, IntoPyObject, - PyWindowSet, +use crate::{ + core::{ + entities::vertices::vertex_ref::VertexRef, + utils::{errors::GraphError, time::error::ParseTimeError}, + Prop, + }, + db::{ + api::{ + properties::Properties, + view::{ + internal::{DynamicGraph, Immutable, IntoDynamic, MaterializedGraph}, + *, + }, + }, + graph::{ + path::{PathFromGraph, PathFromVertex}, + vertex::VertexView, + vertices::Vertices, + views::{ + deletion_graph::GraphWithDeletions, layer_graph::LayeredGraph, + window_graph::WindowedGraph, + }, + }, + }, + prelude::Graph, + python::{ + graph::{ + edge::{PyEdges, PyNestedEdges}, + properties::{PyNestedPropsIterable, PyPropsList}, + }, + types::wrappers::iterators::*, + utils::{PyInterval, PyTime}, + }, + *, }; -use crate::wrappers::iterators::*; -use crate::wrappers::prop::Prop; use chrono::NaiveDateTime; use itertools::Itertools; -use pyo3::exceptions::PyIndexError; -use pyo3::prelude::*; -use pyo3::{pyclass, pymethods, PyAny, PyObject, PyRef, PyRefMut, PyResult, Python}; -use raphtory::core::vertex_ref::VertexRef; -use raphtory::db::path::{PathFromGraph, PathFromVertex}; -use raphtory::db::vertex::VertexView; -use raphtory::db::vertices::Vertices; -use raphtory::db::view_api::layer::LayerOps; -use raphtory::db::view_api::*; -use raphtory::*; -use std::collections::HashMap; -use std::sync::Arc; +use pyo3::{ + exceptions::{PyIndexError, PyKeyError}, + prelude::*, + pyclass, + pyclass::CompareOp, + pymethods, PyAny, PyObject, PyRef, PyRefMut, PyResult, Python, +}; +use python::types::repr::{iterator_repr, Repr}; +use std::{collections::HashMap, ops::Deref}; /// A vertex (or node) in the graph. -#[pyclass(name = "Vertex")] +#[pyclass(name = "Vertex", subclass)] #[derive(Clone)] pub struct PyVertex { vertex: VertexView, @@ -43,13 +66,6 @@ impl From> for PyVertex { } } -impl IntoPyObject for VertexView { - fn into_py_object(self) -> PyObject { - let py_version: PyVertex = self.into(); - Python::with_gil(|py| py_version.into_py(py)) - } -} - /// Converts a python vertex into a rust vertex. impl From for VertexRef { fn from(value: PyVertex) -> Self { @@ -62,11 +78,42 @@ impl From for VertexRef { /// It can also be used to navigate the graph. #[pymethods] impl PyVertex { + /// Rich Comparison for Vertex objects + pub fn __richcmp__(&self, other: PyRef, op: CompareOp) -> Py { + let py = other.py(); + match op { + CompareOp::Eq => (self.vertex.id() == other.id()).into_py(py), + CompareOp::Ne => (self.vertex.id() != other.id()).into_py(py), + _ => py.NotImplemented(), + } + } + + /// TODO: uncomment when we update to py03 0.2 + /// checks if a vertex is equal to another by their id (ids are unqiue) + /// + /// Arguments: + /// other: The other vertex to compare to. + /// + /// Returns: + /// True if the vertices are equal, false otherwise. + // pub fn __eq__(&self, other: &PyVertex) -> bool { + // self.vertex.id() == other.vertex.id() + // } + + /// Returns the hash of the vertex. + /// + /// Returns: + /// The vertex id. + pub fn __hash__(&self) -> u64 { + self.vertex.id() + } + /// Returns the id of the vertex. /// This is a unique identifier for the vertex. /// /// Returns: /// The id of the vertex as an integer. + #[getter] pub fn id(&self) -> u64 { self.vertex.id() } @@ -75,6 +122,7 @@ impl PyVertex { /// /// Returns: /// The name of the vertex as a string. + #[getter] pub fn name(&self) -> String { self.vertex.name() } @@ -86,6 +134,7 @@ impl PyVertex { /// /// Returns: /// The earliest time that the vertex exists as an integer. + #[getter] pub fn earliest_time(&self) -> Option { self.vertex.earliest_time() } @@ -97,15 +146,17 @@ impl PyVertex { /// /// Returns: /// The earliest datetime that the vertex exists as an integer. + #[getter] pub fn earliest_date_time(&self) -> Option { let earliest_time = self.vertex.earliest_time()?; - Some(NaiveDateTime::from_timestamp_millis(earliest_time).unwrap()) + NaiveDateTime::from_timestamp_millis(earliest_time) } /// Returns the latest time that the vertex exists. /// /// Returns: /// The latest time that the vertex exists as an integer. + #[getter] pub fn latest_time(&self) -> Option { self.vertex.latest_time() } @@ -117,117 +168,16 @@ impl PyVertex { /// /// Returns: /// The latest datetime that the vertex exists as an integer. + #[getter] pub fn latest_date_time(&self) -> Option { let latest_time = self.vertex.latest_time()?; - Some(NaiveDateTime::from_timestamp_millis(latest_time).unwrap()) + NaiveDateTime::from_timestamp_millis(latest_time) } - /// Gets the property value of this vertex given the name of the property. - /// - /// Arguments: - /// name: The name of the property. - /// include_static: Whether to include static properties. Defaults to true. - /// - /// Returns: - /// The property value as a `Prop` object. - pub fn property(&self, name: String, include_static: Option) -> Option { - let include_static = include_static.unwrap_or(true); - self.vertex - .property(name, include_static) - .map(|prop| prop.into()) - } - - /// Returns the history of a property value of a vertex at all times - /// - /// Arguments: - /// name: The name of the property. - /// - /// Returns: - /// A list of tuples of the form (time, value) where time is an integer and value is a `Prop` object. - pub fn property_history(&self, name: String) -> Vec<(i64, Prop)> { - self.vertex - .property_history(name) - .into_iter() - .map(|(k, v)| (k, v.into())) - .collect() - } - - /// Returns all the properties of the vertex as a dictionary. - /// - /// Arguments: - /// include_static: Whether to include static properties. Defaults to true. - /// - /// Returns: - /// A dictionary of the form {name: value} where name is a string and value is a `Prop` object. - pub fn properties(&self, include_static: Option) -> HashMap { - let include_static = include_static.unwrap_or(true); - self.vertex - .properties(include_static) - .into_iter() - .map(|(k, v)| (k, v.into())) - .collect() - } - - /// Returns all the properties of the vertex as a dictionary including the history of each property. - /// - /// Arguments: - /// include_static: Whether to include static properties. Defaults to true. - /// - /// Returns: - /// A dictionary of the form {name: [(time, value)]} where name is a string, time is an integer, and value is a `Prop` object. - pub fn property_histories(&self) -> HashMap> { - self.vertex - .property_histories() - .into_iter() - .map(|(k, v)| (k, v.into_iter().map(|(t, p)| (t, p.into())).collect())) - .collect() - } - - /// Returns the names of all the properties of the vertex. - /// - /// Arguments: - /// include_static: Whether to include static properties. Defaults to true. - /// - /// Returns: - /// A list of strings of propert names. - pub fn property_names(&self, include_static: Option) -> Vec { - let include_static = include_static.unwrap_or(true); - self.vertex.property_names(include_static) - } - - /// Checks if a property exists on this vertex. - /// - /// Arguments: - /// name: The name of the property. - /// include_static: Whether to include static properties. Defaults to true. - /// - /// Returns: - /// True if the property exists, false otherwise. - pub fn has_property(&self, name: String, include_static: Option) -> bool { - let include_static = include_static.unwrap_or(true); - self.vertex.has_property(name, include_static) - } - - /// Checks if a static property exists on this vertex. - /// - /// Arguments: - /// name: The name of the property. - /// - /// Returns: - /// True if the property exists, false otherwise. - pub fn has_static_property(&self, name: String) -> bool { - self.vertex.has_static_property(name) - } - - /// Returns the static property value of this vertex given the name of the property. - /// - /// Arguments: - /// name: The name of the property. - /// - /// Returns: - /// The property value as a `Prop` object or None if the property does not exist. - pub fn static_property(&self, name: String) -> Option { - self.vertex.static_property(name).map(|prop| prop.into()) + /// The properties of the vertex + #[getter] + pub fn properties(&self) -> Properties> { + self.vertex.properties() } /// Get the degree of this vertex (i.e., the number of edges that are incident to it). @@ -258,6 +208,7 @@ impl PyVertex { /// /// Returns: /// A list of `Edge` objects. + #[getter] pub fn edges(&self) -> PyEdges { let vertex = self.vertex.clone(); (move || vertex.edges()).into() @@ -267,6 +218,7 @@ impl PyVertex { /// /// Returns: /// A list of `Edge` objects. + #[getter] pub fn in_edges(&self) -> PyEdges { let vertex = self.vertex.clone(); (move || vertex.in_edges()).into() @@ -276,6 +228,7 @@ impl PyVertex { /// /// Returns: /// A list of `Edge` objects. + #[getter] pub fn out_edges(&self) -> PyEdges { let vertex = self.vertex.clone(); (move || vertex.out_edges()).into() @@ -286,6 +239,7 @@ impl PyVertex { /// Returns: /// /// A list of `Vertex` objects. + #[getter] pub fn neighbours(&self) -> PyPathFromVertex { self.vertex.neighbours().into() } @@ -294,6 +248,7 @@ impl PyVertex { /// /// Returns: /// A list of `Vertex` objects. + #[getter] pub fn in_neighbours(&self) -> PyPathFromVertex { self.vertex.in_neighbours().into() } @@ -302,6 +257,7 @@ impl PyVertex { /// /// Returns: /// A list of `Vertex` objects. + #[getter] pub fn out_neighbours(&self) -> PyPathFromVertex { self.vertex.out_neighbours().into() } @@ -312,6 +268,7 @@ impl PyVertex { /// /// Returns: /// The earliest time that this vertex is valid or None if the vertex is valid for all times. + #[getter] pub fn start(&self) -> Option { self.vertex.start() } @@ -320,15 +277,17 @@ impl PyVertex { /// /// Returns: /// The earliest datetime that this vertex is valid or None if the vertex is valid for all times. + #[getter] pub fn start_date_time(&self) -> Option { let start_time = self.vertex.start()?; - Some(NaiveDateTime::from_timestamp_millis(start_time).unwrap()) + NaiveDateTime::from_timestamp_millis(start_time) } /// Gets the latest time that this vertex is valid. /// /// Returns: /// The latest time that this vertex is valid or None if the vertex is valid for all times. + #[getter] pub fn end(&self) -> Option { self.vertex.end() } @@ -337,9 +296,10 @@ impl PyVertex { /// /// Returns: /// The latest datetime that this vertex is valid or None if the vertex is valid for all times. + #[getter] pub fn end_date_time(&self) -> Option { let end_time = self.vertex.end()?; - Some(NaiveDateTime::from_timestamp_millis(end_time).unwrap()) + NaiveDateTime::from_timestamp_millis(end_time) } /// Creates a `PyVertexWindowSet` with the given `step` size and optional `start` and `end` times, @@ -356,8 +316,11 @@ impl PyVertex { /// /// Returns: /// A `PyVertexWindowSet` object. - fn expanding(&self, step: &PyAny) -> PyResult { - expanding_impl(&self.vertex, step) + fn expanding( + &self, + step: PyInterval, + ) -> Result>, ParseTimeError> { + self.vertex.expanding(step) } /// Creates a `PyVertexWindowSet` with the given `window` size and optional `step`, `start` and `end` times, @@ -375,8 +338,12 @@ impl PyVertex { /// /// Returns: /// A `PyVertexWindowSet` object. - fn rolling(&self, window: &PyAny, step: Option<&PyAny>) -> PyResult { - rolling_impl(&self.vertex, window, step) + fn rolling( + &self, + window: PyInterval, + step: Option, + ) -> Result>, ParseTimeError> { + self.vertex.rolling(window, step) } /// Create a view of the vertex including all events between `t_start` (inclusive) and `t_end` (exclusive) @@ -388,8 +355,13 @@ impl PyVertex { /// Returns: /// A `PyVertex` object. #[pyo3(signature = (t_start = None, t_end = None))] - pub fn window(&self, t_start: Option<&PyAny>, t_end: Option<&PyAny>) -> PyResult { - window_impl(&self.vertex, t_start, t_end).map(|v| v.into()) + pub fn window( + &self, + t_start: Option, + t_end: Option, + ) -> VertexView> { + self.vertex + .window(t_start.unwrap_or(PyTime::MIN), t_end.unwrap_or(PyTime::MAX)) } /// Create a view of the vertex including all events at `t`. @@ -400,8 +372,8 @@ impl PyVertex { /// Returns: /// A `PyVertex` object. #[pyo3(signature = (end))] - pub fn at(&self, end: &PyAny) -> PyResult { - at_impl(&self.vertex, end).map(|v| v.into()) + pub fn at(&self, end: PyTime) -> VertexView> { + self.vertex.at(end) } #[doc = default_layer_doc_string!()] @@ -409,10 +381,16 @@ impl PyVertex { self.vertex.default_layer().into() } - #[doc = layer_doc_string!()] + #[doc = layers_doc_string!()] + #[pyo3(signature = (names))] + pub fn layers(&self, names: Vec) -> Option>> { + self.vertex.layer(names) + } + + #[doc = layers_doc_string!()] #[pyo3(signature = (name))] - pub fn layer(&self, name: &str) -> Option { - Some(self.vertex.layer(name)?.into()) + pub fn layer(&self, name: String) -> Option>> { + self.vertex.layer(name) } /// Returns the history of a vertex, including vertex additions and changes made to vertex. @@ -424,8 +402,11 @@ impl PyVertex { } //****** Python ******// - pub fn __getitem__(&self, name: String) -> Option { - self.property(name, Some(true)) + pub fn __getitem__(&self, name: &str) -> PyResult { + self.vertex + .properties() + .get(name) + .ok_or(PyKeyError::new_err(format!("Unknown property {}", name))) } /// Display the vertex as a string. @@ -433,28 +414,111 @@ impl PyVertex { self.repr() } } - impl Repr for PyVertex { fn repr(&self) -> String { + self.vertex.repr() + } +} + +impl Repr for VertexView { + fn repr(&self) -> String { + let earliest_time = self.earliest_time().repr(); + let latest_time = self.latest_time().repr(); let properties: String = self - .properties(Some(true)) + .properties() .iter() - .map(|(k, v)| k.to_string() + " : " + &v.to_string()) + .map(|(k, v)| format!("{}: {}", k.deref(), v)) .join(", "); - if properties.is_empty() { - format!("Vertex(name={})", self.name().trim_matches('"')) + format!( + "Vertex(name={}, earliest_time={:?}, latest_time={:?})", + self.name().trim_matches('"'), + earliest_time, + latest_time + ) } else { - let property_string: String = "{".to_owned() + &properties + "}"; format!( - "Vertex(name={}, properties={})", + "Vertex(name={}, earliest_time={:?}, latest_time={:?}, properties={})", self.name().trim_matches('"'), - property_string + earliest_time, + latest_time, + format!("{{{properties}}}") ) } } } +#[pyclass(name = "MutableVertex", extends=PyVertex)] +pub struct PyMutableVertex { + vertex: VertexView, +} + +impl Repr for PyMutableVertex { + fn repr(&self) -> String { + self.vertex.repr() + } +} + +impl From> for PyMutableVertex { + fn from(vertex: VertexView) -> Self { + Self { vertex } + } +} + +impl IntoPy for VertexView { + fn into_py(self, py: Python<'_>) -> PyObject { + PyVertex::from(self).into_py(py) + } +} + +impl IntoPy for VertexView { + fn into_py(self, py: Python<'_>) -> PyObject { + let graph: MaterializedGraph = self.graph.into(); + let vertex = self.vertex; + let vertex = VertexView { graph, vertex }; + vertex.into_py(py) + } +} + +impl IntoPy for VertexView { + fn into_py(self, py: Python<'_>) -> PyObject { + let graph: MaterializedGraph = self.graph.into(); + let vertex = self.vertex; + let vertex = VertexView { graph, vertex }; + vertex.into_py(py) + } +} + +impl IntoPy for VertexView { + fn into_py(self, py: Python<'_>) -> PyObject { + Py::new( + py, + (PyMutableVertex::from(self.clone()), PyVertex::from(self)), + ) + .unwrap() // I think this only fails if we are out of memory? Seems to be unavoidable! + .into_py(py) + } +} + +#[pymethods] +impl PyMutableVertex { + fn add_updates( + &self, + t: PyTime, + properties: Option>, + ) -> Result<(), GraphError> { + self.vertex.add_updates(t, properties.unwrap_or_default()) + } + + fn add_constant_properties(&self, properties: HashMap) -> Result<(), GraphError> { + self.vertex.add_constant_properties(properties) + } + + fn __repr__(&self) -> String { + self.repr() + } +} + /// A list of vertices that can be iterated over. #[pyclass(name = "Vertices")] pub struct PyVertices { @@ -469,10 +533,9 @@ impl From> for PyVertices { } } -impl IntoPyObject for Vertices { - fn into_py_object(self) -> PyObject { - let py_version: PyVertices = self.into(); - Python::with_gil(|py| py_version.into_py(py)) +impl IntoPy for Vertices { + fn into_py(self, py: Python<'_>) -> PyObject { + PyVertices::from(self).into_py(py) } } @@ -480,137 +543,219 @@ impl IntoPyObject for Vertices { /// These use all the same functions as a normal vertex except it returns a list of results. #[pymethods] impl PyVertices { + /// checks if a list of vertices is equal to another list by their idd (ids are unique) + /// + /// Arguments: + /// other: The other vertices to compare to. + /// + /// Returns: + /// True if the vertices are equal, false otherwise. + fn __eq__(&self, other: &PyVertices) -> bool { + for (v1, v2) in self.vertices.iter().zip(other.vertices.iter()) { + if v1.id() != v2.id() { + return false; + } + } + true + } + + /// Returns an iterator over the vertices ids + #[getter] fn id(&self) -> U64Iterable { let vertices = self.vertices.clone(); (move || vertices.id()).into() } + /// Returns an iterator over the vertices name + #[getter] fn name(&self) -> StringIterable { let vertices = self.vertices.clone(); (move || vertices.name()).into() } + /// Returns an iterator over the vertices earliest time + #[getter] fn earliest_time(&self) -> OptionI64Iterable { let vertices = self.vertices.clone(); (move || vertices.earliest_time()).into() } + /// Returns an iterator over the vertices latest time + #[getter] fn latest_time(&self) -> OptionI64Iterable { let vertices = self.vertices.clone(); (move || vertices.latest_time()).into() } - fn property(&self, name: String, include_static: Option) -> OptionPropIterable { - let vertices = self.vertices.clone(); - (move || vertices.property(name.clone(), include_static.unwrap_or(true))).into() - } - - fn property_history(&self, name: String) -> PropHistoryIterable { - let vertices = self.vertices.clone(); - (move || vertices.property_history(name.clone())).into() - } - - fn properties(&self, include_static: Option) -> PropsIterable { - let vertices = self.vertices.clone(); - (move || vertices.properties(include_static.unwrap_or(true))).into() - } - - fn property_histories(&self) -> PropHistoriesIterable { - let vertices = self.vertices.clone(); - (move || vertices.property_histories()).into() - } - - fn property_names(&self, include_static: Option) -> StringVecIterable { - let vertices = self.vertices.clone(); - (move || vertices.property_names(include_static.unwrap_or(true))).into() - } - - fn has_property(&self, name: String, include_static: Option) -> BoolIterable { - let vertices = self.vertices.clone(); - (move || vertices.has_property(name.clone(), include_static.unwrap_or(true))).into() - } - - fn has_static_property(&self, name: String) -> BoolIterable { + #[getter] + fn properties(&self) -> PyPropsList { let vertices = self.vertices.clone(); - (move || vertices.has_static_property(name.clone())).into() - } - - fn static_property(&self, name: String) -> OptionPropIterable { - let vertices = self.vertices.clone(); - (move || vertices.static_property(name.clone())).into() + (move || vertices.properties()).into() } + /// Returns the number of edges of the vertices + /// + /// Returns: + /// An iterator of the number of edges of the vertices fn degree(&self) -> UsizeIterable { let vertices = self.vertices.clone(); (move || vertices.degree()).into() } + /// Returns the number of in edges of the vertices + /// + /// Returns: + /// An iterator of the number of in edges of the vertices fn in_degree(&self) -> UsizeIterable { let vertices = self.vertices.clone(); (move || vertices.in_degree()).into() } + /// Returns the number of out edges of the vertices + /// + /// Returns: + /// An iterator of the number of out edges of the vertices fn out_degree(&self) -> UsizeIterable { let vertices = self.vertices.clone(); (move || vertices.out_degree()).into() } + /// Returns the edges of the vertices + /// + /// Returns: + /// An iterator of edges of the vertices + #[getter] fn edges(&self) -> PyNestedEdges { let clone = self.vertices.clone(); (move || clone.edges()).into() } + /// Returns the in edges of the vertices + /// + /// Returns: + /// An iterator of in edges of the vertices + #[getter] fn in_edges(&self) -> PyNestedEdges { let clone = self.vertices.clone(); (move || clone.in_edges()).into() } + /// Returns the out edges of the vertices + /// + /// Returns: + /// An iterator of out edges of the vertices + #[getter] fn out_edges(&self) -> PyNestedEdges { let clone = self.vertices.clone(); (move || clone.out_edges()).into() } - fn out_neighbours(&self) -> PyPathFromGraph { - self.vertices.out_neighbours().into() + /// Get the neighbours of the vertices + /// + /// Returns: + /// An iterator of the neighbours of the vertices + #[getter] + fn neighbours(&self) -> PyPathFromGraph { + self.vertices.neighbours().into() } + /// Get the in neighbours of the vertices + /// + /// Returns: + /// An iterator of the in neighbours of the vertices + #[getter] fn in_neighbours(&self) -> PyPathFromGraph { self.vertices.in_neighbours().into() } - fn neighbours(&self) -> PyPathFromGraph { - self.vertices.neighbours().into() + /// Get the out neighbours of the vertices + /// + /// Returns: + /// An iterator of the out neighbours of the vertices + #[getter] + fn out_neighbours(&self) -> PyPathFromGraph { + self.vertices.out_neighbours().into() } + /// Collects all vertices into a list fn collect(&self) -> Vec { self.__iter__().into_iter().collect() } - //****** Perspective APIS ******// + //***** Perspective APIS ******// + /// Returns the start time of the vertices + #[getter] pub fn start(&self) -> Option { self.vertices.start() } + /// Returns the end time of the vertices + #[getter] pub fn end(&self) -> Option { self.vertices.end() } #[doc = window_size_doc_string!()] + #[getter] pub fn window_size(&self) -> Option { self.vertices.window_size() } - fn expanding(&self, step: &PyAny) -> PyResult { - expanding_impl(&self.vertices, step) + /// Creates a PyVertexWindowSet with the given step size using an expanding window. + /// + /// An expanding window is a window that grows by step size at each iteration. + /// This will tell you whether a vertex exists at different points in the window + /// and what its properties are at those points. + /// + /// Arguments: + /// `step` - The step size of the window + /// + /// Returns: + /// A PyVertexWindowSet with the given step size and optional start and end times or an error + fn expanding( + &self, + step: PyInterval, + ) -> Result>, ParseTimeError> { + self.vertices.expanding(step) } - fn rolling(&self, window: &PyAny, step: Option<&PyAny>) -> PyResult { - rolling_impl(&self.vertices, window, step) + /// Creates a PyVertexWindowSet with the given window size and optional step using a rolling window. + /// + /// A rolling window is a window that moves forward by step size at each iteration. + /// This will tell you whether a vertex exists at different points in the window and + /// what its properties are at those points. + /// + /// Arguments: + /// `window` - The window size of the window + /// `step` - The step size of the window + /// + /// Returns: + /// A PyVertexWindowSet with the given window size and optional step size or an error + fn rolling( + &self, + window: PyInterval, + step: Option, + ) -> Result>, ParseTimeError> { + self.vertices.rolling(window, step) } + /// Create a view of the vertices including all events between t_start (inclusive) and + /// t_end (exclusive) + /// + /// Arguments: + /// `t_start` - The start time of the window + /// `t_end` - The end time of the window + /// + /// Returns: + /// A `PyVertices` object. #[pyo3(signature = (t_start = None, t_end = None))] - pub fn window(&self, t_start: Option<&PyAny>, t_end: Option<&PyAny>) -> PyResult { - window_impl(&self.vertices, t_start, t_end).map(|v| v.into()) + pub fn window( + &self, + t_start: Option, + t_end: Option, + ) -> Vertices> { + self.vertices + .window(t_start.unwrap_or(PyTime::MIN), t_end.unwrap_or(PyTime::MAX)) } /// Create a view of the vertices including all events at `t`. @@ -621,8 +766,8 @@ impl PyVertices { /// Returns: /// A `PyVertices` object. #[pyo3(signature = (end))] - pub fn at(&self, end: &PyAny) -> PyResult { - at_impl(&self.vertices, end).map(|v| v.into()) + pub fn at(&self, end: PyTime) -> Vertices> { + self.vertices.at(end) } #[doc = default_layer_doc_string!()] @@ -630,10 +775,10 @@ impl PyVertices { self.vertices.default_layer().into() } - #[doc = layer_doc_string!()] + #[doc = layers_doc_string!()] #[pyo3(signature = (name))] - pub fn layer(&self, name: &str) -> Option { - Some(self.vertices.layer(name)?.into()) + pub fn layer(&self, name: &str) -> Option>> { + self.vertices.layer(name) } //****** Python ******* @@ -649,16 +794,10 @@ impl PyVertices { self.vertices.is_empty() } - pub fn __getitem__(&self, vertex: &PyAny) -> PyResult { - let vref = extract_vertex_ref(vertex)?; - self.vertices.get(vref).map_or_else( - || Err(PyIndexError::new_err("Vertex does not exist")), - |v| Ok(v.into()), - ) - } - - pub fn __call__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { - slf + pub fn __getitem__(&self, vertex: VertexRef) -> PyResult> { + self.vertices + .get(vertex) + .ok_or_else(|| PyIndexError::new_err("Vertex does not exist")) } pub fn __repr__(&self) -> String { @@ -686,64 +825,34 @@ impl PyPathFromGraph { fn collect(&self) -> Vec> { self.__iter__().into_iter().map(|it| it.collect()).collect() } + #[getter] fn id(&self) -> NestedU64Iterable { let path = self.path.clone(); (move || path.id()).into() } + #[getter] fn name(&self) -> NestedStringIterable { let path = self.path.clone(); (move || path.name()).into() } + #[getter] fn earliest_time(&self) -> NestedOptionI64Iterable { let path = self.path.clone(); (move || path.earliest_time()).into() } + #[getter] fn latest_time(&self) -> NestedOptionI64Iterable { let path = self.path.clone(); (move || path.latest_time()).into() } - fn property(&self, name: String, include_static: Option) -> NestedOptionPropIterable { - let path = self.path.clone(); - (move || path.property(name.clone(), include_static.unwrap_or(true))).into() - } - - fn property_history(&self, name: String) -> NestedPropHistoryIterable { - let path = self.path.clone(); - (move || path.property_history(name.clone())).into() - } - - fn properties(&self, include_static: Option) -> NestedPropsIterable { - let path = self.path.clone(); - (move || path.properties(include_static.unwrap_or(true))).into() - } - - fn property_histories(&self) -> NestedPropHistoriesIterable { - let path = self.path.clone(); - (move || path.property_histories()).into() - } - - fn property_names(&self, include_static: Option) -> NestedStringVecIterable { - let path = self.path.clone(); - (move || path.property_names(include_static.unwrap_or(true))).into() - } - - fn has_property(&self, name: String, include_static: Option) -> NestedBoolIterable { + #[getter] + fn properties(&self) -> PyNestedPropsIterable { let path = self.path.clone(); - (move || path.has_property(name.clone(), include_static.unwrap_or(true))).into() - } - - fn has_static_property(&self, name: String) -> NestedBoolIterable { - let path = self.path.clone(); - (move || path.has_static_property(name.clone())).into() - } - - fn static_property(&self, name: String) -> NestedOptionPropIterable { - let path = self.path.clone(); - (move || path.static_property(name.clone())).into() + (move || path.properties()).into() } fn degree(&self) -> NestedUsizeIterable { @@ -761,58 +870,79 @@ impl PyPathFromGraph { (move || path.out_degree()).into() } + #[getter] fn edges(&self) -> PyNestedEdges { let clone = self.path.clone(); (move || clone.edges()).into() } + #[getter] fn in_edges(&self) -> PyNestedEdges { let clone = self.path.clone(); (move || clone.in_edges()).into() } + #[getter] fn out_edges(&self) -> PyNestedEdges { let clone = self.path.clone(); (move || clone.out_edges()).into() } + #[getter] fn out_neighbours(&self) -> Self { self.path.out_neighbours().into() } + #[getter] fn in_neighbours(&self) -> Self { self.path.in_neighbours().into() } + #[getter] fn neighbours(&self) -> Self { self.path.neighbours().into() } //****** Perspective APIS ******// + #[getter] pub fn start(&self) -> Option { self.path.start() } + #[getter] pub fn end(&self) -> Option { self.path.end() } #[doc = window_size_doc_string!()] + #[getter] pub fn window_size(&self) -> Option { self.path.window_size() } - fn expanding(&self, step: &PyAny) -> PyResult { - expanding_impl(&self.path, step) + fn expanding( + &self, + step: PyInterval, + ) -> Result>, ParseTimeError> { + self.path.expanding(step) } - fn rolling(&self, window: &PyAny, step: Option<&PyAny>) -> PyResult { - rolling_impl(&self.path, window, step) + fn rolling( + &self, + window: PyInterval, + step: Option, + ) -> Result>, ParseTimeError> { + self.path.rolling(window, step) } #[pyo3(signature = (t_start = None, t_end = None))] - pub fn window(&self, t_start: Option<&PyAny>, t_end: Option<&PyAny>) -> PyResult { - window_impl(&self.path, t_start, t_end).map(|p| p.into()) + pub fn window( + &self, + t_start: Option, + t_end: Option, + ) -> PathFromGraph> { + self.path + .window(t_start.unwrap_or(PyTime::MIN), t_end.unwrap_or(PyTime::MAX)) } /// Create a view of the vertex including all events at `t`. @@ -823,8 +953,8 @@ impl PyPathFromGraph { /// Returns: /// A `PyVertex` object. #[pyo3(signature = (end))] - pub fn at(&self, end: &PyAny) -> PyResult { - at_impl(&self.path, end).map(|p| p.into()) + pub fn at(&self, end: PyTime) -> PathFromGraph> { + self.path.at(end) } #[doc = default_layer_doc_string!()] @@ -832,10 +962,10 @@ impl PyPathFromGraph { self.path.default_layer().into() } - #[doc = layer_doc_string!()] + #[doc = layers_doc_string!()] #[pyo3(signature = (name))] - pub fn layer(&self, name: &str) -> Option { - Some(self.path.layer(name)?.into()) + pub fn layer(&self, name: &str) -> Option>> { + self.path.layer(name) } fn __repr__(&self) -> String { @@ -863,10 +993,9 @@ impl From> for PyPathFromGraph { } } -impl IntoPyObject for PathFromGraph { - fn into_py_object(self) -> PyObject { - let py_version: PyPathFromGraph = self.into(); - Python::with_gil(|py| py_version.into_py(py)) +impl IntoPy for PathFromGraph { + fn into_py(self, py: Python<'_>) -> PyObject { + PyPathFromGraph::from(self).into_py(py) } } @@ -887,10 +1016,9 @@ impl From> for PyPathFromVertex } } -impl IntoPyObject for PathFromVertex { - fn into_py_object(self) -> PyObject { - let py_version: PyPathFromVertex = self.into(); - Python::with_gil(|py| py_version.into_py(py)) +impl IntoPy for PathFromVertex { + fn into_py(self, py: Python<'_>) -> PyObject { + PyPathFromVertex::from(self).into_py(py) } } @@ -904,64 +1032,34 @@ impl PyPathFromVertex { self.__iter__().into_iter().collect() } + #[getter] fn id(&self) -> U64Iterable { let path = self.path.clone(); (move || path.id()).into() } + #[getter] fn name(&self) -> StringIterable { let path = self.path.clone(); (move || path.name()).into() } + #[getter] fn earliest_time(&self) -> OptionI64Iterable { let path = self.path.clone(); (move || path.earliest_time()).into() } + #[getter] fn latest_time(&self) -> OptionI64Iterable { let path = self.path.clone(); (move || path.latest_time()).into() } - fn property(&self, name: String, include_static: Option) -> OptionPropIterable { - let path = self.path.clone(); - (move || path.property(name.clone(), include_static.unwrap_or(true))).into() - } - - fn property_history(&self, name: String) -> PropHistoryIterable { + #[getter] + fn properties(&self) -> PyPropsList { let path = self.path.clone(); - (move || path.property_history(name.clone())).into() - } - - fn properties(&self, include_static: Option) -> PropsIterable { - let path = self.path.clone(); - (move || path.properties(include_static.unwrap_or(true))).into() - } - - fn property_histories(&self) -> PropHistoriesIterable { - let path = self.path.clone(); - (move || path.property_histories()).into() - } - - fn property_names(&self, include_static: Option) -> StringVecIterable { - let path = self.path.clone(); - (move || path.property_names(include_static.unwrap_or(true))).into() - } - - fn has_property(&self, name: String, include_static: Option) -> BoolIterable { - let path = self.path.clone(); - (move || path.has_property(name.clone(), include_static.unwrap_or(true))).into() - } - - fn has_static_property(&self, name: String) -> BoolIterable { - let path = self.path.clone(); - (move || path.has_static_property(name.clone())).into() - } - - fn static_property(&self, name: String) -> OptionPropIterable { - let path = self.path.clone(); - (move || path.static_property(name.clone())).into() + (move || path.properties()).into() } fn in_degree(&self) -> UsizeIterable { @@ -979,58 +1077,79 @@ impl PyPathFromVertex { (move || path.degree()).into() } + #[getter] fn edges(&self) -> PyEdges { let path = self.path.clone(); (move || path.edges()).into() } + #[getter] fn in_edges(&self) -> PyEdges { let path = self.path.clone(); (move || path.in_edges()).into() } + #[getter] fn out_edges(&self) -> PyEdges { let path = self.path.clone(); (move || path.out_edges()).into() } + #[getter] fn out_neighbours(&self) -> Self { self.path.out_neighbours().into() } + #[getter] fn in_neighbours(&self) -> Self { self.path.in_neighbours().into() } + #[getter] fn neighbours(&self) -> Self { self.path.neighbours().into() } //****** Perspective APIS ******// + #[getter] pub fn start(&self) -> Option { self.path.start() } + #[getter] pub fn end(&self) -> Option { self.path.end() } #[doc = window_size_doc_string!()] + #[getter] pub fn window_size(&self) -> Option { self.path.window_size() } - fn expanding(&self, step: &PyAny) -> PyResult { - expanding_impl(&self.path, step) + fn expanding( + &self, + step: PyInterval, + ) -> Result>, ParseTimeError> { + self.path.expanding(step) } - fn rolling(&self, window: &PyAny, step: Option<&PyAny>) -> PyResult { - rolling_impl(&self.path, window, step) + fn rolling( + &self, + window: PyInterval, + step: Option, + ) -> Result>, ParseTimeError> { + self.path.rolling(window, step) } #[pyo3(signature = (t_start = None, t_end = None))] - pub fn window(&self, t_start: Option<&PyAny>, t_end: Option<&PyAny>) -> PyResult { - window_impl(&self.path, t_start, t_end).map(|p| p.into()) + pub fn window( + &self, + t_start: Option, + t_end: Option, + ) -> PathFromVertex> { + self.path + .window(t_start.unwrap_or(PyTime::MIN), t_end.unwrap_or(PyTime::MAX)) } /// Create a view of the vertex including all events at `t`. @@ -1041,18 +1160,18 @@ impl PyPathFromVertex { /// Returns: /// A `PyVertex` object. #[pyo3(signature = (end))] - pub fn at(&self, end: &PyAny) -> PyResult { - at_impl(&self.path, end).map(|p| p.into()) + pub fn at(&self, end: PyTime) -> PathFromVertex> { + self.path.at(end) } pub fn default_layer(&self) -> Self { self.path.default_layer().into() } - #[doc = layer_doc_string!()] + #[doc = layers_doc_string!()] #[pyo3(signature = (name))] - pub fn layer(&self, name: &str) -> Option { - Some(self.path.layer(name)?.into()) + pub fn layer(&self, name: &str) -> Option>> { + self.path.layer(name) } fn __repr__(&self) -> String { @@ -1139,115 +1258,172 @@ impl PathIterator { } } -py_iterable!( - PyVertexIterable, - VertexView, - PyVertex, - PyVertexIterator -); +py_iterable!(PyVertexIterable, VertexView, PyVertex); #[pymethods] impl PyVertexIterable { + #[getter] fn id(&self) -> U64Iterable { let builder = self.builder.clone(); (move || builder().id()).into() } + #[getter] fn name(&self) -> StringIterable { let vertices = self.builder.clone(); (move || vertices().name()).into() } + #[getter] fn earliest_time(&self) -> OptionI64Iterable { let vertices = self.builder.clone(); (move || vertices().earliest_time()).into() } + #[getter] fn latest_time(&self) -> OptionI64Iterable { let vertices = self.builder.clone(); (move || vertices().latest_time()).into() } - fn property(&self, name: String, include_static: Option) -> OptionPropIterable { + #[getter] + fn properties(&self) -> PyPropsList { let vertices = self.builder.clone(); - (move || vertices().property(name.clone(), include_static.unwrap_or(true))).into() + (move || vertices().properties()).into() } - fn property_history(&self, name: String) -> PropHistoryIterable { + fn degree(&self) -> UsizeIterable { let vertices = self.builder.clone(); - (move || vertices().property_history(name.clone())).into() + (move || vertices().degree()).into() } - fn properties(&self, include_static: Option) -> PropsIterable { + fn in_degree(&self) -> UsizeIterable { let vertices = self.builder.clone(); - (move || vertices().properties(include_static.unwrap_or(true))).into() + (move || vertices().in_degree()).into() } - fn property_histories(&self) -> PropHistoriesIterable { + fn out_degree(&self) -> UsizeIterable { let vertices = self.builder.clone(); - (move || vertices().property_histories()).into() + (move || vertices().out_degree()).into() + } + + #[getter] + fn edges(&self) -> PyEdges { + let clone = self.builder.clone(); + (move || clone().edges()).into() + } + + #[getter] + fn in_edges(&self) -> PyEdges { + let clone = self.builder.clone(); + (move || clone().in_edges()).into() + } + + #[getter] + fn out_edges(&self) -> PyEdges { + let clone = self.builder.clone(); + (move || clone().out_edges()).into() + } + + #[getter] + fn out_neighbours(&self) -> Self { + let builder = self.builder.clone(); + (move || builder().out_neighbours()).into() } - fn property_names(&self, include_static: Option) -> StringVecIterable { + #[getter] + fn in_neighbours(&self) -> Self { + let builder = self.builder.clone(); + (move || builder().in_neighbours()).into() + } + + #[getter] + fn neighbours(&self) -> Self { + let builder = self.builder.clone(); + (move || builder().neighbours()).into() + } +} + +py_nested_iterable!(PyNestedVertexIterable, VertexView); + +#[pymethods] +impl PyNestedVertexIterable { + #[getter] + fn id(&self) -> NestedU64Iterable { + let builder = self.builder.clone(); + (move || builder().id()).into() + } + + #[getter] + fn name(&self) -> NestedStringIterable { let vertices = self.builder.clone(); - (move || vertices().property_names(include_static.unwrap_or(true))).into() + (move || vertices().name()).into() } - fn has_property(&self, name: String, include_static: Option) -> BoolIterable { + #[getter] + fn earliest_time(&self) -> NestedOptionI64Iterable { let vertices = self.builder.clone(); - (move || vertices().has_property(name.clone(), include_static.unwrap_or(true))).into() + (move || vertices().earliest_time()).into() } - fn has_static_property(&self, name: String) -> BoolIterable { + #[getter] + fn latest_time(&self) -> NestedOptionI64Iterable { let vertices = self.builder.clone(); - (move || vertices().has_static_property(name.clone())).into() + (move || vertices().latest_time()).into() } - fn static_property(&self, name: String) -> OptionPropIterable { + #[getter] + fn properties(&self) -> PyNestedPropsIterable { let vertices = self.builder.clone(); - (move || vertices().static_property(name.clone())).into() + (move || vertices().properties()).into() } - fn degree(&self) -> UsizeIterable { + fn degree(&self) -> NestedUsizeIterable { let vertices = self.builder.clone(); (move || vertices().degree()).into() } - fn in_degree(&self) -> UsizeIterable { + fn in_degree(&self) -> NestedUsizeIterable { let vertices = self.builder.clone(); (move || vertices().in_degree()).into() } - fn out_degree(&self) -> UsizeIterable { + fn out_degree(&self) -> NestedUsizeIterable { let vertices = self.builder.clone(); (move || vertices().out_degree()).into() } - fn edges(&self) -> PyEdges { + #[getter] + fn edges(&self) -> PyNestedEdges { let clone = self.builder.clone(); (move || clone().edges()).into() } - fn in_edges(&self) -> PyEdges { + #[getter] + fn in_edges(&self) -> PyNestedEdges { let clone = self.builder.clone(); (move || clone().in_edges()).into() } - fn out_edges(&self) -> PyEdges { + #[getter] + fn out_edges(&self) -> PyNestedEdges { let clone = self.builder.clone(); (move || clone().out_edges()).into() } + #[getter] fn out_neighbours(&self) -> Self { let builder = self.builder.clone(); (move || builder().out_neighbours()).into() } + #[getter] fn in_neighbours(&self) -> Self { let builder = self.builder.clone(); (move || builder().in_neighbours()).into() } + #[getter] fn neighbours(&self) -> Self { let builder = self.builder.clone(); (move || builder().neighbours()).into() diff --git a/raphtory/src/python/graph/views/graph_view.rs b/raphtory/src/python/graph/views/graph_view.rs new file mode 100644 index 0000000000..747fe3ce2f --- /dev/null +++ b/raphtory/src/python/graph/views/graph_view.rs @@ -0,0 +1,444 @@ +//! The API for querying a view of the graph in a read-only state + +use crate::{ + core::{ + entities::vertices::vertex_ref::VertexRef, + utils::{errors::GraphError, time::error::ParseTimeError}, + ArcStr, + }, + db::{ + api::{ + properties::Properties, + view::{ + internal::{DynamicGraph, IntoDynamic, MaterializedGraph}, + LayerOps, WindowSet, + }, + }, + graph::{ + edge::EdgeView, + vertex::VertexView, + views::{ + layer_graph::LayeredGraph, vertex_subgraph::VertexSubgraph, + window_graph::WindowedGraph, + }, + }, + }, + prelude::*, + python::{ + graph::{edge::PyEdges, vertex::PyVertices}, + types::repr::Repr, + utils::{PyInterval, PyTime}, + }, + *, +}; +use chrono::prelude::*; +use itertools::Itertools; +use pyo3::{prelude::*, types::PyBytes}; +use std::ops::Deref; + +impl IntoPy for MaterializedGraph { + fn into_py(self, py: Python<'_>) -> PyObject { + match self { + MaterializedGraph::EventGraph(g) => g.into_py(py), + MaterializedGraph::PersistentGraph(g) => g.into_py(py), + } + } +} + +impl IntoPy for DynamicGraph { + fn into_py(self, py: Python<'_>) -> PyObject { + PyGraphView::from(self).into_py(py) + } +} + +impl<'source> FromPyObject<'source> for DynamicGraph { + fn extract(ob: &'source PyAny) -> PyResult { + ob.extract::>() + .map(|g| g.graph.clone()) + .or_else(|err| { + let res = ob.call_method0("bincode").map_err(|_| err)?; // return original error as probably more helpful + // assume we have a graph at this point, the res probably should not fail + let b = res.extract::<&[u8]>()?; + let g = MaterializedGraph::from_bincode(b)?; + Ok(g.into_dynamic()) + }) + } +} +/// Graph view is a read-only version of a graph at a certain point in time. + +#[pyclass(name = "GraphView", frozen, subclass)] +#[repr(C)] +pub struct PyGraphView { + pub graph: DynamicGraph, +} + +/// Graph view is a read-only version of a graph at a certain point in time. +impl From for PyGraphView { + fn from(value: G) -> Self { + PyGraphView { + graph: value.into_dynamic(), + } + } +} + +impl IntoPy for WindowedGraph { + fn into_py(self, py: Python<'_>) -> PyObject { + PyGraphView::from(self).into_py(py) + } +} + +impl IntoPy for LayeredGraph { + fn into_py(self, py: Python<'_>) -> PyObject { + PyGraphView::from(self).into_py(py) + } +} + +impl IntoPy for VertexSubgraph { + fn into_py(self, py: Python<'_>) -> PyObject { + PyGraphView::from(self).into_py(py) + } +} + +/// The API for querying a view of the graph in a read-only state +#[pymethods] +impl PyGraphView { + /// Return all the layer ids in the graph + #[getter] + pub fn unique_layers(&self) -> Vec { + self.graph.unique_layers().collect() + } + + //****** Metrics APIs ******// + + /// Timestamp of earliest activity in the graph + /// + /// Returns: + /// the timestamp of the earliest activity in the graph + #[getter] + pub fn earliest_time(&self) -> Option { + self.graph.earliest_time() + } + + /// DateTime of earliest activity in the graph + /// + /// Returns: + /// the datetime of the earliest activity in the graph + #[getter] + pub fn earliest_date_time(&self) -> Option { + let earliest_time = self.graph.earliest_time()?; + NaiveDateTime::from_timestamp_millis(earliest_time) + } + + /// Timestamp of latest activity in the graph + /// + /// Returns: + /// the timestamp of the latest activity in the graph + #[getter] + pub fn latest_time(&self) -> Option { + self.graph.latest_time() + } + + /// DateTime of latest activity in the graph + /// + /// Returns: + /// the datetime of the latest activity in the graph + #[getter] + pub fn latest_date_time(&self) -> Option { + let latest_time = self.graph.latest_time()?; + NaiveDateTime::from_timestamp_millis(latest_time) + } + + /// Number of edges in the graph + /// + /// Returns: + /// the number of edges in the graph + pub fn count_edges(&self) -> usize { + self.graph.count_edges() + } + + /// Number of edges in the graph + /// + /// Returns: + /// the number of temporal edges in the graph + pub fn count_temporal_edges(&self) -> usize { + self.graph.count_temporal_edges() + } + + /// Number of vertices in the graph + /// + /// Returns: + /// the number of vertices in the graph + pub fn count_vertices(&self) -> usize { + self.graph.count_vertices() + } + + /// Returns true if the graph contains the specified vertex + /// + /// Arguments: + /// id (str or int): the vertex id + /// + /// Returns: + /// true if the graph contains the specified vertex, false otherwise + pub fn has_vertex(&self, id: VertexRef) -> bool { + self.graph.has_vertex(id) + } + + /// Returns true if the graph contains the specified edge + /// + /// Arguments: + /// src (str or int): the source vertex id + /// dst (str or int): the destination vertex id + /// layer (str): the edge layer (optional) + /// + /// Returns: + /// true if the graph contains the specified edge, false otherwise + #[pyo3(signature = (src, dst, layer=None))] + pub fn has_edge(&self, src: VertexRef, dst: VertexRef, layer: Option<&str>) -> bool { + self.graph.has_edge(src, dst, layer) + } + + //****** Getter APIs ******// + + /// Gets the vertex with the specified id + /// + /// Arguments: + /// id (str or int): the vertex id + /// + /// Returns: + /// the vertex with the specified id, or None if the vertex does not exist + pub fn vertex(&self, id: VertexRef) -> Option> { + self.graph.vertex(id) + } + + /// Gets the vertices in the graph + /// + /// Returns: + /// the vertices in the graph + #[getter] + pub fn vertices(&self) -> PyVertices { + self.graph.vertices().into() + } + + /// Gets the edge with the specified source and destination vertices + /// + /// Arguments: + /// src (str or int): the source vertex id + /// dst (str or int): the destination vertex id + /// layer (str): the edge layer (optional) + /// + /// Returns: + /// the edge with the specified source and destination vertices, or None if the edge does not exist + #[pyo3(signature = (src, dst))] + pub fn edge(&self, src: VertexRef, dst: VertexRef) -> Option> { + self.graph.edge(src, dst) + } + + /// Gets all edges in the graph + /// + /// Returns: + /// the edges in the graph + pub fn edges(&self) -> PyEdges { + let clone = self.graph.clone(); + (move || clone.edges()).into() + } + + //****** Perspective APIS ******// + + /// Returns the default start time for perspectives over the view + /// + /// Returns: + /// the default start time for perspectives over the view + #[getter] + pub fn start(&self) -> Option { + self.graph.start() + } + + /// Returns the default start datetime for perspectives over the view + /// + /// Returns: + /// the default start datetime for perspectives over the view + #[getter] + pub fn start_date_time(&self) -> Option { + let start_time = self.graph.start()?; + NaiveDateTime::from_timestamp_millis(start_time) + } + + /// Returns the default end time for perspectives over the view + /// + /// Returns: + /// the default end time for perspectives over the view + #[getter] + pub fn end(&self) -> Option { + self.graph.end() + } + + #[doc = window_size_doc_string!()] + pub fn window_size(&self) -> Option { + self.graph.window_size() + } + + /// Returns the default end datetime for perspectives over the view + /// + /// Returns: + /// the default end datetime for perspectives over the view + #[getter] + pub fn end_date_time(&self) -> Option { + let end_time = self.graph.end()?; + NaiveDateTime::from_timestamp_millis(end_time) + } + + /// Creates a `WindowSet` with the given `step` size and optional `start` and `end` times, + /// using an expanding window. + /// + /// An expanding window is a window that grows by `step` size at each iteration. + /// + /// Arguments: + /// step (int) : the size of the window + /// start (int): the start time of the window (optional) + /// end (int): the end time of the window (optional) + /// + /// Returns: + /// A `WindowSet` with the given `step` size and optional `start` and `end` times, + #[pyo3(signature = (step))] + fn expanding(&self, step: PyInterval) -> Result, ParseTimeError> { + self.graph.expanding(step) + } + + /// Creates a `WindowSet` with the given `window` size and optional `step`, `start` and `end` times, + /// using a rolling window. + /// + /// A rolling window is a window that moves forward by `step` size at each iteration. + /// + /// Arguments: + /// window (int): the size of the window + /// step (int): the size of the step (optional) + /// start (int): the start time of the window (optional) + /// end: the end time of the window (optional) + /// + /// Returns: + /// a `WindowSet` with the given `window` size and optional `step`, `start` and `end` times, + fn rolling( + &self, + window: PyInterval, + step: Option, + ) -> Result, ParseTimeError> { + self.graph.rolling(window, step) + } + + /// Create a view including all events between `start` (inclusive) and `end` (exclusive) + /// + /// Arguments: + /// start (int): the start time of the window (optional) + /// end (int): the end time of the window (optional) + /// + /// Returns: + /// a view including all events between `start` (inclusive) and `end` (exclusive) + #[pyo3(signature = (start=None, end=None))] + pub fn window( + &self, + start: Option, + end: Option, + ) -> WindowedGraph { + self.graph + .window(start.unwrap_or(PyTime::MIN), end.unwrap_or(PyTime::MAX)) + } + + /// Create a view including all events until `end` (inclusive) + /// + /// Arguments: + /// end (int) : the end time of the window + /// + /// Returns: + /// a view including all events until `end` (inclusive) + #[pyo3(signature = (end))] + pub fn at(&self, end: PyTime) -> WindowedGraph { + self.graph.at(end) + } + + #[doc = default_layer_doc_string!()] + pub fn default_layer(&self) -> LayeredGraph { + self.graph.default_layer() + } + + #[doc = layers_doc_string!()] + #[pyo3(signature = (names))] + pub fn layers(&self, names: Vec) -> Option> { + self.graph.layer(names) + } + + #[doc = layers_doc_string!()] + #[pyo3(signature = (name))] + pub fn layer(&self, name: String) -> Option> { + self.graph.layer(name) + } + + /// Get all graph properties + /// + /// + /// Returns: + /// HashMap - Properties paired with their names + #[getter] + fn properties(&self) -> Properties { + self.graph.properties() + } + + /// Returns a subgraph given a set of vertices + /// + /// Arguments: + /// * `vertices`: set of vertices + /// + /// Returns: + /// GraphView - Returns the subgraph + fn subgraph(&self, vertices: Vec) -> VertexSubgraph { + self.graph.subgraph(vertices) + } + + /// Returns a graph clone + /// + /// Arguments: + /// + /// Returns: + /// GraphView - Returns a graph clone + fn materialize(&self) -> Result { + self.graph.materialize() + } + + /// Get bincode encoded graph + pub fn bincode<'py>(&'py self, py: Python<'py>) -> Result<&'py PyBytes, GraphError> { + let bytes = self.graph.materialize()?.bincode()?; + Ok(PyBytes::new(py, &bytes)) + } + + /// Displays the graph + pub fn __repr__(&self) -> String { + self.repr() + } +} + +impl Repr for PyGraphView { + fn repr(&self) -> String { + let num_edges = self.graph.count_edges(); + let num_vertices = self.graph.count_vertices(); + let num_temporal_edges: usize = self.graph.count_temporal_edges(); + let earliest_time = self.graph.earliest_time().repr(); + let latest_time = self.graph.latest_time().repr(); + let properties: String = self + .graph + .properties() + .iter() + .map(|(k, v)| format!("{}: {}", k.deref(), v)) + .join(", "); + if properties.is_empty() { + return format!( + "Graph(number_of_edges={:?}, number_of_vertices={:?}, number_of_temporal_edges={:?}, earliest_time={:?}, latest_time={:?})", + num_edges, num_vertices, num_temporal_edges, earliest_time, latest_time + ); + } else { + let property_string: String = format!("{{{properties}}}"); + return format!( + "Graph(number_of_edges={:?}, number_of_vertices={:?}, number_of_temporal_edges={:?}, earliest_time={:?}, latest_time={:?}, properties={})", + num_edges, num_vertices, num_temporal_edges, earliest_time, latest_time, property_string + ); + } + } +} diff --git a/raphtory/src/python/graph/views/mod.rs b/raphtory/src/python/graph/views/mod.rs new file mode 100644 index 0000000000..c0068a9452 --- /dev/null +++ b/raphtory/src/python/graph/views/mod.rs @@ -0,0 +1 @@ +pub mod graph_view; diff --git a/raphtory/src/python/mod.rs b/raphtory/src/python/mod.rs new file mode 100644 index 0000000000..f8ff9f5c5f --- /dev/null +++ b/raphtory/src/python/mod.rs @@ -0,0 +1,7 @@ +extern crate core; + +#[macro_use] +pub mod types; +pub mod graph; +pub mod packages; +pub mod utils; diff --git a/raphtory/src/python/packages/algorithms.rs b/raphtory/src/python/packages/algorithms.rs new file mode 100644 index 0000000000..77a8c253ca --- /dev/null +++ b/raphtory/src/python/packages/algorithms.rs @@ -0,0 +1,413 @@ +use std::collections::HashMap; + +use crate::python::graph::edge::PyDirection; +/// Implementations of various graph algorithms that can be run on a graph. +/// +/// To run an algorithm simply import the module and call the function with the graph as the argument +/// +use crate::{ + algorithms::{ + algorithm_result::AlgorithmResult, + balance::balance as balance_rs, + connected_components, + degree::{ + average_degree as average_degree_rs, max_in_degree as max_in_degree_rs, + max_out_degree as max_out_degree_rs, min_in_degree as min_in_degree_rs, + min_out_degree as min_out_degree_rs, + }, + directed_graph_density::directed_graph_density as directed_graph_density_rs, + hits::hits as hits_rs, + local_clustering_coefficient::local_clustering_coefficient as local_clustering_coefficient_rs, + local_triangle_count::local_triangle_count as local_triangle_count_rs, + motifs::three_node_temporal_motifs::{ + global_temporal_three_node_motif as global_temporal_three_node_motif_rs, + global_temporal_three_node_motif_general as global_temporal_three_node_motif_general_rs, + temporal_three_node_motif as local_three_node_rs, + }, + pagerank::unweighted_page_rank, + reciprocity::{ + all_local_reciprocity as all_local_reciprocity_rs, + global_reciprocity as global_reciprocity_rs, + }, + temporal_reachability::temporally_reachable_nodes as temporal_reachability_rs, + }, + core::entities::vertices::vertex_ref::VertexRef, + python::{graph::views::graph_view::PyGraphView, utils::PyInputVertex}, +}; +use ordered_float::OrderedFloat; +use pyo3::prelude::*; + +/// Local triangle count - calculates the number of triangles (a cycle of length 3) a vertex participates in. +/// +/// This function returns the number of pairs of neighbours of a given node which are themselves connected. +/// +/// Arguments: +/// g (Raphtory graph) : Raphtory graph, this can be directed or undirected but will be treated as undirected +/// v (int or str) : vertex id or name +/// +/// Returns: +/// triangles(int) : number of triangles associated with vertex v +/// +#[pyfunction] +pub fn local_triangle_count(g: &PyGraphView, v: VertexRef) -> Option { + local_triangle_count_rs(&g.graph, v) +} + +/// Weakly connected components -- partitions the graph into node sets which are mutually reachable by an undirected path +/// +/// This function assigns a component id to each vertex such that vertices with the same component id are mutually reachable +/// by an undirected path. +/// +/// Arguments: +/// g (Raphtory graph) : Raphtory graph +/// iter_count (int) : Maximum number of iterations to run. Note that this will terminate early if the labels converge prior to the number of iterations being reached. +/// +/// Returns: +/// AlgorithmResult : AlgorithmResult object with string keys and integer values mapping vertex names to their component ids. +#[pyfunction] +#[pyo3(signature = (g, iter_count=9223372036854775807))] +pub fn weakly_connected_components( + g: &PyGraphView, + iter_count: usize, +) -> AlgorithmResult { + connected_components::weakly_connected_components(&g.graph, iter_count, None) +} + +/// Pagerank -- pagerank centrality value of the vertices in a graph +/// +/// This function calculates the Pagerank value of each vertex in a graph. See https://en.wikipedia.org/wiki/PageRank for more information on PageRank centrality. +/// A default damping factor of 0.85 is used. This is an iterative algorithm which terminates if the sum of the absolute difference in pagerank values between iterations +/// is less than the max diff value given. +/// +/// Arguments: +/// g (Raphtory graph) : Raphtory graph +/// iter_count (int) : Maximum number of iterations to run. Note that this will terminate early if convergence is reached. +/// max_diff (float) : Optional parameter providing an alternative stopping condition. The algorithm will terminate if the sum of the absolute difference in pagerank values between iterations +/// is less than the max diff value given. +/// +/// Returns: +/// AlgorithmResult : AlgorithmResult with string keys and float values mapping vertex names to their pagerank value. +#[pyfunction] +#[pyo3(signature = (g, iter_count=20, max_diff=None))] +pub fn pagerank( + g: &PyGraphView, + iter_count: usize, + max_diff: Option, +) -> AlgorithmResult> { + unweighted_page_rank(&g.graph, iter_count, None, max_diff, true) +} + +/// Temporally reachable nodes -- the nodes that are reachable by a time respecting path followed out from a set of seed nodes at a starting time. +/// +/// This function starts at a set of seed nodes and follows all time respecting paths until either a) a maximum number of hops is reached, b) one of a set of +/// stop nodes is reached, or c) no further time respecting edges exist. A time respecting path is a sequence of nodes v_1, v_2, ... , v_k such that there exists +/// a sequence of edges (v_i, v_i+1, t_i) with t_i < t_i+1 for i = 1, ... , k - 1. +/// +/// Arguments: +/// g (Raphtory graph) : directed Raphtory graph +/// max_hops (int) : maximum number of hops to propagate out +/// start_time (int) : time at which to start the path (such that t_1 > start_time for any path starting from these seed nodes) +/// seed_nodes (list(str) or list(int)) : list of vertex names or ids which should be the starting nodes +/// stop_nodes (list(str) or list(int)) : nodes at which a path shouldn't go any further +/// +/// Returns: +/// AlgorithmResult : AlgorithmResult with string keys and float values mapping vertex names to their pagerank value. +#[pyfunction] +pub fn temporally_reachable_nodes( + g: &PyGraphView, + max_hops: usize, + start_time: i64, + seed_nodes: Vec, + stop_nodes: Option>, +) -> AlgorithmResult> { + temporal_reachability_rs(&g.graph, None, max_hops, start_time, seed_nodes, stop_nodes) +} + +/// Local clustering coefficient - measures the degree to which nodes in a graph tend to cluster together. +/// +/// The proportion of pairs of neighbours of a node who are themselves connected. +/// +/// Arguments: +/// g (Raphtory graph) : Raphtory graph, can be directed or undirected but will be treated as undirected. +/// v (int or str): vertex id or name +/// +/// Returns: +/// float : the local clustering coefficient of vertex v in g. +#[pyfunction] +pub fn local_clustering_coefficient(g: &PyGraphView, v: VertexRef) -> Option { + local_clustering_coefficient_rs(&g.graph, v) +} + +/// Graph density - measures how dense or sparse a graph is. +/// +/// The ratio of the number of directed edges in the graph to the total number of possible directed +/// edges (given by N * (N-1) where N is the number of nodes). +/// +/// Arguments: +/// g (Raphtory graph) : a directed Raphtory graph +/// +/// Returns: +/// float : Directed graph density of G. +#[pyfunction] +pub fn directed_graph_density(g: &PyGraphView) -> f32 { + directed_graph_density_rs(&g.graph) +} + +/// The average (undirected) degree of all vertices in the graph. +/// +/// Note that this treats the graph as simple and undirected and is equal to twice +/// the number of undirected edges divided by the number of nodes. +/// +/// Arguments: +/// g (Raphtory graph) : a Raphtory graph +/// +/// Returns: +/// float : the average degree of the nodes in the graph +#[pyfunction] +pub fn average_degree(g: &PyGraphView) -> f64 { + average_degree_rs(&g.graph) +} + +/// The maximum out degree of any vertex in the graph. +/// +/// Arguments: +/// g (Raphtory graph) : a directed Raphtory graph +/// +/// Returns: +/// int : value of the largest outdegree +#[pyfunction] +pub fn max_out_degree(g: &PyGraphView) -> usize { + max_out_degree_rs(&g.graph) +} + +/// The maximum in degree of any vertex in the graph. +/// +/// Arguments: +/// g (Raphtory graph) : a directed Raphtory graph +/// +/// Returns: +/// int : value of the largest indegree +#[pyfunction] +pub fn max_in_degree(g: &PyGraphView) -> usize { + max_in_degree_rs(&g.graph) +} + +/// The minimum out degree of any vertex in the graph. +/// +/// Arguments: +/// g (Raphtory graph) : a directed Raphtory graph +/// +/// Returns: +/// int : value of the smallest outdegree +#[pyfunction] +pub fn min_out_degree(g: &PyGraphView) -> usize { + min_out_degree_rs(&g.graph) +} + +/// The minimum in degree of any vertex in the graph. +/// +/// Arguments: +/// g (Raphtory graph) : a directed Raphtory graph +/// +/// Returns: +/// int : value of the smallest indegree +#[pyfunction] +pub fn min_in_degree(g: &PyGraphView) -> usize { + min_in_degree_rs(&g.graph) +} + +/// Reciprocity - measure of the symmetry of relationships in a graph, the global reciprocity of +/// the entire graph. +/// This calculates the number of reciprocal connections (edges that go in both directions) in a +/// graph and normalizes it by the total number of directed edges. +/// +/// Arguments: +/// g (Raphtory graph) : a directed Raphtory graph +/// +/// Returns: +/// float : reciprocity of the graph between 0 and 1. + +#[pyfunction] +pub fn global_reciprocity(g: &PyGraphView) -> f64 { + global_reciprocity_rs(&g.graph, None) +} + +/// Local reciprocity - measure of the symmetry of relationships associated with a vertex +/// +/// This measures the proportion of a vertex's outgoing edges which are reciprocated with an incoming edge. +/// +/// Arguments: +/// g (Raphtory graph) : a directed Raphtory graph +/// +/// Returns: +/// AlgorithmResult : AlgorithmResult with string keys and float values mapping each vertex name to its reciprocity value. +/// +#[pyfunction] +pub fn all_local_reciprocity(g: &PyGraphView) -> AlgorithmResult> { + all_local_reciprocity_rs(&g.graph, None) +} + +/// Computes the number of connected triplets within a graph +/// +/// A connected triplet (also known as a wedge, 2-hop path) is a pair of edges with one node in common. For example, the triangle made up of edges +/// A-B, B-C, C-A is formed of three connected triplets. +/// +/// Arguments: +/// g (Raphtory graph) : a Raphtory graph, treated as undirected +/// +/// Returns: +/// int : the number of triplets in the graph +#[pyfunction] +pub fn triplet_count(g: &PyGraphView) -> usize { + crate::algorithms::triplet_count::triplet_count(&g.graph, None) +} + +/// Computes the global clustering coefficient of a graph. The global clustering coefficient is +/// defined as the number of triangles in the graph divided by the number of triplets in the graph. +/// +/// Note that this is also known as transitivity and is different to the average clustering coefficient. +/// +/// Arguments: +/// g (Raphtory graph) : a Raphtory graph, treated as undirected +/// +/// Returns: +/// float : the global clustering coefficient of the graph +/// +/// See also: +/// [`Triplet Count`](triplet_count) +#[pyfunction] +pub fn global_clustering_coefficient(g: &PyGraphView) -> f64 { + crate::algorithms::clustering_coefficient::clustering_coefficient(&g.graph) +} + +/// Computes the number of three edge, up-to-three node delta-temporal motifs in the graph, using the algorithm of Paranjape et al, Motifs in Temporal Networks (2017). +/// We point the reader to this reference for more information on the algorithm and background, but provide a short summary below. +/// +/// Motifs included: +/// +/// Stars +/// +/// There are three classes (in the order they are outputted) of star motif on three nodes based on the switching behaviour of the edges between the two leaf nodes. +/// +/// - PRE: Stars of the form i<->j, i<->j, i<->k (ie two interactions with leaf j followed by one with leaf k) +/// - MID: Stars of the form i<->j, i<->k, i<->j (ie switching interactions from leaf j to leaf k, back to j again) +/// - POST: Stars of the form i<->j, i<->k, i<->k (ie one interaction with leaf j followed by two with leaf k) +/// +/// Within each of these classes is 8 motifs depending on the direction of the first to the last edge -- incoming "I" or outgoing "O". +/// These are enumerated in the order III, IIO, IOI, IOO, OII, OIO, OOI, OOO (like binary with "I"-0 and "O"-1). +/// +/// Two node motifs: +/// +/// Also included are two node motifs, of which there are 8 when counted from the perspective of each vertex. These are characterised by the direction of each edge, enumerated +/// in the above order. Note that for the global graph counts, each motif is counted in both directions (a single III motif for one vertex is an OOO motif for the other vertex). +/// +/// Triangles: +/// +/// There are 8 triangle motifs: +/// +/// 1. i --> j, k --> j, i --> k +/// 2. i --> j, k --> i, j --> k +/// 3. i --> j, j --> k, i --> k +/// 4. i --> j, i --> k, j --> k +/// 5. i --> j, k --> j, k --> i +/// 6. i --> j, k --> i, k --> j +/// 7. i --> j, j --> k, k --> i +/// 8. i --> j, i --> k, k --> j +/// +/// Arguments: +/// g (raphtory graph) : A directed raphtory graph +/// delta (int) - Maximum time difference between the first and last edge of the +/// motif. NB if time for edges was given as a UNIX epoch, this should be given in seconds, otherwise +/// milliseconds should be used (if edge times were given as string) +/// +/// Returns: +/// list : A 40 dimensional array with the counts of each motif, given in the same order as described above. Note that the two-node motif counts are symmetrical so it may be more useful just to consider the first four elements. +/// +/// Notes: +/// This is achieved by calling the local motif counting algorithm, summing the resulting arrays and dealing with overcounted motifs: the triangles (by dividing each motif count by three) and two-node motifs (dividing by two). +/// +#[pyfunction] +pub fn global_temporal_three_node_motif(g: &PyGraphView, delta: i64) -> Vec { + global_temporal_three_node_motif_rs(&g.graph, delta, None) +} + +#[pyfunction] +pub fn global_temporal_three_node_motif_multi( + g: &PyGraphView, + deltas: Vec, +) -> Vec> { + global_temporal_three_node_motif_general_rs(&g.graph, deltas, None) +} + +/// Computes the number of each type of motif that each node participates in. See global_temporal_three_node_motifs for a summary of the motifs involved. +/// +/// Arguments: +/// g (raphtory graph) : A directed raphtory graph +/// delta (int) - Maximum time difference between the first and last edge of the +/// motif. NB if time for edges was given as a UNIX epoch, this should be given in seconds, otherwise +/// milliseconds should be used (if edge times were given as string) +/// +/// Returns: +/// AlgorithmResult : An AlgorithmResult with node ids as keys and a 40d array of motif counts (in the same order as the global motif counts) with the number of each +/// motif that node participates in. +/// +/// Notes: +/// For this local count, a node is counted as participating in a motif in the following way. For star motifs, only the centre node counts +/// the motif. For two node motifs, both constituent nodes count the motif. For triangles, all three constituent nodes count the motif. +#[pyfunction] +pub fn local_temporal_three_node_motifs( + g: &PyGraphView, + delta: i64, +) -> HashMap> { + local_three_node_rs(&g.graph, vec![delta], None) + .into_iter() + .map(|(k, v)| (String::from(k), v[0].clone())) + .collect::>>() +} + +/// HITS (Hubs and Authority) Algorithm: +/// AuthScore of a vertex (A) = Sum of HubScore of all vertices pointing at vertex (A) from previous iteration / +/// Sum of HubScore of all vertices in the current iteration +/// +/// HubScore of a vertex (A) = Sum of AuthScore of all vertices pointing away from vertex (A) from previous iteration / +/// Sum of AuthScore of all vertices in the current iteration +/// +/// Returns +/// +/// * An AlgorithmResult object containing the mapping from vertex ID to the hub and authority score of the vertex +#[pyfunction] +#[pyo3(signature = (g, iter_count=20, threads=None))] +pub fn hits( + g: &PyGraphView, + iter_count: usize, + threads: Option, +) -> AlgorithmResult, OrderedFloat)> { + hits_rs(&g.graph, iter_count, threads) +} + +/// Sums the weights of edges in the graph based on the specified direction. +/// +/// This function computes the sum of edge weights based on the direction provided, and can be executed in parallel using a given number of threads. +/// +/// # Parameters +/// * `g` (`&PyGraphView`): The graph view on which the operation is to be performed. +/// * `name` (`String`, default = "weight"): The name of the edge property used as the weight. Defaults to "weight" if not provided. +/// * `direction` (`PyDirection`, default = `PyDirection::new("BOTH")`): Specifies the direction of the edges to be considered for summation. +/// - `PyDirection::new("OUT")`: Only consider outgoing edges. +/// - `PyDirection::new("IN")`: Only consider incoming edges. +/// - `PyDirection::new("BOTH")`: Consider both outgoing and incoming edges. This is the default. +/// * `threads` (`Option`, default = `None`): The number of threads to be used for parallel execution. Defaults to single-threaded operation if not provided. +/// +/// # Returns +/// `AlgorithmResult>`: A result containing a mapping of vertex names to the computed sum of their associated edge weights. +/// +#[pyfunction] +#[pyo3[signature = (g, name="weight".to_string(), direction=PyDirection::new("BOTH"), threads=None)]] +pub fn balance( + g: &PyGraphView, + name: String, + direction: PyDirection, + threads: Option, +) -> AlgorithmResult> { + balance_rs(&g.graph, name.clone(), direction.into(), threads) +} diff --git a/py-raphtory/src/graph_gen.rs b/raphtory/src/python/packages/graph_gen.rs similarity index 91% rename from py-raphtory/src/graph_gen.rs rename to raphtory/src/python/packages/graph_gen.rs index d96ba28f74..cb38b93ed6 100644 --- a/py-raphtory/src/graph_gen.rs +++ b/raphtory/src/python/packages/graph_gen.rs @@ -1,11 +1,14 @@ //! Provides functionality for generating graphs for testing and benchmarking. //! Allows us to generate graphs using the preferential attachment model and //! the random attachment model. - -use crate::graph::PyGraph; +use crate::{ + graphgen::{ + preferential_attachment::ba_preferential_attachment as pa, + random_attachment::random_attachment as ra, + }, + python::graph::graph::PyGraph, +}; use pyo3::prelude::*; -use raphtory::graphgen::preferential_attachment::ba_preferential_attachment as pa; -use raphtory::graphgen::random_attachment::random_attachment as ra; /// Generates a graph using the random attachment model /// diff --git a/py-raphtory/src/graph_loader.rs b/raphtory/src/python/packages/graph_loader.rs similarity index 77% rename from py-raphtory/src/graph_loader.rs rename to raphtory/src/python/packages/graph_loader.rs index e1083852f2..5132338341 100644 --- a/py-raphtory/src/graph_loader.rs +++ b/raphtory/src/python/packages/graph_loader.rs @@ -1,6 +1,6 @@ //! `GraphLoader` provides some default implementations for loading a pre-built graph. //! This base class is used to load in-built graphs such as the LOTR, reddit and StackOverflow. -use crate::graph::PyGraph; +use crate::python::graph::graph::PyGraph; use pyo3::prelude::*; use tokio::runtime::Runtime; @@ -24,11 +24,8 @@ use tokio::runtime::Runtime; /// Returns: /// A Graph containing the LOTR dataset #[pyfunction] -#[pyo3(signature = (shards=1))] -pub fn lotr_graph(shards: usize) -> PyResult> { - PyGraph::py_from_db_graph(raphtory_io::graph_loader::example::lotr_graph::lotr_graph( - shards, - )) +pub fn lotr_graph() -> PyResult> { + PyGraph::py_from_db_graph(crate::graph_loader::example::lotr_graph::lotr_graph()) } /// Load (a subset of) Reddit hyperlinks dataset into a graph. @@ -67,37 +64,35 @@ pub fn lotr_graph(shards: usize) -> PyResult> { /// Returns: /// A Graph containing the Reddit hyperlinks dataset #[pyfunction] -#[pyo3(signature = (shards=1,timeout_seconds=600))] -pub fn reddit_hyperlink_graph(shards: usize, timeout_seconds: u64) -> PyResult> { +#[pyo3(signature = (timeout_seconds=600))] +pub fn reddit_hyperlink_graph(timeout_seconds: u64) -> PyResult> { PyGraph::py_from_db_graph( - raphtory_io::graph_loader::example::reddit_hyperlinks::reddit_graph( - shards, - timeout_seconds, - false, - ), + crate::graph_loader::example::reddit_hyperlinks::reddit_graph(timeout_seconds, false), ) } #[pyfunction] -#[pyo3(signature = (path=None,subset=None,shards=1))] -pub fn stable_coin_graph(path: Option, subset:Option, shards: usize) -> PyResult> { +#[pyo3(signature = (path=None,subset=None))] +pub fn stable_coin_graph(path: Option, subset: Option) -> PyResult> { PyGraph::py_from_db_graph( - raphtory_io::graph_loader::example::stable_coins::stable_coin_graph(path, subset.unwrap_or(false),shards), + crate::graph_loader::example::stable_coins::stable_coin_graph( + path, + subset.unwrap_or(false), + ), ) } #[pyfunction] -#[pyo3(signature = (uri,username,password,database="neo4j".to_string(),shards=1))] +#[pyo3(signature = (uri,username,password,database="neo4j".to_string()))] pub fn neo4j_movie_graph( uri: String, username: String, password: String, database: String, - shards: usize, ) -> PyResult> { let g = Runtime::new().unwrap().block_on( - raphtory_io::graph_loader::example::neo4j_examples::neo4j_movie_graph( - uri, username, password, database, shards, + crate::graph_loader::example::neo4j_examples::neo4j_movie_graph( + uri, username, password, database, ), ); PyGraph::py_from_db_graph(g) diff --git a/raphtory/src/python/packages/mod.rs b/raphtory/src/python/packages/mod.rs new file mode 100644 index 0000000000..a2a2988bb2 --- /dev/null +++ b/raphtory/src/python/packages/mod.rs @@ -0,0 +1,4 @@ +pub mod algorithms; +pub mod graph_gen; +pub mod graph_loader; +pub mod vectors; diff --git a/raphtory/src/python/packages/vectors.rs b/raphtory/src/python/packages/vectors.rs new file mode 100644 index 0000000000..2b82a8b69c --- /dev/null +++ b/raphtory/src/python/packages/vectors.rs @@ -0,0 +1,121 @@ +use crate::{ + db::{ + api::view::internal::DynamicGraph, + graph::{edge::EdgeView, vertex::VertexView}, + }, + prelude::{EdgeViewOps, VertexViewOps}, + python::graph::views::graph_view::PyGraphView, + vectors::{Embedding, EmbeddingFunction, Vectorizable, VectorizedGraph}, +}; +use futures_util::future::BoxFuture; +use itertools::Itertools; +use pyo3::{ + prelude::*, + types::{PyFunction, PyList}, +}; +use std::{path::PathBuf, sync::Arc}; + +/// Graph view is a read-only version of a graph at a certain point in time. +#[pyclass(name = "VectorizedGraph", frozen)] +pub struct PyVectorizedGraph { + vectors: Arc>, +} + +#[pymethods] +impl PyVectorizedGraph { + #[new] + fn new( + py: Python<'_>, + graph: &PyGraphView, + embedding: &PyFunction, + cache: &str, + node_document: Option, + edge_document: Option, + ) -> PyResult { + // FIXME: we should be able to specify templates only for one type of entity: nodes/edges + + let embedding: Py = embedding.into(); + let graph = graph.graph.clone(); + let cache = PathBuf::from(cache); + + // FIXME: Maybe we should have two versions: a VectorizedGraph (sync) and AsyncVectorizedGraph, in both python and rust + // this instead is just terrible + pyo3_asyncio::tokio::run(py, async move { + let vectorized_graph = match (node_document, edge_document) { + (Some(node_document), Some(edge_document)) => { + let node_template = move |vertex: &VertexView| { + vertex.properties().get(&node_document).unwrap().to_string() + }; + let edge_template = move |edge: &EdgeView| { + edge.properties().get(&edge_document).unwrap().to_string() + }; + graph.vectorize_with_templates( + Box::new(embedding.clone()), + &cache, + node_template, + edge_template, + ) + } + (None, None) => graph.vectorize(Box::new(embedding.clone()), &cache), + _ => panic!("you need to specify both templates for now sadly"), + }; + + Ok(PyVectorizedGraph { + vectors: Arc::new(vectorized_graph.await), + }) + }) + } + + fn similarity_search( + &self, + py: Python<'_>, + query: String, + init: usize, + min_nodes: usize, + min_edges: usize, + limit: usize, + ) -> PyResult> { + let vectors = self.vectors.clone(); + pyo3_asyncio::tokio::run(py, async move { + let docs = vectors + .similarity_search( + query.as_str(), + init, + min_nodes, + min_edges, + limit, + None, + None, + ) + .await; + Ok(docs) + }) + } +} + +impl EmbeddingFunction for Py { + fn call(&self, texts: Vec) -> BoxFuture<'static, Vec> { + // FIXME: return result and avoid unwraps!! + + let embedding_function = self.clone(); + + Box::pin(async move { + Python::with_gil(|py| { + let python_texts = PyList::new(py, texts); + let result = embedding_function.call1(py, (python_texts,)).unwrap(); + let embeddings: &PyList = result.downcast(py).unwrap(); + + embeddings + .iter() + .map(|embedding| { + let pylist: &PyList = embedding.downcast().unwrap(); + pylist + .iter() + .map(|element| element.extract::().unwrap()) + .collect_vec() + }) + .collect_vec() + }) + }) + } +} diff --git a/raphtory/src/python/types/iterable.rs b/raphtory/src/python/types/iterable.rs new file mode 100644 index 0000000000..9f73ffea8b --- /dev/null +++ b/raphtory/src/python/types/iterable.rs @@ -0,0 +1,115 @@ +use crate::{ + db::api::view::BoxedIter, + python::types::repr::{iterator_repr, Repr}, +}; +use pyo3::{IntoPy, PyObject}; +use std::{marker::PhantomData, sync::Arc}; + +pub struct Iterable + From + Repr> { + pub name: &'static str, + pub builder: Arc BoxedIter + Send + Sync + 'static>, + pytype: PhantomData, +} + +impl + From + Repr> Iterable { + pub fn iter(&self) -> BoxedIter { + (self.builder)() + } + pub fn py_iter(&self) -> BoxedIter { + Box::new(self.iter().map(|i| i.into())) + } + pub fn new It + Send + Sync + 'static, It: Iterator + Send + 'static>( + name: &'static str, + builder: F, + ) -> Self + where + It::Item: Into, + { + let builder = Arc::new(move || { + let iter: BoxedIter = Box::new(builder().map(|v| v.into())); + iter + }); + Self { + name, + builder, + pytype: Default::default(), + } + } + pub fn iter_eq>(&self, other: J) -> bool + where + I: PartialEq, + { + self.iter().eq(other) + } +} + +impl + From + Repr, J> PartialEq + for Iterable +where + for<'a> &'a J: IntoIterator, +{ + fn eq(&self, other: &J) -> bool { + self.iter_eq(other) + } +} + +impl + From + Repr> Repr for Iterable { + fn repr(&self) -> String { + format!("{}([{}])", self.name, iterator_repr(self.py_iter())) + } +} + +pub struct NestedIterable + From + Repr> { + pub name: &'static str, + pub builder: Arc BoxedIter> + Send + Sync + 'static>, + pytype: PhantomData, +} + +impl + From + Repr> NestedIterable { + pub fn iter(&self) -> BoxedIter> { + (self.builder)() + } + pub fn new It + Send + Sync + 'static, It: Iterator + Send + 'static>( + name: &'static str, + builder: F, + ) -> Self + where + It::Item: Iterator + Send, + ::Item: Into + Send, + { + let builder = Arc::new(move || { + let iter: BoxedIter> = Box::new(builder().map(|it| { + let iter: BoxedIter = Box::new(it.map(|v| v.into())); + iter + })); + iter + }); + Self { + name, + builder, + pytype: Default::default(), + } + } + + pub fn iter_eq, J: IntoIterator>(&self, other: JJ) -> bool + where + I: PartialEq, + { + self.iter() + .zip(other) + .all(|(t, o)| t.zip(o).all(|(tt, oo)| tt == oo)) + } +} + +impl + From + Repr> Repr for NestedIterable { + fn repr(&self) -> String { + format!( + "{}([{}])", + self.name, + iterator_repr( + self.iter() + .map(|it| format!("[{}]", iterator_repr(it.map(|i| PyI::from(i))))) + ) + ) + } +} diff --git a/raphtory/src/python/types/macros/cmp.rs b/raphtory/src/python/types/macros/cmp.rs new file mode 100644 index 0000000000..87627a6798 --- /dev/null +++ b/raphtory/src/python/types/macros/cmp.rs @@ -0,0 +1,29 @@ +/// Add equality support to pyclass +/// +/// # Arguments +/// +/// * `name` - The identifier for the struct +/// * `cmp_item` - Struct to use for comparisons, needs to support `cmp_item: From<&name>` +/// and `cmp_item: PartialEq` and `cmp_item: FromPyObject` with conversion for all +/// the python types we want to compare with +macro_rules! py_eq { + ($name:ty, $cmp_name:ty) => { + #[pyo3::pymethods] + impl $name { + pub fn __richcmp__( + &self, + other: $cmp_name, + op: pyo3::basic::CompareOp, + ) -> pyo3::PyResult { + match op { + pyo3::basic::CompareOp::Lt => Err(PyTypeError::new_err("not ordered")), + pyo3::basic::CompareOp::Le => Err(PyTypeError::new_err("not ordered")), + pyo3::basic::CompareOp::Eq => Ok(<$cmp_name>::from(self) == other), + pyo3::basic::CompareOp::Ne => Ok(<$cmp_name>::from(self) != other), + pyo3::basic::CompareOp::Gt => Err(PyTypeError::new_err("not ordered")), + pyo3::basic::CompareOp::Ge => Err(PyTypeError::new_err("not ordered")), + } + } + } + }; +} diff --git a/raphtory/src/python/types/macros/iterable.rs b/raphtory/src/python/types/macros/iterable.rs new file mode 100644 index 0000000000..db7f727538 --- /dev/null +++ b/raphtory/src/python/types/macros/iterable.rs @@ -0,0 +1,301 @@ +// internal macro for sum and mean methods +macro_rules! _py_numeric_methods { + ($name:ident, $item:ty, $pyitem:ty) => { + #[pymethods] + impl $name { + pub fn sum(&self) -> $pyitem { + let v: $item = self.iter().sum(); + v.into() + } + + pub fn mean(&self) -> f64 { + use $crate::python::types::wrappers::iterators::MeanExt; + self.iter().mean() + } + } + }; +} + +// Internal macro defining max and min on ordered iterables +macro_rules! _py_ord_max_min_methods { + ($name:ident, $pyitem:ty) => { + #[pymethods] + impl $name { + pub fn max(&self) -> Option<$pyitem> { + self.iter().max().map(|v| v.into()) + } + + pub fn min(&self) -> Option<$pyitem> { + self.iter().min().map(|v| v.into()) + } + } + }; +} + +// Internal macro defining max and min on float iterables +macro_rules! _py_float_max_min_methods { + ($name:ident, $pyitem:ty) => { + #[pymethods] + impl $name { + pub fn max(&self) -> Option<$pyitem> { + self.iter().max_by(|a, b| a.total_cmp(b)).map(|v| v.into()) + } + pub fn min(&self) -> Option<$pyitem> { + self.iter().min_by(|a, b| a.total_cmp(b)).map(|v| v.into()) + } + } + }; +} + +// Internal macro for methods supported by all iterables (also used by nested iterables) +macro_rules! _py_iterable_base_methods { + ($name:ident, $iter:ty) => { + #[pymethods] + impl $name { + pub fn __iter__(&self) -> $iter { + self.iter().into() + } + + pub fn __len__(&self) -> usize { + self.iter().count() + } + + pub fn __repr__(&self) -> String { + self.repr() + } + } + }; +} + +// internal macro for the collect method (as it is different for nested iterables) +macro_rules! _py_iterable_collect_method { + ($name:ident, $pyitem:ty) => { + #[pymethods] + impl $name { + pub fn collect(&self) -> Vec<$pyitem> { + self.iter().map(|v| v.into()).collect() + } + } + }; +} + +/// Construct a python Iterable struct which wraps a closure that returns an iterator +/// +/// Does not implement any methods! +/// +/// # Arguments +/// +/// * `name` - The identifier for the new struct +/// * `item` - The type of `Item` for the wrapped iterator builder +/// * `pyitem` - The type of the python wrapper for `Item` (optional if `item` implements `IntoPy`, need `item: Into`) +macro_rules! py_iterable_base { + ($name:ident, $item:ty) => { + py_iterable!($name, $item, $item); + }; + ($name:ident, $item:ty, $pyitem:ty) => { + #[pyclass] + pub struct $name($crate::python::types::iterable::Iterable<$item, $pyitem>); + + impl Repr for $name { + fn repr(&self) -> String { + self.0.repr() + } + } + + impl std::ops::Deref for $name { + type Target = $crate::python::types::iterable::Iterable<$item, $pyitem>; + + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + impl It + Send + Sync + 'static, It: Iterator + Send + 'static> From for $name + where + It::Item: Into<$item>, + { + fn from(value: F) -> Self { + Self($crate::python::types::iterable::Iterable::new( + stringify!($name), + value, + )) + } + } + }; +} + +/// Construct a python Iterable struct which wraps a closure that returns an iterator +/// +/// Has methods `__iter__`, `__len__`, `__repr__`, `collect` +/// +/// # Arguments +/// +/// * `name` - The identifier for the new struct +/// * `item` - The type of `Item` for the wrapped iterator builder +/// * `pyitem` - The type of the python wrapper for `Item` (optional if `item` implements `IntoPy`, need Into<`pyitem`> to be implemented for `item`) +macro_rules! py_iterable { + ($name:ident, $item:ty) => { + py_iterable!($name, $item, $item); + }; + ($name:ident, $item:ty, $pyitem:ty) => { + py_iterable_base!($name, $item, $pyitem); + _py_iterable_base_methods!($name, $crate::python::utils::PyGenericIterator); + _py_iterable_collect_method!($name, $pyitem); + }; +} + +/// Construct a python Iterable struct which wraps a closure that returns an iterator of ordered values +/// +/// additionally adds the `min` and `max` methods to those created by `py_iterable` +/// # Arguments +/// +/// * `name` - The identifier for the new struct +/// * `item` - The type of `Item` for the wrapped iterator builder +/// * `pyitem` - The type of the python wrapper for `Item` (optional if `item` implements `IntoPy`, need Into<`pyitem`> to be implemented for `item`) +/// * `pyiter` - The python iterator wrapper that should be returned when calling `__iter__` (needs to have the same `item` and `pyitem`) +macro_rules! py_ordered_iterable { + ($name:ident, $item:ty) => { + py_ordered_iterable!($name, $item, $item); + }; + ($name:ident, $item:ty, $pyitem:ty) => { + py_iterable!($name, $item, $pyitem); + _py_ord_max_min_methods!($name, $pyitem); + }; +} + +/// Construct a python Iterable struct which wraps a closure that returns an iterator of ordered and summable values +/// +/// additionally adds the `mean` and `sum` methods to those created by `py_ordered_iterable` +/// # Arguments +/// +/// * `name` - The identifier for the new struct +/// * `item` - The type of `Item` for the wrapped iterator builder +/// * `pyitem` - The type of the python wrapper for `Item` (optional if `item` implements `IntoPy`, need Into<`pyitem`> to be implemented for `item`) +/// * `pyiter` - The python iterator wrapper that should be returned when calling `__iter__` (needs to have the same `item` and `pyitem`) +macro_rules! py_numeric_iterable { + ($name:ident, $item:ty) => { + py_numeric_iterable!($name, $item, $item); + }; + ($name:ident, $item:ty, $pyitem:ty) => { + py_ordered_iterable!($name, $item, $pyitem); + _py_numeric_methods!($name, $item, $pyitem); + }; +} + +/// Construct a python Iterable struct which wraps a closure that returns an iterator of float values +/// +/// This acts the same as `py_numeric_iterable` but with special implementations of `max` and `min` for floats. +/// +/// # Arguments +/// +/// * `name` - The identifier for the new struct +/// * `item` - The type of `Item` for the wrapped iterator builder +/// * `pyitem` - The type of the python wrapper for `Item` (optional if `item` implements `IntoPy`, need Into<`pyitem`> to be implemented for `item`) +/// * `pyiter` - The python iterator wrapper that should be returned when calling `__iter__` (needs to have the same `item` and `pyitem`) +macro_rules! py_float_iterable { + ($name:ident, $item:ty) => { + py_float_iterable!($name, $item, $item); + }; + ($name:ident, $item:ty, $pyitem:ty) => { + py_iterable!($name, $item, $pyitem); + _py_numeric_methods!($name, $item, $pyitem); + _py_float_max_min_methods!($name, $pyitem); + }; +} + +/// Add equality support to iterable +/// +/// +/// # Arguments +/// +/// * `name` - The identifier for the iterable struct +/// * `cmp_item` - Struct to use for comparisons, needs to support `cmp_item: From` +/// and `cmp_item: PartialEq` and FromPyObject for all the python types we +/// want to compare with +/// * `cmp_internal` - Name for the internal Enum that is created by the macro to implement +/// the conversion from python (only needed because we can't create our own +/// unique identifier without a proc macro) +macro_rules! py_iterable_comp { + ($name:ty, $cmp_item:ty, $cmp_internal:ident) => { + #[derive(Clone)] + enum $cmp_internal { + Vec(Vec<$cmp_item>), + This(Py<$name>), + } + + impl<'source> FromPyObject<'source> for $cmp_internal { + fn extract(ob: &'source PyAny) -> PyResult { + if let Ok(s) = ob.extract::>() { + Ok($cmp_internal::This(s)) + } else if let Ok(v) = ob.extract::>() { + Ok($cmp_internal::Vec(v)) + } else { + Err(pyo3::exceptions::PyTypeError::new_err("cannot compare")) + } + } + } + + impl From<$name> for $cmp_internal { + fn from(value: $name) -> Self { + let py_value = Python::with_gil(|py| Py::new(py, value)).unwrap(); + Self::This(py_value) + } + } + + impl $cmp_internal { + fn iter_py<'py>( + &'py self, + py: Python<'py>, + ) -> Box + 'py> { + match self { + Self::Vec(v) => Box::new(v.iter().cloned()), + Self::This(t) => Box::new(t.borrow(py).iter().map_into()), + } + } + } + + impl PartialEq for $cmp_internal { + fn eq(&self, other: &Self) -> bool { + Python::with_gil(|py| self.iter_py(py).eq(other.iter_py(py))) + } + } + + impl, J: Into<$cmp_item>> From for $cmp_internal { + fn from(value: I) -> Self { + Self::Vec(value.map_into().collect()) + } + } + + #[pymethods] + impl $name { + fn __richcmp__( + &self, + other: $cmp_internal, + op: pyo3::basic::CompareOp, + py: Python<'_>, + ) -> PyResult { + match op { + pyo3::basic::CompareOp::Lt => { + Err(pyo3::exceptions::PyTypeError::new_err("not ordered")) + } + pyo3::basic::CompareOp::Le => { + Err(pyo3::exceptions::PyTypeError::new_err("not ordered")) + } + pyo3::basic::CompareOp::Eq => Ok(self + .iter() + .map(|t| <$cmp_item>::from(t)) + .eq(other.iter_py(py))), + pyo3::basic::CompareOp::Ne => { + Ok(!self.__richcmp__(other, pyo3::basic::CompareOp::Eq, py)?) + } + pyo3::basic::CompareOp::Gt => { + Err(pyo3::exceptions::PyTypeError::new_err("not ordered")) + } + pyo3::basic::CompareOp::Ge => { + Err(pyo3::exceptions::PyTypeError::new_err("not ordered")) + } + } + } + } + }; +} diff --git a/raphtory/src/python/types/macros/mod.rs b/raphtory/src/python/types/macros/mod.rs new file mode 100644 index 0000000000..8d589e93db --- /dev/null +++ b/raphtory/src/python/types/macros/mod.rs @@ -0,0 +1,6 @@ +#[macro_use] +pub mod iterable; +#[macro_use] +pub mod nested_iterable; +#[macro_use] +pub mod cmp; diff --git a/py-raphtory/src/macros/nested_iterable.rs b/raphtory/src/python/types/macros/nested_iterable.rs similarity index 81% rename from py-raphtory/src/macros/nested_iterable.rs rename to raphtory/src/python/types/macros/nested_iterable.rs index 8606b83ccc..3f28dd35d1 100644 --- a/py-raphtory/src/macros/nested_iterable.rs +++ b/raphtory/src/python/types/macros/nested_iterable.rs @@ -1,21 +1,28 @@ // Internal macro to create the struct for a nested iterable -macro_rules! _py_nested_iterable_base { +macro_rules! py_nested_iterable_base { + ($name:ident, $item:ty) => { + py_nested_iterable_base!($name, $item, $item); + }; ($name:ident, $item:ty, $pyitem:ty) => { #[pyclass] - pub struct $name($crate::types::iterable::NestedIterable<$item, $pyitem>); + pub struct $name($crate::python::types::iterable::NestedIterable<$item, $pyitem>); - impl Deref for $name { - type Target = $crate::types::iterable::NestedIterable<$item, $pyitem>; + impl std::ops::Deref for $name { + type Target = $crate::python::types::iterable::NestedIterable<$item, $pyitem>; fn deref(&self) -> &Self::Target { &self.0 } } - impl BoxedIter> + Sync + Send + 'static> From for $name { + impl It + Send + Sync + 'static, It: Iterator + Send + 'static> From for $name + where + It::Item: Iterator + Send, + ::Item: Into<$item> + Send, + { fn from(value: F) -> Self { - Self($crate::types::iterable::NestedIterable::new( - stringify!($name).to_string(), + Self($crate::python::types::iterable::NestedIterable::new( + stringify!($name), value, )) } @@ -25,8 +32,8 @@ macro_rules! _py_nested_iterable_base { // Internal macro to create basic methods for a nested iterable macro_rules! _py_nested_iterable_methods { - ($name:ident, $pyitem:ty, $iter:ty) => { - _py_iterable_base_methods!($name, $iter); + ($name:ident, $pyitem:ty) => { + _py_iterable_base_methods!($name, $crate::python::utils::PyNestedGenericIterator); #[pymethods] impl $name { @@ -130,12 +137,12 @@ macro_rules! _py_nested_float_max_min_methods { /// * `pyitem` - The type of the python wrapper for `Item` (optional if `item` implements `IntoPy`, need Into<`pyitem`> to be implemented for `item`) /// * `iter` - The python iterator wrapper that should be returned when calling `__iter__` macro_rules! py_nested_iterable { - ($name:ident, $item:ty, $iter:ty) => { - py_nested_iterable!($name, $item, $item, $iter); + ($name:ident, $item:ty) => { + py_nested_iterable!($name, $item, $item); }; - ($name:ident, $item:ty, $pyitem:ty, $iter:ty) => { - _py_nested_iterable_base!($name, $item, $pyitem); - _py_nested_iterable_methods!($name, $pyitem, $iter); + ($name:ident, $item:ty, $pyitem:ty) => { + py_nested_iterable_base!($name, $item, $pyitem); + _py_nested_iterable_methods!($name, $pyitem); }; } @@ -147,11 +154,10 @@ macro_rules! py_nested_iterable { /// /// * `name` - The identifier for the new struct /// * `item` - The type of `Item` for the wrapped iterator builder -/// * `iter` - The python iterator wrapper that should be returned when calling `__iter__` /// * `option_value_iterable` - The iterable to return for `max` and `min` (should have item type `Option`) macro_rules! py_nested_ordered_iterable { - ($name:ident, $item:ty, $iter:ty, $option_value_iterable:ty) => { - py_nested_iterable!($name, $item, $iter); + ($name:ident, $item:ty, $option_value_iterable:ty) => { + py_nested_iterable!($name, $item); _py_nested_ord_max_min_methods!($name, $item, $option_value_iterable); }; } @@ -169,8 +175,8 @@ macro_rules! py_nested_ordered_iterable { /// * `value_iterable` - The iterable to return for `sum` and `mean` /// * `option_value_iterable` - The iterable to return for `max` and `min` (should have item type `Option`) macro_rules! py_nested_numeric_iterable { - ($name:ident, $item:ty, $iter:ty, $value_iterable:ty, $option_value_iterable:ty) => { - py_nested_ordered_iterable!($name, $item, $iter, $option_value_iterable); + ($name:ident, $item:ty, $value_iterable:ty, $option_value_iterable:ty) => { + py_nested_ordered_iterable!($name, $item, $option_value_iterable); _py_nested_numeric_methods!($name, $item, $value_iterable); }; } @@ -187,8 +193,9 @@ macro_rules! py_nested_numeric_iterable { /// * `iter` - The python iterator wrapper that should be returned when calling `__iter__` /// * `value_iterable` - The iterable to return for `sum` and `mean` /// * `option_value_iterable` - The iterable to return for `max` and `min` (should have item type `Option`) +#[allow(unused_macros)] macro_rules! py_nested_float_iterable { - ($name:ident, $item:ty, $iter:ty, $value_iterable:ty, $option_value_iterable:ty) => { + ($name:ident, $item:ty, $value_iterable:ty, $option_value_iterable:ty) => { py_nested_iterable!($name, $item); _py_nested_numeric_methods!($name, $item, $value_iterable); _py_nested_float_max_min_methods!($name, $item, $option_value_iterable); diff --git a/raphtory/src/python/types/mod.rs b/raphtory/src/python/types/mod.rs new file mode 100644 index 0000000000..713089c41a --- /dev/null +++ b/raphtory/src/python/types/mod.rs @@ -0,0 +1,6 @@ +#[macro_use] +pub mod macros; + +pub mod iterable; +pub mod repr; +pub mod wrappers; diff --git a/py-raphtory/src/types/repr.rs b/raphtory/src/python/types/repr.rs similarity index 74% rename from py-raphtory/src/types/repr.rs rename to raphtory/src/python/types/repr.rs index 40df935d52..4bd0b1f192 100644 --- a/py-raphtory/src/types/repr.rs +++ b/raphtory/src/python/types/repr.rs @@ -1,6 +1,7 @@ +use crate::core::{storage::locked_view::LockedView, ArcStr}; use chrono::NaiveDateTime; use itertools::Itertools; -use std::collections::HashMap; +use std::{collections::HashMap, ops::Deref}; pub fn iterator_repr, V: Repr>(iter: I) -> String { let values: Vec = iter.take(11).map(|v| v.repr()).collect(); @@ -11,6 +12,18 @@ pub fn iterator_repr, V: Repr>(iter: I) -> String { } } +pub fn iterator_dict_repr, K: Repr, V: Repr>(iter: I) -> String { + let values: Vec = iter + .take(11) + .map(|(k, v)| format!("{}: {}", k.repr(), v.repr())) + .collect(); + if values.len() < 11 { + values.join(", ") + } else { + values[0..10].join(", ") + ", ..." + } +} + pub trait Repr { fn repr(&self) -> String; } @@ -27,6 +40,18 @@ impl Repr for u32 { } } +impl Repr for u8 { + fn repr(&self) -> String { + self.to_string() + } +} + +impl Repr for u16 { + fn repr(&self) -> String { + self.to_string() + } +} + impl Repr for u64 { fn repr(&self) -> String { self.to_string() @@ -69,6 +94,18 @@ impl Repr for String { } } +impl Repr for ArcStr { + fn repr(&self) -> String { + self.to_string() + } +} + +impl Repr for &ArcStr { + fn repr(&self) -> String { + self.to_string() + } +} + impl Repr for &str { fn repr(&self) -> String { self.to_string() @@ -113,6 +150,12 @@ impl Repr for (S, T) { } } +impl<'a, T: Repr> Repr for LockedView<'a, T> { + fn repr(&self) -> String { + self.deref().repr() + } +} + #[cfg(test)] mod repr_tests { use super::*; diff --git a/raphtory/src/python/types/wrappers/iterators.rs b/raphtory/src/python/types/wrappers/iterators.rs new file mode 100644 index 0000000000..7ac80f0863 --- /dev/null +++ b/raphtory/src/python/types/wrappers/iterators.rs @@ -0,0 +1,75 @@ +use crate::{db::api::view::BoxedIter, prelude::Prop, python::types::repr::Repr}; +use itertools::Itertools; +use num::cast::AsPrimitive; +use pyo3::prelude::*; +use std::{i64, iter::Sum}; + +pub(crate) trait MeanExt: Iterator +where + V: AsPrimitive + Sum, +{ + fn mean(self) -> f64 + where + Self: Sized, + { + let mut count: usize = 0; + let sum: V = self.inspect(|_| count += 1).sum(); + + if count > 0 { + sum.as_() / (count as f64) + } else { + 0.0 + } + } +} + +impl, V: AsPrimitive + Sum> MeanExt for I {} + +py_float_iterable!(Float64Iterable, f64); + +py_numeric_iterable!(U64Iterable, u64); +py_nested_numeric_iterable!(NestedU64Iterable, u64, U64Iterable, OptionU64Iterable); + +py_iterable!(OptionU64U64Iterable, Option<(u64, u64)>); +py_ordered_iterable!(U64U64Iterable, (u64, u64)); +py_nested_ordered_iterable!(NestedU64U64Iterable, (u64, u64), OptionU64U64Iterable); + +py_iterable!(OptionU64Iterable, Option, Option); +_py_ord_max_min_methods!(OptionU64Iterable, Option); + +py_iterable!(PropIterable, Prop, Prop); +py_iterable_comp!(PropIterable, Prop, PropIterableCmp); + +py_numeric_iterable!(I64Iterable, i64); +py_nested_numeric_iterable!(NestedI64Iterable, i64, I64Iterable, OptionI64Iterable); + +py_iterable!(OptionI64Iterable, Option); +_py_ord_max_min_methods!(OptionI64Iterable, Option); +py_iterable!(OptionOptionI64Iterable, Option>); +_py_ord_max_min_methods!(OptionOptionI64Iterable, Option>); + +py_nested_ordered_iterable!( + NestedOptionI64Iterable, + Option, + OptionOptionI64Iterable +); + +py_numeric_iterable!(UsizeIterable, usize); +py_iterable_comp!(UsizeIterable, usize, UsizeIterableCmp); + +py_ordered_iterable!(OptionUsizeIterable, Option); +py_nested_numeric_iterable!( + NestedUsizeIterable, + usize, + UsizeIterable, + OptionUsizeIterable +); + +py_iterable!(BoolIterable, bool); +py_nested_iterable!(NestedBoolIterable, bool); + +py_iterable!(StringIterable, String); +py_nested_iterable!(NestedStringIterable, String); + +py_iterable!(StringVecIterable, Vec); +py_nested_iterable!(NestedStringVecIterable, Vec); diff --git a/py-raphtory/src/wrappers/mod.rs b/raphtory/src/python/types/wrappers/mod.rs similarity index 100% rename from py-raphtory/src/wrappers/mod.rs rename to raphtory/src/python/types/wrappers/mod.rs diff --git a/raphtory/src/python/types/wrappers/prop.rs b/raphtory/src/python/types/wrappers/prop.rs new file mode 100644 index 0000000000..402884351c --- /dev/null +++ b/raphtory/src/python/types/wrappers/prop.rs @@ -0,0 +1,84 @@ +use crate::{ + core::Prop, + python::{graph::views::graph_view::PyGraphView, types::repr::Repr}, +}; +use pyo3::{ + exceptions::PyTypeError, types::PyBool, FromPyObject, IntoPy, PyAny, PyObject, PyResult, Python, +}; +use std::{ops::Deref, sync::Arc}; + +impl IntoPy for Prop { + fn into_py(self, py: Python<'_>) -> PyObject { + match self { + Prop::Str(s) => s.into_py(py), + Prop::Bool(bool) => bool.into_py(py), + Prop::U8(u8) => u8.into_py(py), + Prop::U16(u16) => u16.into_py(py), + Prop::I64(i64) => i64.into_py(py), + Prop::U64(u64) => u64.into_py(py), + Prop::F64(f64) => f64.into_py(py), + Prop::DTime(dtime) => dtime.into_py(py), + Prop::Graph(g) => g.into_py(py), // Need to find a better way + Prop::I32(v) => v.into_py(py), + Prop::U32(v) => v.into_py(py), + Prop::F32(v) => v.into_py(py), + Prop::List(v) => v.deref().clone().into_py(py), // Fixme: optimise the clone here? + Prop::Map(v) => v.deref().clone().into_py(py), + } + } +} + +// Manually implemented to make sure we don't end up with f32/i32/u32 from python ints/floats +impl<'source> FromPyObject<'source> for Prop { + fn extract(ob: &'source PyAny) -> PyResult { + if ob.is_instance_of::() { + return Ok(Prop::Bool(ob.extract()?)); + } + if let Ok(v) = ob.extract() { + return Ok(Prop::I64(v)); + } + if let Ok(v) = ob.extract() { + return Ok(Prop::F64(v)); + } + if let Ok(d) = ob.extract() { + return Ok(Prop::DTime(d)); + } + if let Ok(s) = ob.extract::() { + return Ok(Prop::Str(s.into())); + } + if let Ok(g) = ob.extract() { + return Ok(Prop::Graph(g)); + } + if let Ok(list) = ob.extract() { + return Ok(Prop::List(Arc::new(list))); + } + if let Ok(map) = ob.extract() { + return Ok(Prop::Map(Arc::new(map))); + } + Err(PyTypeError::new_err("Not a valid property type")) + } +} + +impl Repr for Prop { + fn repr(&self) -> String { + match &self { + Prop::Str(v) => v.repr(), + Prop::Bool(v) => v.repr(), + Prop::I64(v) => v.repr(), + Prop::U8(v) => v.repr(), + Prop::U16(v) => v.repr(), + Prop::U64(v) => v.repr(), + Prop::F64(v) => v.repr(), + Prop::DTime(v) => v.repr(), + Prop::Graph(g) => PyGraphView::from(g.clone()).repr(), + Prop::I32(v) => v.repr(), + Prop::U32(v) => v.repr(), + Prop::F32(v) => v.repr(), + Prop::List(v) => v.repr(), + Prop::Map(v) => v.repr(), + } + } +} + +pub type PropValue = Option; +pub type PropHistItems = Vec<(i64, Prop)>; diff --git a/raphtory/src/python/utils/errors.rs b/raphtory/src/python/utils/errors.rs new file mode 100644 index 0000000000..7a67443639 --- /dev/null +++ b/raphtory/src/python/utils/errors.rs @@ -0,0 +1,32 @@ +use crate::{ + core::utils::{errors::GraphError, time::error::ParseTimeError}, + graph_loader::source::csv_loader::CsvErr, +}; +use pyo3::{exceptions::PyException, PyErr}; +use std::error::Error; + +impl From for PyErr { + fn from(value: ParseTimeError) -> Self { + adapt_err_value(&value) + } +} + +impl From for PyErr { + fn from(value: GraphError) -> Self { + adapt_err_value(&value) + } +} + +impl From for PyErr { + fn from(value: CsvErr) -> Self { + adapt_err_value(&value) + } +} + +pub fn adapt_err_value(err: &E) -> PyErr +where + E: Error + ?Sized, +{ + let error_log = display_error_chain::DisplayErrorChain::new(err).to_string(); + PyException::new_err(error_log) +} diff --git a/py-raphtory/src/utils.rs b/raphtory/src/python/utils/mod.rs similarity index 50% rename from py-raphtory/src/utils.rs rename to raphtory/src/python/utils/mod.rs index 666a5e2847..a756d1fd23 100644 --- a/py-raphtory/src/utils.rs +++ b/raphtory/src/python/utils/mod.rs @@ -2,18 +2,18 @@ //! //! This module contains helper functions for the Python bindings. //! These functions are not part of the public API and are not exported to the Python module. -use crate::vertex::PyVertex; +use crate::{ + core::{ + entities::vertices::{input_vertex::InputVertex, vertex_ref::VertexRef}, + utils::time::{error::ParseTimeError, Interval, IntoTime, TryIntoTime}, + }, + db::api::view::*, + python::graph::vertex::PyVertex, +}; use chrono::NaiveDateTime; -use pyo3::exceptions::{PyException, PyTypeError}; -use pyo3::prelude::*; -use raphtory::core as dbc; -use raphtory::core::time::error::ParseTimeError; -use raphtory::core::time::{Interval, TryIntoTime}; -use raphtory::core::vertex::InputVertex; -use raphtory::core::vertex_ref::VertexRef; -use raphtory::db::view_api::time::WindowSet; -use raphtory::db::view_api::TimeOps; -use std::error::Error; +use pyo3::{exceptions::PyTypeError, prelude::*}; + +pub mod errors; /// Extract a `VertexRef` from a Python object. /// The object can be a `str`, `u64` or `PyVertex`. @@ -26,77 +26,20 @@ use std::error::Error; /// /// Returns /// A `VertexRef` extracted from the Python object. -pub(crate) fn extract_vertex_ref(vref: &PyAny) -> PyResult { - if let Ok(s) = vref.extract::() { - Ok(s.into()) - } else if let Ok(gid) = vref.extract::() { - Ok(gid.into()) - } else if let Ok(v) = vref.extract::() { - Ok(v.into()) - } else { - Err(PyTypeError::new_err("Not a valid vertex")) +impl<'source> FromPyObject<'source> for VertexRef { + fn extract(vref: &'source PyAny) -> PyResult { + if let Ok(s) = vref.extract::() { + Ok(s.into()) + } else if let Ok(gid) = vref.extract::() { + Ok(gid.into()) + } else if let Ok(v) = vref.extract::() { + Ok(v.into()) + } else { + Err(PyTypeError::new_err("Not a valid vertex")) + } } } -pub(crate) fn window_impl( - slf: &T, - t_start: Option<&PyAny>, - t_end: Option<&PyAny>, -) -> PyResult { - let t_start = t_start.map(|t| extract_time(t)).transpose()?; - let t_end = t_end.map(|t| extract_time(t)).transpose()?; - Ok(slf.window(t_start.unwrap_or(i64::MIN), t_end.unwrap_or(i64::MAX))) -} - -pub(crate) fn at_impl( - slf: &T, - end: &PyAny, -) -> PyResult { - let end = extract_time(end)?; - Ok(slf.at(end)) -} - -pub fn adapt_err_value(err: &E) -> PyErr -where - E: Error + ?Sized, -{ - let error_log = display_error_chain::DisplayErrorChain::new(err).to_string(); - PyException::new_err(error_log) -} - -pub fn adapt_result(result: Result) -> PyResult -// TODO: make this private -where - E: Error, -{ - result.map_err(|e| adapt_err_value(&e)) -} - -pub(crate) fn expanding_impl(slf: &T, step: &PyAny) -> PyResult -where - T: TimeOps + Clone + Sync + Send + 'static, - T::WindowedViewType: IntoPyObject + Send + Sync, -{ - let step = extract_interval(step)?; - let window_set: WindowSet = adapt_result(slf.expanding(step)).map(|iter| iter.into())?; - Ok(window_set.into()) -} - -pub(crate) fn rolling_impl( - slf: &T, - window: &PyAny, - step: Option<&PyAny>, -) -> PyResult -where - T: TimeOps + Clone + Sync + Send + 'static, - T::WindowedViewType: IntoPyObject + Send + Sync, -{ - let window = extract_interval(window)?; - let step = step.map(extract_interval).transpose()?; - let window_set: WindowSet = adapt_result(slf.rolling(window, step))?; - Ok(window_set.into()) -} - fn parse_email_timestamp(timestamp: &str) -> PyResult { Python::with_gil(|py| { let email_utils = PyModule::import(py, "email.utils")?; @@ -107,89 +50,53 @@ fn parse_email_timestamp(timestamp: &str) -> PyResult { }) } -pub(crate) fn extract_time(time: &PyAny) -> PyResult { - let from_number = time.extract::().map(|n| Ok(n)); - let from_str = time.extract::<&str>().map(|str| { - str.try_into_time() - .or_else(|e| parse_email_timestamp(str).map_err(|_| e)) - }); - - let mut extract_results = vec![from_number, from_str].into_iter(); - let first_valid_extraction = extract_results - .find_map(|result| match result { - Ok(val) => Some(Ok(val)), - Err(_) => None, - }) - .unwrap_or_else(|| { - let message = format!("time '{time}' must be a str or an int"); - Err(PyTypeError::new_err(message)) - })?; - - adapt_result(first_valid_extraction) +pub struct PyTime { + parsing_result: i64, } -pub(crate) fn extract_into_time(time: &PyAny) -> PyResult { - let string = time.extract::(); - let result = string.map(|string| { - let timestamp = string.as_str(); - let parsing_result = timestamp - .try_into_time() - .or_else(|e| parse_email_timestamp(timestamp).map_err(|_| e)); - TimeBox::new(parsing_result) - }); - - let result = result.or_else(|_| { - let number = time.extract::(); - number.map(|number| TimeBox::new(number.try_into_time())) - }); - - let result = result.or_else(|_| { - let parsed_datetime = time.extract::(); - parsed_datetime.map(|parsed_datetime | TimeBox::new(parsed_datetime.try_into_time())) - }); - - result.map_err(|_| { +impl<'source> FromPyObject<'source> for PyTime { + fn extract(time: &'source PyAny) -> PyResult { + if let Ok(string) = time.extract::() { + let timestamp = string.as_str(); + let parsing_result = timestamp + .try_into_time() + .or_else(|e| parse_email_timestamp(timestamp).map_err(|_| e))?; + return Ok(PyTime::new(parsing_result)); + } + if let Ok(number) = time.extract::() { + return Ok(PyTime::new(number.try_into_time()?)); + } + if let Ok(parsed_datetime) = time.extract::() { + return Ok(PyTime::new(parsed_datetime.try_into_time()?)); + } let message = format!("time '{time}' must be a str, dt or an integer"); - PyTypeError::new_err(message) - }) -} - -pub(crate) struct TimeBox { - parsing_result: Result, + Err(PyTypeError::new_err(message)) + } } -impl TimeBox { - fn new(parsing_result: Result) -> Self { +impl PyTime { + fn new(parsing_result: i64) -> Self { Self { parsing_result } } + pub const MIN: PyTime = PyTime { + parsing_result: i64::MIN, + }; + pub const MAX: PyTime = PyTime { + parsing_result: i64::MAX, + }; } -impl TryIntoTime for TimeBox { - fn try_into_time(self) -> Result { +impl IntoTime for PyTime { + fn into_time(self) -> i64 { self.parsing_result } } -pub(crate) fn extract_interval(interval: &PyAny) -> PyResult { - let string = interval.extract::(); - let result = string.map(|string| IntervalBox::new(string.as_str())); - - let result = result.or_else(|_| { - let number = interval.extract::(); - number.map(|number| IntervalBox::new(number)) - }); - - result.map_err(|_| { - let message = format!("interval '{interval}' must be a str or an unsigned integer"); - PyTypeError::new_err(message) - }) -} - -pub(crate) struct IntervalBox { +pub(crate) struct PyInterval { interval: Result, } -impl IntervalBox { +impl PyInterval { fn new(interval: I) -> Self where I: TryInto, @@ -200,9 +107,26 @@ impl IntervalBox { } } -impl TryFrom for Interval { +impl<'source> FromPyObject<'source> for PyInterval { + fn extract(interval: &'source PyAny) -> PyResult { + let string = interval.extract::(); + let result = string.map(|string| PyInterval::new(string.as_str())); + + let result = result.or_else(|_| { + let number = interval.extract::(); + number.map(PyInterval::new) + }); + + result.map_err(|_| { + let message = format!("interval '{interval}' must be a str or an unsigned integer"); + PyTypeError::new_err(message) + }) + } +} + +impl TryFrom for Interval { type Error = ParseTimeError; - fn try_from(value: IntervalBox) -> Result { + fn try_from(value: PyInterval) -> Result { value.interval } } @@ -210,46 +134,51 @@ impl TryFrom for Interval { /// A trait for vertices that can be used as input for the graph. /// This allows us to add vertices with different types of ids, either strings or ints. #[derive(Clone, Debug)] -pub struct InputVertexBox { +pub struct PyInputVertex { id: u64, - name_prop: Option, + name: Option, +} + +impl<'source> FromPyObject<'source> for PyInputVertex { + fn extract(id: &'source PyAny) -> PyResult { + match id.extract::() { + Ok(string) => Ok(PyInputVertex::new(string)), + Err(_) => { + let msg = "IDs need to be strings or an unsigned integers"; + let number = id.extract::().map_err(|_| PyTypeError::new_err(msg))?; + Ok(PyInputVertex::new(number)) + } + } + } } /// Implementation for vertices that can be used as input for the graph. /// This allows us to add vertices with different types of ids, either strings or ints. -impl InputVertexBox { - pub(crate) fn new(vertex: T) -> InputVertexBox +impl PyInputVertex { + pub(crate) fn new(vertex: T) -> PyInputVertex where T: InputVertex, { - InputVertexBox { + PyInputVertex { id: vertex.id(), - name_prop: vertex.name_prop(), + name: vertex.id_str().map(|s| s.into()), } } } /// Implementation for vertices that can be used as input for the graph. /// This allows us to add vertices with different types of ids, either strings or ints. -impl InputVertex for InputVertexBox { +impl InputVertex for PyInputVertex { /// Returns the id of the vertex. fn id(&self) -> u64 { self.id } /// Returns the name property of the vertex. - fn name_prop(&self) -> Option { - self.name_prop.clone() - } -} - -pub(crate) fn extract_input_vertex(id: &PyAny) -> PyResult { - match id.extract::() { - Ok(string) => Ok(InputVertexBox::new(string)), - Err(_) => { - let msg = "IDs need to be strings or an unsigned integers"; - let number = id.extract::().map_err(|_| PyTypeError::new_err(msg))?; - Ok(InputVertexBox::new(number)) + fn id_str(&self) -> Option<&str> { + match &self.name { + Some(n) => Some(n), + None => None, } } } @@ -262,10 +191,10 @@ pub trait WindowSetOps { impl WindowSetOps for WindowSet where T: TimeOps + Clone + Sync + 'static + Send, - T::WindowedViewType: IntoPyObject + Send, + T::WindowedViewType: IntoPy + Send, { fn build_iter(&self) -> PyGenericIterator { - self.clone().map(|v| v.into_py_object()).into() + self.clone().into() } fn time_index(&self, center: bool) -> PyGenericIterable { @@ -301,7 +230,7 @@ pub struct PyWindowSet { impl From> for PyWindowSet where T: TimeOps + Clone + Sync + Send + 'static, - T::WindowedViewType: IntoPyObject + Send + Sync, + T::WindowedViewType: IntoPy + Send + Sync, { fn from(value: WindowSet) -> Self { Self { @@ -310,6 +239,16 @@ where } } +impl IntoPy for WindowSet +where + T: TimeOps + Clone + Sync + Send + 'static, + T::WindowedViewType: IntoPy + Send + Sync, +{ + fn into_py(self, py: Python<'_>) -> PyObject { + PyWindowSet::from(self).into_py(py) + } +} + #[pymethods] impl PyWindowSet { fn __iter__(&self) -> PyGenericIterator { @@ -385,6 +324,29 @@ impl PyGenericIterator { } } -pub(crate) trait IntoPyObject { - fn into_py_object(self) -> PyObject; +#[pyclass(name = "NestedIterator")] +pub struct PyNestedGenericIterator { + iter: BoxedIter, +} + +impl From for PyNestedGenericIterator +where + I: Iterator + Send + 'static, + J: Iterator + Send + 'static, + T: IntoPy + 'static, +{ + fn from(value: I) -> Self { + let py_iter = Box::new(value.map(|item| item.into())); + Self { iter: py_iter } + } +} + +#[pymethods] +impl PyNestedGenericIterator { + fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { + slf + } + fn __next__(&mut self) -> Option { + self.iter.next() + } } diff --git a/raphtory/src/search/mod.rs b/raphtory/src/search/mod.rs new file mode 100644 index 0000000000..2dbd02387c --- /dev/null +++ b/raphtory/src/search/mod.rs @@ -0,0 +1,1123 @@ +// search goes here + +use std::{collections::HashSet, ops::Deref, sync::Arc}; + +use rayon::{prelude::ParallelIterator, slice::ParallelSlice}; +use tantivy::{ + collector::TopDocs, + schema::{Field, Schema, SchemaBuilder, FAST, INDEXED, STORED, TEXT}, + Document, Index, IndexReader, IndexSettings, IndexWriter, TantivyError, +}; + +use crate::{ + core::{ + entities::{vertices::vertex_ref::VertexRef, EID, VID}, + storage::timeindex::{AsTime, TimeIndexEntry}, + utils::errors::GraphError, + ArcStr, PropType, + }, + db::{ + api::{ + mutation::internal::InternalAdditionOps, + view::{ + internal::{DynamicGraph, InheritViewOps, IntoDynamic}, + EdgeViewInternalOps, + }, + }, + graph::{edge::EdgeView, vertex::VertexView}, + }, + prelude::*, +}; + +#[derive(Clone)] +pub struct IndexedGraph { + pub(crate) graph: G, + pub(crate) vertex_index: Arc, + pub(crate) edge_index: Arc, + pub(crate) reader: IndexReader, + pub(crate) edge_reader: IndexReader, +} + +impl Deref for IndexedGraph { + type Target = G; + + fn deref(&self) -> &Self::Target { + &self.graph + } +} + +impl IntoDynamic for IndexedGraph { + fn into_dynamic(self) -> DynamicGraph { + DynamicGraph::new(self) + } +} + +impl InheritViewOps for IndexedGraph {} + +pub(in crate::search) mod fields { + pub const TIME: &str = "time"; + pub const VERTEX_ID: &str = "vertex_id"; + pub const VERTEX_ID_REV: &str = "vertex_id_rev"; + pub const NAME: &str = "name"; + + // edges + // pub const SRC_ID: &str = "src_id"; + pub const SOURCE: &str = "from"; + // pub const DEST_ID: &str = "dest_id"; + pub const DESTINATION: &str = "to"; + pub const EDGE_ID: &str = "edge_id"; +} + +impl From for IndexedGraph { + fn from(graph: G) -> Self { + Self::from_graph(&graph).expect("failed to generate index from graph") + } +} + +impl IndexedGraph { + pub fn into_dynamic_indexed(self) -> IndexedGraph { + IndexedGraph { + graph: self.graph.into_dynamic(), + vertex_index: self.vertex_index, + edge_index: self.edge_index, + reader: self.reader, + edge_reader: self.edge_reader, + } + } +} + +impl IndexedGraph { + fn new_vertex_schema_builder() -> SchemaBuilder { + let mut schema = Schema::builder(); + + // we first add GID time, ID and ID_REV + // ensure time is part of the index + schema.add_i64_field(fields::TIME, INDEXED | STORED); + // ensure we add vertex_id as stored to get back the vertex id after the search + schema.add_u64_field(fields::VERTEX_ID, FAST | STORED); + // reverse to sort by it + schema.add_u64_field(fields::VERTEX_ID_REV, FAST | STORED); + // add name + schema.add_text_field(fields::NAME, TEXT); + schema + } + + fn new_edge_schema_builder() -> SchemaBuilder { + let mut schema = Schema::builder(); + // we first add GID time, ID and ID_REV + // ensure time is part of the index + schema.add_i64_field(fields::TIME, INDEXED | STORED); + // ensure we add vertex_id as stored to get back the vertex id after the search + schema.add_text_field(fields::SOURCE, TEXT); + schema.add_text_field(fields::DESTINATION, TEXT); + schema.add_u64_field(fields::EDGE_ID, FAST | STORED); + + schema + } + + fn schema_from_props, I: IntoIterator>(props: I) -> Schema { + let mut schema = Self::new_vertex_schema_builder(); + + for (prop_name, prop) in props.into_iter() { + match prop { + Prop::Str(_) => { + schema.add_text_field(prop_name.as_ref(), TEXT); + } + Prop::DTime(_) => { + schema.add_date_field(prop_name.as_ref(), INDEXED); + } + _ => todo!(), + } + } + + schema.build() + } + + fn set_schema_field_from_prop(schema: &mut SchemaBuilder, prop: &str, prop_value: Prop) { + match prop_value { + Prop::Str(_) => { + schema.add_text_field(prop, TEXT); + } + Prop::DTime(_) => { + schema.add_date_field(prop, INDEXED); + } + Prop::U8(_) => { + schema.add_u64_field(prop, INDEXED); + } + Prop::U16(_) => { + schema.add_u64_field(prop, INDEXED); + } + Prop::U64(_) => { + schema.add_u64_field(prop, INDEXED); + } + Prop::I64(_) => { + schema.add_i64_field(prop, INDEXED); + } + Prop::I32(_) => { + schema.add_i64_field(prop, INDEXED); + } + Prop::F64(_) => { + schema.add_f64_field(prop, INDEXED); + } + Prop::F32(_) => { + schema.add_f64_field(prop, INDEXED); + } + Prop::Bool(_) => { + schema.add_u64_field(prop, INDEXED); + } + _ => { + schema.add_text_field(prop, TEXT); + } + } + } + + // we need to check every vertex for the properties and add them + // to the schem depending on the type of the property + // + fn schema_for_vertex(g: &G) -> Schema { + let mut schema = Self::new_vertex_schema_builder(); + + // TODO: load all these from the graph at some point in the future + let mut prop_names_set = g + .vertex_meta() + .temporal_prop_meta() + .get_keys() + .into_iter() + .chain(g.vertex_meta().const_prop_meta().get_keys().into_iter()) + .collect::>(); + + for vertex in g.vertices() { + if prop_names_set.is_empty() { + break; + } + let mut found_props: HashSet = HashSet::default(); + found_props.insert("name".into()); + + for prop in prop_names_set.iter() { + // load temporal props + if let Some(prop_value) = vertex + .properties() + .temporal() + .get(prop) + .and_then(|p| p.latest()) + { + if found_props.contains(prop) { + continue; + } + Self::set_schema_field_from_prop(&mut schema, prop, prop_value); + found_props.insert(prop.clone()); + } + // load static props + if let Some(prop_value) = vertex.properties().constant().get(prop) { + if !found_props.contains(prop) { + Self::set_schema_field_from_prop(&mut schema, prop, prop_value); + found_props.insert(prop.clone()); + } + } + } + + for found_prop in found_props { + prop_names_set.remove(&found_prop); + } + } + + schema.build() + } + + // we need to check every vertex for the properties and add them + // to the schem depending on the type of the property + // + fn schema_for_edge(g: &G) -> Schema { + let mut schema = Self::new_edge_schema_builder(); + + // TODO: load all these from the graph at some point in the future + let mut prop_names_set = g + .edge_meta() + .temporal_prop_meta() + .get_keys() + .into_iter() + .chain(g.edge_meta().const_prop_meta().get_keys()) + .collect::>(); + + for edge in g.edges() { + if prop_names_set.is_empty() { + break; + } + let mut found_props: HashSet = HashSet::new(); + + for prop in prop_names_set.iter() { + // load temporal props + if let Some(prop_value) = edge + .properties() + .temporal() + .get(prop) + .and_then(|p| p.latest()) + { + if found_props.contains(prop) { + continue; + } + Self::set_schema_field_from_prop(&mut schema, prop, prop_value); + found_props.insert(prop.clone()); + } + // load static props + if let Some(prop_value) = edge.properties().constant().get(prop) { + if !found_props.contains(prop) { + Self::set_schema_field_from_prop(&mut schema, prop, prop_value); + found_props.insert(prop.clone()); + } + } + } + + for found_prop in found_props { + prop_names_set.remove(&found_prop); + } + } + + schema.build() + } + + fn index_prop_value(document: &mut Document, prop_field: Field, prop_value: Prop) { + match prop_value { + Prop::Str(prop_text) => { + // add the property to the document + document.add_text(prop_field, prop_text); + } + Prop::DTime(prop_time) => { + let time = + tantivy::DateTime::from_timestamp_nanos(prop_time.and_utc().timestamp_nanos()); + document.add_date(prop_field, time); + } + Prop::U8(prop_u8) => { + document.add_u64(prop_field, u64::from(prop_u8)); + } + Prop::U16(prop_u16) => { + document.add_u64(prop_field, u64::from(prop_u16)); + } + Prop::U64(prop_u64) => { + document.add_u64(prop_field, prop_u64); + } + Prop::I64(prop_i64) => { + document.add_i64(prop_field, prop_i64); + } + Prop::I32(prop_i32) => { + document.add_i64(prop_field, i64::from(prop_i32)); + } + Prop::F64(prop_f64) => { + document.add_f64(prop_field, prop_f64); + } + Prop::F32(prop_f32) => { + document.add_f64(prop_field, f64::from(prop_f32)); + } + Prop::Bool(prop_bool) => { + document.add_bool(prop_field, prop_bool); + } + prop => document.add_text(prop_field, prop.to_string()), + } + } + + fn index_vertices(g: &G) -> tantivy::Result<(Index, IndexReader)> { + let schema = Self::schema_for_vertex(g); + let (index, reader) = + Self::new_index(schema.clone(), Self::default_vertex_index_settings()); + + let time_field = schema.get_field(fields::TIME)?; + let vertex_id_field = schema.get_field(fields::VERTEX_ID)?; + let vertex_id_rev_field = schema.get_field(fields::VERTEX_ID_REV)?; + + let writer = Arc::new(parking_lot::RwLock::new(index.writer(100_000_000)?)); + + let v_ids = (0..g.count_vertices()).collect::>(); + + v_ids.par_chunks(128).try_for_each(|v_ids| { + let writer_lock = writer.clone(); + { + let writer_guard = writer_lock.read(); + for v_id in v_ids { + if let Some(vertex) = g.vertex(VertexRef::new((*v_id).into())) { + Self::index_vertex_view( + vertex, + &schema, + &writer_guard, + time_field, + vertex_id_field, + vertex_id_rev_field, + )?; + } + } + } + + let mut writer_guard = writer_lock.write(); + writer_guard.commit()?; + Ok::<(), TantivyError>(()) + })?; + + reader.reload()?; + Ok((index, reader)) + } + + pub fn from_graph(g: &G) -> tantivy::Result { + let (vertex_index, vertex_reader) = Self::index_vertices(g)?; + let (edge_index, edge_reader) = Self::index_edges(g)?; + + Ok(IndexedGraph { + graph: g.clone(), + vertex_index: Arc::new(vertex_index), + edge_index: Arc::new(edge_index), + reader: vertex_reader, + edge_reader, + }) + } + + fn index_vertex_view>( + vertex: VertexView, + schema: &Schema, + writer: &W, + time_field: Field, + vertex_id_field: Field, + vertex_id_rev_field: Field, + ) -> tantivy::Result<()> { + let vertex_id: u64 = usize::from(vertex.vertex) as u64; + + let mut document = Document::new(); + // add the vertex_id + document.add_u64(vertex_id_field, vertex_id); + document.add_u64(vertex_id_rev_field, u64::MAX - vertex_id); + + let name_field = schema.get_field("name")?; + document.add_text(name_field, vertex.name()); + + for (temp_prop_name, temp_prop_value) in vertex.properties().temporal() { + let prop_field = schema.get_field(&temp_prop_name)?; + for (time, prop_value) in temp_prop_value { + // add time to the document + document.add_i64(time_field, time); + + Self::index_prop_value(&mut document, prop_field, prop_value); + } + } + + for (prop_name, prop_value) in vertex.properties().constant() { + let prop_field = schema.get_field(&prop_name)?; + Self::index_prop_value(&mut document, prop_field, prop_value); + } + + writer.add_document(document)?; + Ok(()) + } + + fn index_edge_view>( + e_ref: EdgeView, + schema: &Schema, + writer: &W, + time_field: Field, + source_field: Field, + destination_field: Field, + edge_id_field: Field, + ) -> tantivy::Result<()> { + let edge_ref = e_ref.eref(); + + let src = e_ref.src(); + let dst = e_ref.dst(); + + let mut document = Document::new(); + let edge_id: u64 = Into::::into(edge_ref.pid()) as u64; + document.add_u64(edge_id_field, edge_id); + document.add_text(source_field, src.name()); + document.add_text(destination_field, dst.name()); + + // add all time events + for e in e_ref.explode() { + if let Some(t) = e.time() { + document.add_i64(time_field, t); + } + } + + for (temp_prop_name, temp_prop_value) in e_ref.properties().temporal() { + let prop_field = schema.get_field(&temp_prop_name)?; + for (time, prop_value) in temp_prop_value { + // add time to the document + document.add_i64(time_field, time); + Self::index_prop_value(&mut document, prop_field, prop_value); + } + } + + for (prop_name, prop_value) in e_ref.properties().constant() { + let prop_field = schema.get_field(&prop_name)?; + Self::index_prop_value(&mut document, prop_field, prop_value); + } + + writer.add_document(document)?; // add the edge itself + Ok(()) + } + + pub fn index_edges(g: &G) -> tantivy::Result<(Index, IndexReader)> { + let schema = Self::schema_for_edge(g); + let (index, reader) = Self::new_index(schema.clone(), Self::default_edge_index_settings()); + + let time_field = schema.get_field(fields::TIME)?; + let source_field = schema.get_field(fields::SOURCE)?; + let destination_field = schema.get_field(fields::DESTINATION)?; + let edge_id_field = schema.get_field(fields::EDGE_ID)?; + + let writer = Arc::new(parking_lot::RwLock::new(index.writer(100_000_000)?)); + + let e_ids = (0..g.count_edges()).collect::>(); + let edge_filter = g.edge_filter(); + e_ids.par_chunks(128).try_for_each(|e_ids| { + let writer_lock = writer.clone(); + { + let writer_guard = writer_lock.read(); + for e_id in e_ids { + if let Some(e_ref) = + g.find_edge_id((*e_id).into(), &g.layer_ids(), edge_filter.as_deref()) + { + let e_view = EdgeView::new(g.clone(), e_ref); + Self::index_edge_view( + e_view, + &schema, + &writer_guard, + time_field, + source_field, + destination_field, + edge_id_field, + )?; + } + } + } + + let mut writer_guard = writer_lock.write(); + writer_guard.commit()?; + Ok::<(), TantivyError>(()) + })?; + + reader.reload()?; + Ok((index, reader)) + } + + fn default_vertex_index_settings() -> IndexSettings { + IndexSettings::default() + } + + fn default_edge_index_settings() -> IndexSettings { + IndexSettings::default() + } + + fn new_index(schema: Schema, index_settings: IndexSettings) -> (Index, IndexReader) { + let index = Index::builder() + .settings(index_settings) + .schema(schema) + .create_in_ram() + .expect("failed to create index"); + + let reader = index + .reader_builder() + .reload_policy(tantivy::ReloadPolicy::Manual) + .try_into() + .unwrap(); + (index, reader) + } + + pub fn new(graph: G, vertex_props: I, edge_props: I2) -> Self + where + S: AsRef, + I: IntoIterator, + I2: IntoIterator, + { + let schema = Self::schema_from_props(vertex_props); + + let (index, reader) = Self::new_index(schema, Self::default_vertex_index_settings()); + + let schema = Self::schema_from_props(edge_props); + + let (edge_index, edge_reader) = + Self::new_index(schema, Self::default_edge_index_settings()); + + IndexedGraph { + graph, + vertex_index: Arc::new(index), + edge_index: Arc::new(edge_index), + reader, + edge_reader, + } + } + + pub fn reload(&self) -> Result<(), GraphError> { + self.reader.reload()?; + Ok(()) + } + + fn resolve_vertex_from_search_result( + &self, + vertex_id: Field, + doc: Document, + ) -> Option> { + let vertex_id: usize = doc + .get_first(vertex_id) + .and_then(|value| value.as_u64())? + .try_into() + .ok()?; + let vertex_id = VertexRef::Internal(vertex_id.into()); + self.graph.vertex(vertex_id) + } + + fn resolve_edge_from_search_result( + &self, + edge_id: Field, + doc: Document, + ) -> Option> { + let edge_id: usize = doc + .get_first(edge_id) + .and_then(|value| value.as_u64())? + .try_into() + .ok()?; + let e_ref = self.graph.find_edge_id( + edge_id.into(), + &self.graph.layer_ids(), + self.graph.edge_filter().as_deref(), + )?; + let e_view = EdgeView::new(self.graph.clone(), e_ref); + Some(e_view) + } + + pub fn search( + &self, + q: &str, + limit: usize, + offset: usize, + ) -> Result>, GraphError> { + let searcher = self.reader.searcher(); + let query_parser = tantivy::query::QueryParser::for_index(&self.vertex_index, vec![]); + let query = query_parser.parse_query(q)?; + + let ranking = TopDocs::with_limit(limit).and_offset(offset); + + let top_docs = searcher.search(&query, &ranking)?; + + let vertex_id = self.vertex_index.schema().get_field(fields::VERTEX_ID)?; + + let results = top_docs + .into_iter() + .map(|(_, doc_address)| searcher.doc(doc_address)) + .filter_map(Result::ok) + .filter_map(|doc| self.resolve_vertex_from_search_result(vertex_id, doc)) + .collect::>(); + + Ok(results) + } + + pub fn search_edges( + &self, + q: &str, + limit: usize, + offset: usize, + ) -> Result>, GraphError> { + let searcher = self.edge_reader.searcher(); + let query_parser = tantivy::query::QueryParser::for_index(&self.edge_index, vec![]); + let query = query_parser.parse_query(q)?; + + let ranking = TopDocs::with_limit(limit).and_offset(offset); + + let top_docs = searcher.search(&query, &ranking)?; + + let edge_id = self.edge_index.schema().get_field(fields::EDGE_ID)?; + + let results = top_docs + .into_iter() + .map(|(_, doc_address)| searcher.doc(doc_address)) + .filter_map(Result::ok) + .filter_map(|doc| self.resolve_edge_from_search_result(edge_id, doc)) + .collect::>(); + + Ok(results) + } +} + +impl InternalAdditionOps for IndexedGraph { + #[inline] + fn next_event_id(&self) -> usize { + self.graph.next_event_id() + } + #[inline] + fn resolve_layer(&self, layer: Option<&str>) -> usize { + self.graph.resolve_layer(layer) + } + + #[inline] + fn resolve_vertex(&self, id: u64, name: Option<&str>) -> VID { + self.graph.resolve_vertex(id, name) + } + + #[inline] + fn resolve_graph_property(&self, prop: &str, is_static: bool) -> usize { + self.graph.resolve_graph_property(prop, is_static) + } + + #[inline] + fn resolve_vertex_property( + &self, + prop: &str, + dtype: PropType, + is_static: bool, + ) -> Result { + self.graph.resolve_vertex_property(prop, dtype, is_static) + } + + #[inline] + fn resolve_edge_property( + &self, + prop: &str, + dtype: PropType, + is_static: bool, + ) -> Result { + self.graph.resolve_edge_property(prop, dtype, is_static) + } + + #[inline] + fn process_prop_value(&self, prop: Prop) -> Prop { + self.graph.process_prop_value(prop) + } + + fn internal_add_vertex( + &self, + t: TimeIndexEntry, + v: VID, + props: Vec<(usize, Prop)>, + ) -> Result<(), GraphError> { + let mut document = Document::new(); + // add time to the document + let time = self.vertex_index.schema().get_field(fields::TIME)?; + document.add_i64(time, *t.t()); + // add name to the document + + let name = self.vertex_index.schema().get_field(fields::NAME)?; + document.add_text(name, self.graph.vertex_name(v)); + + // index all props that are declared in the schema + for (prop_id, prop) in props.iter() { + let prop_name = self.graph.vertex_meta().get_prop_name(*prop_id, false); + if let Ok(field) = self.vertex_index.schema().get_field(&prop_name) { + if let Prop::Str(s) = prop { + document.add_text(field, s) + } + } + } + // add the vertex id to the document + self.graph.internal_add_vertex(t, v, props)?; + // get the field from the index + let vertex_id = self.vertex_index.schema().get_field(fields::VERTEX_ID)?; + let vertex_id_rev = self + .vertex_index + .schema() + .get_field(fields::VERTEX_ID_REV)?; + let index_v_id: u64 = Into::::into(v) as u64; + + document.add_u64(vertex_id, index_v_id); + document.add_u64(vertex_id_rev, u64::MAX - index_v_id); + + let mut writer = self.vertex_index.writer(50_000_000)?; + + writer.add_document(document)?; + + writer.commit()?; + + Ok(()) + } + + fn internal_add_edge( + &self, + _t: TimeIndexEntry, + _src: VID, + _dst: VID, + _props: Vec<(usize, Prop)>, + _layer: usize, + ) -> Result { + todo!() + } +} + +#[cfg(test)] +mod test { + use std::time::SystemTime; + + use tantivy::{doc, DocAddress}; + + use super::*; + + #[test] + fn index_numeric_props() { + let graph = Graph::new(); + + graph + .add_vertex( + 1, + "Blerg", + [ + ("age".to_string(), Prop::U64(42)), + ("balance".to_string(), Prop::I64(-1234)), + ], + ) + .expect("failed to add vertex"); + + let ig: IndexedGraph = graph.into(); + + let results = ig + .search("age:42", 5, 0) + .expect("failed to search for vertex") + .into_iter() + .map(|v| v.name()) + .collect::>(); + + assert_eq!(results, vec!["Blerg"]); + } + + #[test] + #[ignore = "this test is for experiments with the jira graph"] + fn load_jira_graph() -> Result<(), GraphError> { + let graph = Graph::load_from_file("/tmp/graphs/jira").expect("failed to load graph"); + assert!(graph.count_vertices() > 0); + + let now = SystemTime::now(); + + let index_graph: IndexedGraph = graph.into(); + let elapsed = now.elapsed().unwrap().as_secs(); + println!("indexing took: {:?}", elapsed); + + let issues = index_graph.search("name:'DEV-1690'", 5, 0)?; + + assert!(!issues.is_empty()); + + let names = issues.into_iter().map(|v| v.name()).collect::>(); + println!("names: {:?}", names); + + Ok(()) + } + + #[test] + fn create_indexed_graph_from_existing_graph() { + let graph = Graph::new(); + + graph + .add_vertex(1, "Gandalf", [("kind".to_string(), Prop::str("Wizard"))]) + .expect("add vertex failed"); + + graph + .add_vertex( + 2, + "Frodo", + [ + ("kind".to_string(), Prop::str("Hobbit")), + ("has_ring".to_string(), Prop::str("yes")), + ], + ) + .expect("add vertex failed"); + + graph + .add_vertex(2, "Merry", [("kind".to_string(), Prop::str("Hobbit"))]) + .expect("add vertex failed"); + + graph + .add_vertex(4, "Gollum", [("kind".to_string(), Prop::str("Creature"))]) + .expect("add vertex failed"); + + graph + .add_vertex(9, "Gollum", [("has_ring".to_string(), Prop::str("yes"))]) + .expect("add vertex failed"); + + graph + .add_vertex(9, "Frodo", [("has_ring".to_string(), Prop::str("no"))]) + .expect("add vertex failed"); + + graph + .add_vertex(10, "Frodo", [("has_ring".to_string(), Prop::str("yes"))]) + .expect("add vertex failed"); + + graph + .add_vertex(10, "Gollum", [("has_ring".to_string(), Prop::str("no"))]) + .expect("add vertex failed"); + + let indexed_graph: IndexedGraph = + IndexedGraph::from_graph(&graph).expect("failed to generate index from graph"); + indexed_graph.reload().expect("failed to reload index"); + + let results = indexed_graph + .search("kind:hobbit", 10, 0) + .expect("search failed"); + let mut actual = results.into_iter().map(|v| v.name()).collect::>(); + let mut expected = vec!["Frodo", "Merry"]; + // FIXME: this is not deterministic + actual.sort(); + expected.sort(); + + assert_eq!(actual, expected); + + let results = indexed_graph + .search("kind:wizard", 10, 0) + .expect("search failed"); + let actual = results.into_iter().map(|v| v.name()).collect::>(); + let expected = vec!["Gandalf"]; + assert_eq!(actual, expected); + + let results = indexed_graph + .search("kind:creature", 10, 0) + .expect("search failed"); + let actual = results.into_iter().map(|v| v.name()).collect::>(); + let expected = vec!["Gollum"]; + assert_eq!(actual, expected); + + // search by name + let results = indexed_graph + .search("name:gollum", 10, 0) + .expect("search failed"); + let actual = results.into_iter().map(|v| v.name()).collect::>(); + let expected = vec!["Gollum"]; + assert_eq!(actual, expected); + } + + #[test] + fn add_vertex_search_by_name() { + let graph = IndexedGraph::new(Graph::new(), NO_PROPS, NO_PROPS); + + graph + .add_vertex(1, "Gandalf", NO_PROPS) + .expect("add vertex failed"); + + graph.reload().expect("reload failed"); + + let vertices = graph + .search(r#"name:gandalf"#, 10, 0) + .expect("search failed"); + + let actual = vertices.into_iter().map(|v| v.name()).collect::>(); + let expected = vec!["Gandalf"]; + + assert_eq!(actual, expected); + } + + #[test] + fn add_vertex_search_by_description() { + let graph = IndexedGraph::new(Graph::new(), [("description", Prop::str(""))], NO_PROPS); + + graph + .add_vertex( + 1, + "Bilbo", + [("description".to_string(), Prop::str("A hobbit"))], + ) + .expect("add vertex failed"); + + graph + .add_vertex( + 2, + "Gandalf", + [("description".to_string(), Prop::str("A wizard"))], + ) + .expect("add vertex failed"); + + graph.reload().expect("reload failed"); + // Find the Wizard + let vertices = graph + .search(r#"description:wizard"#, 10, 0) + .expect("search failed"); + let actual = vertices.into_iter().map(|v| v.name()).collect::>(); + let expected = vec!["Gandalf"]; + assert_eq!(actual, expected); + // Find the Hobbit + let vertices = graph + .search(r#"description:'hobbit'"#, 10, 0) + .expect("search failed"); + let actual = vertices.into_iter().map(|v| v.name()).collect::>(); + let expected = vec!["Bilbo"]; + assert_eq!(actual, expected); + } + + #[test] + fn add_vertex_search_by_description_and_time() { + let graph = IndexedGraph::new(Graph::new(), [("description", Prop::str(""))], NO_PROPS); + + graph + .add_vertex( + 1, + "Gandalf", + [("description".to_string(), Prop::str("The wizard"))], + ) + .expect("add vertex failed"); + + graph + .add_vertex( + 2, + "Saruman", + [("description".to_string(), Prop::str("Another wizard"))], + ) + .expect("add vertex failed"); + + graph.reload().expect("reload failed"); + // Find Saruman + let vertices = graph + .search(r#"description:wizard AND time:[2 TO 5]"#, 10, 0) + .expect("search failed"); + let actual = vertices.into_iter().map(|v| v.name()).collect::>(); + let expected = vec!["Saruman"]; + assert_eq!(actual, expected); + // Find Gandalf + let vertices = graph + .search(r#"description:'wizard' AND time:[1 TO 2}"#, 10, 0) + .expect("search failed"); + let actual = vertices.into_iter().map(|v| v.name()).collect::>(); + let expected = vec!["Gandalf"]; + assert_eq!(actual, expected); + // Find both wizards + let vertices = graph + .search(r#"description:'wizard' AND time:[1 TO 100]"#, 10, 0) + .expect("search failed"); + let mut actual = vertices.into_iter().map(|v| v.name()).collect::>(); + let mut expected = vec!["Gandalf", "Saruman"]; + + // FIXME: this is not deterministic + actual.sort(); + expected.sort(); + + assert_eq!(actual, expected); + } + + #[test] + fn search_by_edge_props() { + let g = Graph::new(); + + g.add_edge( + 1, + "Frodo", + "Gandalf", + [("type".to_string(), Prop::str("friends"))], + None, + ) + .expect("add edge failed"); + g.add_edge( + 1, + "Frodo", + "Gollum", + [("type".to_string(), Prop::str("enemies"))], + None, + ) + .expect("add edge failed"); + + let ig: IndexedGraph = g.into(); + + let results = ig + .search_edges(r#"type:friends"#, 10, 0) + .expect("search failed"); + let actual = results + .into_iter() + .map(|e| (e.src().name(), e.dst().name())) + .collect::>(); + let expected = vec![("Frodo".to_string(), "Gandalf".to_string())]; + + assert_eq!(actual, expected); + + let results = ig + .search_edges(r#"type:enemies"#, 10, 0) + .expect("search failed"); + let actual = results + .into_iter() + .map(|e| (e.src().name(), e.dst().name())) + .collect::>(); + let expected = vec![("Frodo".to_string(), "Gollum".to_string())]; + + assert_eq!(actual, expected); + } + + #[test] + fn search_by_edge_src_dst() { + let g = Graph::new(); + + g.add_edge(1, "Frodo", "Gandalf", NO_PROPS, None) + .expect("add edge failed"); + g.add_edge(1, "Frodo", "Gollum", NO_PROPS, None) + .expect("add edge failed"); + + let ig: IndexedGraph = g.into(); + + let results = ig + .search_edges(r#"from:Frodo"#, 10, 0) + .expect("search failed"); + let mut actual = results + .into_iter() + .map(|e| (e.src().name(), e.dst().name())) + .collect::>(); + let mut expected = vec![ + ("Frodo".to_string(), "Gandalf".to_string()), + ("Frodo".to_string(), "Gollum".to_string()), + ]; + + actual.sort(); + expected.sort(); + + assert_eq!(actual, expected); + + // search by destination + let results = ig.search_edges("to:gollum", 10, 0).expect("search failed"); + let actual = results + .into_iter() + .map(|e| (e.src().name(), e.dst().name())) + .collect::>(); + let expected = vec![("Frodo".to_string(), "Gollum".to_string())]; + + assert_eq!(actual, expected); + } + + #[test] + fn tantivy_101() { + let vertex_index_props = vec!["name"]; + + let mut schema = Schema::builder(); + + for prop in vertex_index_props { + schema.add_text_field(prop.as_ref(), TEXT); + } + + // ensure time is part of the index + schema.add_u64_field("time", INDEXED | STORED); + // ensure we add vertex_id as stored to get back the vertex id after the search + schema.add_text_field("vertex_id", FAST | STORED); + + let index = Index::create_in_ram(schema.build()); + + let reader = index + .reader_builder() + .reload_policy(tantivy::ReloadPolicy::OnCommit) + .try_into() + .unwrap(); + + { + let mut writer = index.writer(50_000_000).unwrap(); + + let name = index.schema().get_field("name").unwrap(); + let time = index.schema().get_field("time").unwrap(); + let vertex_id = index.schema().get_field("vertex_id").unwrap(); + + writer + .add_document(doc!(name => "Gandalf", time => 1u64, vertex_id => 0u64)) + .expect("add document failed"); + + writer.commit().expect("commit failed"); + } + + reader.reload().unwrap(); + + let searcher = reader.searcher(); + + let query_parser = tantivy::query::QueryParser::for_index(&index, vec![]); + let query = query_parser.parse_query(r#"name:"gandalf""#).unwrap(); + + let ranking = TopDocs::with_limit(10).order_by_u64_field(fields::VERTEX_ID.to_string()); + let top_docs: Vec<(u64, DocAddress)> = searcher.search(&query, &ranking).unwrap(); + + assert!(!top_docs.is_empty()); + } + + #[test] + fn property_name_on_vertex_does_not_crash() { + let g = Graph::new(); + g.add_vertex(0, "test", [("name", "test")]).unwrap(); + let gi: IndexedGraph<_> = g.into(); + } +} diff --git a/raphtory/src/vectors/mod.rs b/raphtory/src/vectors/mod.rs new file mode 100644 index 0000000000..3ef1bd3aa2 --- /dev/null +++ b/raphtory/src/vectors/mod.rs @@ -0,0 +1,976 @@ +// use async_openai::types::{CreateEmbeddingRequest, EmbeddingInput}; +// use async_openai::Client; +use async_trait::async_trait; +use futures_util::future::{join_all, BoxFuture}; +// use futures_util::StreamExt; +use itertools::{chain, Itertools}; +use serde::{Deserialize, Serialize, Serializer}; +use std::{ + borrow::Borrow, + collections::{hash_map::DefaultHasher, HashMap, HashSet}, + convert::identity, + fmt::{Display, Formatter}, + fs::{create_dir_all, File}, + future::Future, + hash::{Hash, Hasher}, + io::{BufReader, BufWriter}, + path::Path, +}; + +// use crate::model::graph::edge::Edge; +// use numpy::PyArray2; +// use pyo3::{types::IntoPyDict, Python}; +use crate::{ + db::{ + api::view::internal::{DynamicGraph, IntoDynamic}, + graph::{edge::EdgeView, vertex::VertexView, views::window_graph::WindowedGraph}, + }, + prelude::{EdgeViewOps, GraphViewOps, Layer, LayerOps, TimeOps, VertexViewOps}, +}; + +// #[derive(Clone)] +// struct EdgeId { +// src: u64, +// dst: u64, +// } + +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +enum EntityId { + Node { id: u64 }, + Edge { src: u64, dst: u64 }, +} + +impl EntityId { + fn as_node(&self) -> u64 { + match self { + EntityId::Node { id } => *id, + EntityId::Edge { .. } => panic!("edge id unwrapped as a node id"), + } + } +} + +impl From<&VertexView> for EntityId { + fn from(value: &VertexView) -> Self { + EntityId::Node { id: value.id() } + } +} + +impl From> for EntityId { + fn from(value: VertexView) -> Self { + EntityId::Node { id: value.id() } + } +} + +impl From<&EdgeView> for EntityId { + fn from(value: &EdgeView) -> Self { + EntityId::Edge { + src: value.src().id(), + dst: value.dst().id(), + } + } +} + +impl From> for EntityId { + fn from(value: EdgeView) -> Self { + EntityId::Edge { + src: value.src().id(), + dst: value.dst().id(), + } + } +} + +pub trait EmbeddingFunction: Send + Sync { + fn call(&self, texts: Vec) -> BoxFuture<'static, Vec>; +} + +impl EmbeddingFunction for T +where + T: Fn(Vec) -> F + Send + Sync, + F: Future> + Send + 'static, +{ + fn call(&self, texts: Vec) -> BoxFuture<'static, Vec> { + Box::pin(self(texts)) + } +} + +#[async_trait] +pub trait Vectorizable { + async fn vectorize( + &self, + embedding: Box, + cache_dir: &Path, + ) -> VectorizedGraph; + + async fn vectorize_with_templates( + &self, + embedding: Box, + cache_dir: &Path, + node_template: N, + edge_template: E, + // FIXME: I tried to put templates behind an option but didn't work and hadn't time to fix it + ) -> VectorizedGraph + where + N: Fn(&VertexView) -> String + Sync + Send + 'static, + E: Fn(&EdgeView) -> String + Sync + Send + 'static; +} + +#[async_trait] +impl Vectorizable for G { + async fn vectorize( + &self, + embedding: Box, + cache_dir: &Path, + ) -> VectorizedGraph { + let node_template = |vertex: &VertexView| default_node_template(vertex); + let edge_template = |edge: &EdgeView| default_edge_template(edge); + + self.vectorize_with_templates(embedding, cache_dir, node_template, edge_template) + .await + } + + async fn vectorize_with_templates( + &self, + embedding: Box, + cache_dir: &Path, + node_template: N, + edge_template: E, + ) -> VectorizedGraph + where + N: Fn(&VertexView) -> String + Sync + Send + 'static, + E: Fn(&EdgeView) -> String + Sync + Send + 'static, + { + create_dir_all(cache_dir).expect("Impossible to use cache dir"); + + let node_docs = self + .vertices() + .iter() + .map(|vertex| vertex.generate_doc(&node_template)); + let edge_docs = self.edges().map(|edge| edge.generate_doc(&edge_template)); + + let node_embeddings = generate_embeddings(node_docs, &embedding, cache_dir).await; + let edge_embeddings = generate_embeddings(edge_docs, &embedding, cache_dir).await; + + VectorizedGraph { + graph: self.clone(), + embedding, + node_embeddings, + edge_embeddings, + node_template: Box::new(node_template), + edge_template: Box::new(edge_template), + } + } +} + +fn default_node_template(vertex: &VertexView) -> String { + let name = vertex.name(); + let property_list = vertex.generate_property_list(&identity, vec![], vec![]); + format!("The entity {name} has the following details:\n{property_list}") +} + +#[allow(unstable_name_collisions)] // just update itertools when this is actually stabilised +fn default_edge_template(edge: &EdgeView) -> String { + let src = edge.src().name(); + let dst = edge.dst().name(); + // TODO: property list + + edge.layer_names() + .map(|layer| { + let times = edge + .layer(layer.clone()) + .unwrap() + .history() + .iter() + .join(", "); + match layer.as_ref() { + "_default" => format!("{src} interacted with {dst} at times: {times}"), + layer => format!("{src} {layer} {dst} at times: {times}"), + } + }) + .intersperse("\n".to_owned()) + .collect() +} + +pub struct VectorizedGraph { + graph: G, + embedding: Box, + node_embeddings: HashMap, + edge_embeddings: HashMap, + node_template: Box) -> String + Sync + Send>, + edge_template: Box) -> String + Sync + Send>, +} + +const CHUNK_SIZE: usize = 1000; + +impl VectorizedGraph { + // FIXME: this should return a Result + pub async fn similarity_search( + &self, + query: &str, + init: usize, + min_nodes: usize, + min_edges: usize, + limit: usize, + window_start: Option, + window_end: Option, + ) -> Vec { + let query_embedding = self.embedding.call(vec![query.to_owned()]).await.remove(0); + + let (graph, window_nodes, window_edges): ( + DynamicGraph, + Box>, + Box>, + ) = match (window_start, window_end) { + (None, None) => ( + self.graph.clone().into_dynamic(), + Box::new(self.node_embeddings.iter()), + Box::new(self.edge_embeddings.iter()), + ), + (start, end) => { + let start = start.unwrap_or(i64::MIN); + let end = end.unwrap_or(i64::MAX); + let window = self.graph.window(start, end); + let nodes = self.window_embeddings(&self.node_embeddings, &window); + let edges = self.window_embeddings(&self.edge_embeddings, &window); + ( + window.clone().into_dynamic(), + Box::new(nodes), + Box::new(edges), + ) + } + }; + + // FIRST STEP: ENTRY POINT SELECTION: + assert!( + min_nodes + min_edges <= init, + "min_nodes + min_edges needs to be less or equal to init" + ); + let generic_init = init - min_nodes - min_edges; + + let mut entry_point: Vec = vec![]; + + let scored_nodes = score_entities(&query_embedding, window_nodes); + let mut selected_nodes = find_top_k(scored_nodes, init); + + let scored_edges = score_entities(&query_embedding, window_edges); + let mut selected_edges = find_top_k(scored_edges, init); + + for _ in 0..min_nodes { + let (id, _) = selected_nodes.next().unwrap(); + entry_point.push(id.clone()); + } + for _ in 0..min_edges { + let (id, _) = selected_edges.next().unwrap(); + entry_point.push(id.clone()); + } + + let remaining_entities = find_top_k(chain!(selected_nodes, selected_edges), generic_init); + for (id, _distance) in remaining_entities { + entry_point.push(id.clone()); + } + + // SECONDS STEP: EXPANSION + let mut entity_ids = entry_point; + + while entity_ids.len() < limit { + let candidates = entity_ids.iter().flat_map(|id| match id { + EntityId::Node { id } => { + let edges = graph.vertex(*id).unwrap().edges(); + edges + .map(|edge| { + let edge_id = edge.into(); + let edge_embedding = self.edge_embeddings.get(&edge_id).unwrap(); + (edge_id, edge_embedding) + }) + .collect_vec() + } + EntityId::Edge { src, dst } => { + let edge = graph.edge(*src, *dst).unwrap(); + let src_id: EntityId = edge.src().into(); + let dst_id: EntityId = edge.dst().into(); + let src_embedding = self.node_embeddings.get(&src_id).unwrap(); + let dst_embedding = self.node_embeddings.get(&dst_id).unwrap(); + vec![(src_id, src_embedding), (dst_id, dst_embedding)] + } + }); + + let unique_candidates = candidates.unique_by(|(id, _)| id.clone()); + let valid_candidates = unique_candidates.filter(|(id, _)| !entity_ids.contains(id)); + let scored_candidates = score_entities(&query_embedding, valid_candidates); + let sorted_candidates = find_top_k(scored_candidates, usize::MAX); + let sorted_candidates_ids = sorted_candidates.map(|(id, _)| id).collect_vec(); + + if sorted_candidates_ids.is_empty() { + // TODO: use similarity search again with the whole graph with init + 1 !! + break; + } + + entity_ids.extend(sorted_candidates_ids); + } + + // FINAL STEP: REPRODUCE DOCUMENTS: + + entity_ids + .iter() + .take(limit) + .map(|id| match id { + EntityId::Node { id } => { + self.graph + .vertex(*id) + .unwrap() + .generate_doc(&self.node_template) + .content + } + EntityId::Edge { src, dst } => { + self.graph + .edge(*src, *dst) + .unwrap() + .generate_doc(&self.edge_template) + .content + } + }) + .collect_vec() + } + + fn window_embeddings<'a, I>( + &self, + embeddings: I, + window: &WindowedGraph, + ) -> impl Iterator + 'a + where + I: IntoIterator + 'a, + { + let window = window.clone(); + embeddings.into_iter().filter(move |(id, _)| match id { + EntityId::Node { id } => window.has_vertex(*id), + EntityId::Edge { src, dst } => window.has_edge(*src, *dst, Layer::All), + }) + } + + // pub async fn search_old( + // &self, + // query: &str, + // node_init: usize, + // edge_init: usize, + // limit: usize, + // ) -> Vec { + // let query_embedding = compute_embeddings(vec![query.to_owned()]).await.remove(0); + // + // let mut entry_point: Vec = vec![]; + // let selected_nodes = find_top_k(&query_embedding, &self.node_embeddings, node_init); + // let selected_edges = find_top_k(&query_embedding, &self.edge_embeddings, edge_init); + // for (id, distance) in chain!(selected_nodes, selected_edges) { + // println!(" - At {distance}: {id}"); + // entry_point.push(id.clone()); + // if let EntityId::Edge { src, dst } = id { + // entry_point.push(EntityId::Node { id: *src }); + // entry_point.push(EntityId::Node { id: *dst }); + // } + // } + // + // let mut entity_ids = entry_point; + // + // // it might happen that a node is include here twice, from two different paths in the graph + // // but that is not a problem because the entity_ids list is force to be unique + // let candidates: Vec = entity_ids + // .iter() + // .filter(|id| matches!(id, EntityId::Node { .. })) + // .flat_map(|id| self.get_candidates_from_node(&query_embedding, id)) + // .unique_by(|candidate| (candidate.node.clone(), candidate.edge.clone())) + // .collect_vec(); + // + // let mut sorted_candidates = SortedVec::from(candidates); + // + // println!("TODO: print sorted candidates"); + // + // while entity_ids.len() < limit && sorted_candidates.len() > 0 { + // let ExpandCandidate { node, edge, .. } = sorted_candidates.pop().unwrap(); + // // we could terminate the loop instead I guess + // + // if !entity_ids.contains(&node) { + // entity_ids.push(node.clone()); + // } + // if !entity_ids.contains(&edge) { + // entity_ids.push(edge); + // } + // + // for new_candidate in self.get_candidates_from_node(&query_embedding, &node) { + // let already_candidate = || { + // sorted_candidates + // .iter() + // .any(|candidate| candidate.edge == new_candidate.edge) + // }; + // let already_selected = || entity_ids.iter().any(|id| id == &new_candidate.edge); + // if !already_selected() && !already_candidate() { + // sorted_candidates.insert(new_candidate); + // } + // } + // } + // + // entity_ids + // .iter() + // .take(limit) + // .map(|id| match id { + // EntityId::Node { id } => { + // self.graph + // .vertex(*id) + // .unwrap() + // .generate_doc(&self.node_template) + // .content + // } + // EntityId::Edge { src, dst } => { + // self.graph + // .edge(*src, *dst) + // .unwrap() + // .generate_doc(&self.edge_template) + // .content + // } + // }) + // .collect_vec() + // } + + // returns an iterator of triplets: (node id, edge id, score) as candidates to be included in entity_ids + // fn get_candidates_from_node<'a>( + // &'a self, + // query: &'a Embedding, + // node_id: &EntityId, + // ) -> impl Iterator + 'a { + // let vertex = self.graph.vertex(node_id.as_node()).unwrap(); + // let in_edges = vertex.in_edges().map(move |edge| ExpandCandidate { + // node: (&edge.src()).into(), + // edge: (&edge).into(), + // score: self.score_pair(&query, edge.src(), edge), + // }); + // let out_edges = vertex.out_edges().map(move |edge| ExpandCandidate { + // node: (&edge.dst()).into(), + // edge: (&edge).into(), + // score: self.score_pair(&query, edge.dst(), edge), + // }); + // chain!(in_edges, out_edges) + // } + + // fn score_pair(&self, query: &Embedding, node: VertexView, edge: EdgeView) -> f32 { + // let node_vector = self.node_embeddings.get(&(&node).into()).unwrap(); + // let node_similarity = cosine(query, node_vector); + // let edge_vector = self.edge_embeddings.get(&(&edge).into()).unwrap(); + // let edge_similarity = cosine(query, edge_vector); + // + // if node_similarity > edge_similarity { + // node_similarity + // } else { + // edge_similarity + // } + // } +} + +async fn generate_embeddings( + docs: I, + embedding: &Box, + cache_dir: &Path, +) -> HashMap +where + I: Iterator, +{ + // ----------------- SEQUENTIAL-ASYNC-VERSION ----------------- + // let mut embeddings = vec![]; + // let embedding_stream = stream! { + // for doc in docs { + // yield (doc.id, doc_to_vec(doc, cache_dir)) + // } + // }; + // pin_mut!(embedding_stream); + // while let Some(embedding) = embedding_stream.next() { + // embeddings.push(embedding); + // } + // ------------------------------------------------------------ + + let mut embeddings = HashMap::new(); + let mut misses = vec![]; + + for doc in docs { + match retrieve_embedding_from_cache(&doc, cache_dir) { + Some(embedding) => { + embeddings.insert(doc.id, embedding); + } + None => misses.push(doc), + } + } + + let embedding_tasks = misses + .chunks(CHUNK_SIZE) + .map(|chunk| compute_embeddings_with_cache(chunk.to_vec(), embedding, cache_dir)); + let computed_embeddings = join_all(embedding_tasks).await.into_iter().flatten(); + for (id, embedding) in computed_embeddings { + embeddings.insert(id, embedding); + } + + embeddings +} + +async fn compute_embeddings_with_cache( + docs: Vec, + embedding: &Box, + cache_dir: &Path, +) -> Vec<(EntityId, Embedding)> { + let texts = docs.iter().map(|doc| doc.content.clone()).collect_vec(); + let embeddings = embedding.call(texts).await; + docs.into_iter() + .zip(embeddings) + .map(|(doc, embedding)| { + let doc_hash = hash_doc(&doc); // FIXME: I'm hashing twice + let embedding_cache = EmbeddingCache { + doc_hash, + embedding, + }; + let doc_path = cache_dir.join(doc.id.to_string()); + let doc_file = + File::create(doc_path).expect("Couldn't create file to store embedding cache"); + let mut doc_writer = BufWriter::new(doc_file); + bincode::serialize_into(&mut doc_writer, &embedding_cache) + .expect("Couldn't serialize embedding cache"); + (doc.id, embedding_cache.embedding) + }) + .collect_vec() +} + +fn retrieve_embedding_from_cache(doc: &EntityDocument, cache_dir: &Path) -> Option { + let doc_path = cache_dir.join(doc.id.to_string()); + let doc_file = File::open(doc_path).ok()?; + let mut doc_reader = BufReader::new(doc_file); + let embedding_cache: EmbeddingCache = bincode::deserialize_from(&mut doc_reader).ok()?; + let doc_hash = hash_doc(doc); + if doc_hash == embedding_cache.doc_hash { + Some(embedding_cache.embedding) + } else { + None + } +} + +// fn find_top_k_old<'a>( +// query: &'a Embedding, +// entities: &'a HashMap, +// k: usize, +// ) -> impl Iterator { +// entities +// .iter() +// .map(|(id, embedding)| (id, cosine(query, embedding))) +// .sorted_by(|(_, d1), (_, d2)| d1.partial_cmp(d2).unwrap().reverse()) +// // We use reverse because default sorting is ascending but we want it descending +// .take(k) +// } + +fn score_entities<'a, I, E>( + query: &'a Embedding, + entities: I, +) -> impl Iterator + 'a +where + I: IntoIterator + 'a, + E: Borrow + 'a, +{ + entities + .into_iter() + .map(|(id, embedding)| (id, cosine(query, embedding))) +} + +/// Returns the top k nodes in descending order +fn find_top_k<'a, I, E>(entities: I, k: usize) -> impl Iterator + 'a +where + I: Iterator + 'a, + E: Borrow + 'a, +{ + entities + .sorted_by(|(_, d1), (_, d2)| d1.partial_cmp(d2).unwrap().reverse()) + // We use reverse because default sorting is ascending but we want it descending + .take(k) +} + +fn cosine(vector1: &Embedding, vector2: &Embedding) -> f32 { + assert_eq!(vector1.len(), vector2.len()); + + let dot_product: f32 = vector1.iter().zip(vector2.iter()).map(|(x, y)| x * y).sum(); + let x_length: f32 = vector1.iter().map(|x| x * x).sum(); + let y_length: f32 = vector2.iter().map(|y| y * y).sum(); + // TODO: store the length of the vector as well so we don't need to recompute it + // Vectors are already normalized for ada but nor for all the models: + // see: https://platform.openai.com/docs/guides/embeddings/which-distance-function-should-i-use + + dot_product / (x_length.sqrt() * y_length.sqrt()) + // dot_product + // TODO: assert that the result is between -1 and 1 +} + +#[derive(Clone)] +pub struct EntityDocument { + id: EntityId, + content: String, +} + +#[derive(Serialize, Deserialize)] +struct EmbeddingCache { + doc_hash: u64, + embedding: Embedding, +} + +pub type Embedding = Vec; + +// async fn compute_embeddings(texts: Vec) -> Vec { +// println!("computing embeddings for {} texts", texts.len()); +// Python::with_gil(|py| { +// let sentence_transformers = py.import("sentence_transformers")?; +// let locals = [("sentence_transformers", sentence_transformers)].into_py_dict(py); +// locals.set_item("texts", texts); +// +// let pyarray: &PyArray2 = py +// .eval( +// &format!( +// "sentence_transformers.SentenceTransformer('thenlper/gte-small').encode(texts)" +// ), +// Some(locals), +// None, +// )? +// .extract()?; +// +// let readonly = pyarray.readonly(); +// let chunks = readonly.as_slice().unwrap().chunks(384).into_iter(); +// let embeddings = chunks +// .map(|chunk| chunk.iter().copied().collect_vec()) +// .collect_vec(); +// +// Ok::>, Box>(embeddings) +// }) +// .unwrap() +// } + +fn hash_doc(doc: &EntityDocument) -> u64 { + let mut hasher = DefaultHasher::new(); + doc.content.hash(&mut hasher); + hasher.finish() +} + +impl Display for EntityId { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + EntityId::Node { id } => f.serialize_u64(*id), + EntityId::Edge { src, dst } => { + f.serialize_u64(*src) + .expect("src ID couldn't be serialized"); + f.write_str("-") + .expect("edge ID separator couldn't be serialized"); + f.serialize_u64(*dst) + } + } + } +} + +pub trait GraphEntity: Sized { + // fn entity_id(&self) -> EntityId; + fn generate_doc(&self, template: &T) -> EntityDocument + where + T: Fn(&Self) -> String; + + fn generate_property_list( + &self, + time_fmt: &F, + filter_out: Vec<&str>, + force_static: Vec<&str>, + ) -> String + where + F: Fn(i64) -> D, + D: Display; +} + +impl GraphEntity for VertexView { + #[allow(unstable_name_collisions)] // just update itertools when this is actually stabilised + fn generate_property_list( + &self, + time_fmt: &F, + filter_out: Vec<&str>, + force_static: Vec<&str>, + ) -> String + where + F: Fn(i64) -> D, + D: Display, + { + let time_fmt = |time: i64| time_fmt(time).to_string(); + let missing = || "missing".to_owned(); + let min_time_fmt = self.earliest_time().map(time_fmt).unwrap_or_else(missing); + let min_time = format!("earliest activity: {}", min_time_fmt); + let max_time_fmt = self.latest_time().map(time_fmt).unwrap_or_else(missing); + let max_time = format!("latest activity: {}", max_time_fmt); + + let temporal_props = self + .properties() + .temporal() + .iter() + .filter(|(key, _)| !filter_out.contains(&key.as_ref())) + .filter(|(key, _)| !force_static.contains(&key.as_ref())) + .filter(|(_, v)| { + // the history of the temporal prop has more than one value + v.values() + .into_iter() + .map(|prop| prop.to_string()) + .unique() + .collect_vec() + .len() + > 1 + }) + .collect_vec(); + + let temporal_keys: HashSet<_> = temporal_props.iter().map(|(key, _)| key).collect(); + let temporal_props = temporal_props.iter().map(|(key, value)| { + let time_value_pairs = value.iter().map(|(k, v)| (k, v.to_string())); + time_value_pairs + .unique_by(|(_, value)| value.clone()) + .map(|(time, value)| { + let time = time_fmt(time); + format!("{key} changed to {value} at {time}") + }) + .intersperse("\n".to_owned()) + .collect() + }); + + let prop_storage = self.properties(); + + let static_props = prop_storage + .keys() + .filter(|key| !filter_out.contains(&key.as_ref())) + .filter(|key| !temporal_keys.contains(key)) + .map(|key| { + let prop = prop_storage.get(&key).unwrap().to_string(); + let key = key.to_string(); + format!("{key}: {prop}") + }); + + let props = chain!(static_props, temporal_props).sorted_by(|a, b| a.len().cmp(&b.len())); + // We sort by length so when cutting out the tail of the document we don't remove small properties + + let lines = chain!([min_time, max_time], props); + lines.intersperse("\n".to_owned()).collect() + } + + fn generate_doc(&self, template: &T) -> EntityDocument + where + T: Fn(&Self) -> String, + { + let raw_content = template(self); + let content = match raw_content.char_indices().nth(1000) { + Some((index, _)) => (&raw_content[..index]).to_owned(), + None => raw_content, + }; + // TODO: allow multi document entities !!!!! + // shortened to 1000 (around 250 tokens) to avoid exceeding the max number of tokens, + // when embedding but also when inserting documents into prompts + + EntityDocument { + id: EntityId::Node { id: self.id() }, + content, + } + } +} + +impl GraphEntity for EdgeView { + fn generate_property_list( + &self, + _time_fmt: &F, + _filter_out: Vec<&str>, + _force_static: Vec<&str>, + ) -> String + where + F: Fn(i64) -> D, + D: Display, + { + // TODO: not needed yet + "".to_owned() + } + fn generate_doc(&self, template: &T) -> EntityDocument + where + T: Fn(&Self) -> String, + { + let content = template(self); + EntityDocument { + id: EntityId::Edge { + src: self.src().id(), + dst: self.dst().id(), + }, + content, + } + } +} + +// TODO: re-enable +// #[cfg(test)] +// mod vector_tests { +// use super::*; +// use crate::{ +// core::Prop, +// prelude::{AdditionOps, Graph}, +// }; +// use std::path::PathBuf; +// +// const NO_PROPS: [(&str, Prop); 0] = []; +// +// fn format_time(time: i64) -> String { +// format!("line {time}") +// } +// +// fn node_template(vertex: &VertexView) -> String { +// let name = vertex.name(); +// let node_type = vertex.properties().get("type").unwrap().to_string(); +// let property_list = +// vertex.generate_property_list(&format_time, vec!["type", "_id"], vec![]); +// format!("{name} is a {node_type} with the following details:\n{property_list}") +// } +// +// fn edge_template(edge: &EdgeView) -> String { +// let src = edge.src().name(); +// let dst = edge.dst().name(); +// let lines = edge.history().iter().join(","); +// format!("{src} appeared with {dst} in lines: {lines}") +// } +// +// // TODO: test default templates +// +// #[test] +// fn test_node_into_doc() { +// let g = Graph::new(); +// g.add_vertex( +// 0, +// "Frodo", +// [ +// ("type".to_string(), Prop::str("hobbit")), +// ("age".to_string(), Prop::str("30")), +// ], +// ) +// .unwrap(); +// +// let doc = g +// .vertex("Frodo") +// .unwrap() +// .generate_doc(&node_template) +// .content; +// let expected_doc = r###"Frodo is a hobbit with the following details: +// earliest activity: line 0 +// latest activity: line 0 +// age: 30"###; +// assert_eq!(doc, expected_doc); +// } +// +// #[test] +// fn test_edge_into_doc() { +// let g = Graph::new(); +// g.add_edge(0, "Frodo", "Gandalf", NO_PROPS, Some("talk to")) +// .unwrap(); +// +// let doc = g +// .edge("Frodo", "Gandalf") +// .unwrap() +// .generate_doc(&edge_template) +// .content; +// let expected_doc = "Frodo appeared with Gandalf in lines: 0"; +// assert_eq!(doc, expected_doc); +// } +// +// #[tokio::test] +// async fn test_vector_store() { +// let g = Graph::new(); +// g.add_vertex( +// 0, +// "Gandalf", +// [ +// ("type".to_string(), Prop::str("wizard")), +// ("age".to_string(), Prop::str("120")), +// ], +// ) +// .unwrap(); +// g.add_vertex( +// 0, +// "Frodo", +// [ +// ("type".to_string(), Prop::str("hobbit")), +// ("age".to_string(), Prop::str("30")), +// ], +// ) +// .unwrap(); +// g.add_edge(0, "Frodo", "Gandalf", NO_PROPS, Some("talk to")) +// .unwrap(); +// g.add_vertex( +// 2, +// "Aragorn", +// [ +// ("type".to_string(), Prop::str("human")), +// ("age".to_string(), Prop::str("40")), +// ], +// ) +// .unwrap(); +// +// dotenv().ok(); +// let vec_store = VectorStore::load_graph( +// g, +// &PathBuf::from("/tmp/raphtory/vector-cache-lotr-test"), +// Some(Box::new(node_template)), +// Some(Box::new(edge_template)), +// ) +// .await; +// +// let docs = vec_store +// .search("Find a magician", 1, 0, 0, 1, None, None) +// .await; +// assert!(docs[0].contains("Gandalf is a wizard")); +// +// let docs = vec_store +// .search("Find a young person", 1, 0, 0, 1, None, None) +// .await; +// assert!(docs[0].contains("Frodo is a hobbit")); // this fails when using gte-small +// +// // with window! +// let docs = vec_store +// .search("Find a young person", 1, 0, 0, 1, Some(1), Some(3)) +// .await; +// assert!(!docs[0].contains("Frodo is a hobbit")); // this fails when using gte-small +// +// let docs = vec_store +// .search( +// "Has anyone appeared with anyone else?", +// 1, +// 0, +// 0, +// 1, +// None, +// None, +// ) +// .await; +// assert!(docs[0].contains("Frodo appeared with Gandalf")); +// } +// +// fn average_vectors(vec1: &Embedding, vec2: &Embedding) -> Embedding { +// vec1.iter() +// .zip(vec2) +// .map(|(a, b)| (a + b) / 2.0) +// .collect_vec() +// } +// +// #[tokio::test] +// async fn test_combinations() { +// dotenv().ok(); +// // I want to test if a document tuple node-edge can rank higher than +// +// let ticket = "DEV-1303 is an issue created by the Pometry team with the following details:\nearliest activity: 1667924841177\nlatest activity: 1676301689177\n_id: DEV-1303\nname: DEV-1303\njira_id: 12212\npriority: Medium\nresolution: Done\nstatus: CANCELLED\njira_url: https://pometry.atlassian.net/rest/agile/1.0/issue/12212\nsummary: Build ReadTheDocs during CI/CD as a Test to ensure it still works\ndescription: {panel:bgColor=#eae6ff}\nRemove me and Insert *what* needs to be done and *why* it needs to be done\n{panel}\n\nThis must replicate the read the docs build process. "; +// let edge = +// "Pedro Rico Pinazo was assigned to work on issue DEV-1303 at time: 2022-06-29 12:34:15"; +// let question = "tell me about someone that has been working on documentation"; +// +// let ticket_embedding = compute_embeddings(vec![ticket.to_owned()]).await.remove(0); +// let edge_embedding = compute_embeddings(vec![edge.to_owned()]).await.remove(0); +// let question_embedding = compute_embeddings(vec![question.to_owned()]) +// .await +// .remove(0); +// let comb_embedding = average_vectors(&ticket_embedding, &edge_embedding); +// +// let ticket_score = cosine(&question_embedding, &ticket_embedding); +// let edge_score = cosine(&question_embedding, &edge_embedding); +// let comb_score = cosine(&question_embedding, &comb_embedding); +// +// dbg!(ticket_score); +// dbg!(edge_score); +// dbg!(comb_score); +// } +// } diff --git a/resource/graphql-demo.jpg b/resource/graphql-demo.jpg new file mode 100644 index 0000000000..3049a045e3 Binary files /dev/null and b/resource/graphql-demo.jpg differ