diff --git a/.github/gh-disabled-workflows/python-publish-test.yml b/.github/gh-disabled-workflows/python-publish-test.yml index 17ead9c..c67e4aa 100644 --- a/.github/gh-disabled-workflows/python-publish-test.yml +++ b/.github/gh-disabled-workflows/python-publish-test.yml @@ -17,7 +17,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: '3.8' + python-version: '3.12' - name: Install dependencies run: | python -m pip install --upgrade pip diff --git a/.github/workflows/directory_writer.yml b/.github/workflows/directory_writer.yml index b469e47..d164300 100644 --- a/.github/workflows/directory_writer.yml +++ b/.github/workflows/directory_writer.yml @@ -14,10 +14,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 with: - python-version: 3.8 + python-version: 3.12 - name: Write DIRECTORY.md run: | @@ -27,7 +27,7 @@ jobs: uses: EndBug/add-and-commit@v4 with: author_name: Martin Röbke - author_email: martin.roebke@tu-dresden.de + author_email: martin.roebke@web.de message: "Automatic Update: DIRECTORY.md" add: "DIRECTORY.md" env: diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index 4ed47f0..7bf067d 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -5,64 +5,93 @@ name: Tests on: push: - branches: [ master ] + branches: [master] paths-ignore: - - 'doc/**' - - 'gh-disabled-workflows/**' - - '*.md' + - "doc/**" + - "gh-disabled-workflows/**" + - "*.md" pull_request: - branches: [ master ] + branches: [master] paths-ignore: - - 'doc/**' - - 'gh-disabled-workflows/**' - - '*.md' + - "doc/**" + - "gh-disabled-workflows/**" + - "*.md" + +env: + graphviz-version: "12.2.1" + pg-version: "16" jobs: linux_and_coverage: - runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.8", "3.12"] steps: - - uses: actions/checkout@v4 - - - name: Set up Python 3.8 - uses: actions/setup-python@v2 - with: - python-version: 3.8 - - - name: Install Workflow dependencies - run: | - python -m pip install --upgrade pip - pip install wheel - pip install flake8 flake8-import-order pytest coverage - - - name: Install project dependencies - run: | - pip install .[test] - - - name: Lint with flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - - name: graphviz for visualization test - run: | - sudo apt-get update && sudo apt-get install -yq graphviz - - - name: Test with coverage & pytest - run: | - coverage run --source=tdvisu -m pytest . - coverage xml - - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - file: ./coverage.xml - flags: unittests - + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Get Graphviz + working-directory: .. + run: | + wget -nv "https://gitlab.com/api/v4/projects/4207231/packages/generic/graphviz-releases/${{ env.graphviz-version }}/graphviz-${{ env.graphviz-version }}.tar.gz" + graphviz_12_sha256="242bc18942eebda6db4039f108f387ec97856fc91ba47f21e89341c34b554df8 graphviz-${{ env.graphviz-version }}.tar.gz" + if [[ "$(sha256sum graphviz-${{ env.graphviz-version }}.tar.gz)" != "$graphviz_12_sha256" ]]; then + echo "The file is corrupted, calculated_hash: $calculated_hash" + exit 1 + fi + # extract + tar -xzf graphviz-${{ env.graphviz-version }}.tar.gz + # dev dependencies + sudo apt-get install -y build-essential pkg-config libgraphviz-dev libperl-dev libsodium-dev libargon2-dev libgts-dev + + - name: Install Graphviz + working-directory: .. + run: | + cd graphviz-${{ env.graphviz-version }} + ./configure --prefix=$PWD/out --with-gts + make --quiet + make install --quiet + echo "$PWD/out/bin" >> "$GITHUB_PATH" + echo "LD_LIBRARY_PATH=$PWD/out/lib" >> "$GITHUB_ENV" + + - name: Install Workflow dependencies + run: | + python -m pip install --upgrade pip + pip install wheel + pip install flake8 flake8-import-order pytest coverage + + - name: Install project dependencies + run: | + pip install .[test] + + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + + - name: Test with coverage & pytest + run: | + dot -V + which coverage + which dot + which pytest + coverage run --source=tdvisu -m pytest . + coverage xml + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./coverage.xml + flags: unittests other_os_tests: strategy: @@ -73,61 +102,52 @@ jobs: continue-on-error: true steps: - - uses: actions/checkout@v4 - - - name: Set up Python 3.8 - uses: actions/setup-python@v2 - with: - python-version: 3.8 - - - name: Install Workflow dependencies - run: | - python -m pip install --upgrade pip - pip install wheel - - - name: Install graphviz for visualization test - if: ${{ runner.os == 'macOS' }} - uses: ts-graphviz/setup-graphviz@v2 - with: - macos-skip-brew-update: 'true' # default false - - - name: Setup Conda - if: ${{ runner.os == 'Windows' }} - uses: s-weigand/setup-conda@v1 - with: - # Whether to activate the conda base env (Default: 'true') - activate-conda: true - # Python version which should be installed with conda (default: 'Default') - python-version: 3.8 - - - name: Install graphviz via conda - if: ${{ runner.os == 'Windows' }} - run: | - conda install -c anaconda graphviz - - - name: add graphviz PATH - if: ${{ runner.os == 'Windows' }} - run: | - # should give base conda python - $python=Get-Command python - $pypath=Split-Path $python.Source -Parent - # Add to PATH, we know pypath is on it - $dot=[IO.Path]::Combine($pypath, 'Library', 'bin', 'graphviz') - echo $dot | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append - shell: powershell - - - name: register plugins for graphviz - if: ${{ runner.os == 'Windows' }} - run: | - # execute dot - dot -V - dot -c - - - name: Install project dependencies - run: | - pip install .[test] - - - name: Test with pytest - run: | - # Tests reside in folder 'test' - pytest ./test + - uses: actions/checkout@v4 + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: 3.12 + + - name: Install workflow dependencies + run: | + python -m pip install --upgrade pip + pip install wheel + + - name: Brew install PostgreSQL ${{ env.pg-version }} + if: runner.os == 'MacOS' + run: | + brew install libpq postgresql@${{ env.pg-version }} + # make available on PATH + pgpath="/opt/homebrew/opt/postgresql@${{ env.pg-version }}/bin" + echo $pgpath >> "$GITHUB_PATH" + ls "$pgpath" + $pgpath/pg_config --version + + - name: Add PostgreSQL to PATH + if: runner.os == 'Windows' + shell: pwsh + run: | + echo "$env:PGBIN" >> "$env:GITHUB_PATH" + pg_config --version + + - name: Setup Graphviz + uses: ts-graphviz/setup-graphviz@v2 + with: + macos-skip-brew-update: "true" # default false + windows-graphviz-version: ${{ env.graphviz-version }} + + - name: Register plugins for Graphviz + if: runner.os == 'Windows' + run: | + dot -V + dot -c + + - name: Install project dependencies + run: | + pip install .[test] + + - name: Test with pytest + run: | + # Tests reside in folder 'test' + pytest ./test diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index afe077d..f58e6af 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -9,23 +9,22 @@ on: jobs: deploy: - runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Set up Python - uses: actions/setup-python@v2 - with: - python-version: '3.8' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install setuptools wheel twine - - name: Build and publish - env: - TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} - TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} - run: | - python setup.py sdist bdist_wheel - twine upload dist/* + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install 'build[virtualenv]' + - name: Build and publish + env: + TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + python -m build + twine upload dist/* diff --git a/CHANGELOG.md b/CHANGELOG.md index 5abb84a..4cdcde4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,89 +2,140 @@ All notable changes to this project will be documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/ ), and this -project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html ). +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this +project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] + - No unreleased changes yet. +## [1.2.0] - 2024-12-24 + +### Added + +- Testsetup with python-version: `3.12` while maintaining `3.8 compatibility` +- Specifying `psycopg[c]` for distribution: https://www.psycopg.org/psycopg3/docs/basic/install.html#local-installation + See [Installation](https://www.psycopg.org/psycopg3/docs/basic/install.html#installation) for the other options. +- Several improvements and specifications to `.github\workflows\python-app.yml` + +### Changed + +See [#47] + +- Updated GitHub Actions [#48] + - Replacing installation of `setuptools` with + > pip install 'build[virtualenv]' + - Replacing old `python setup.py sdist bdist_wheel` with + > python -m build +- Updated `author_email` to +- Update and format `setup.py` +- Updated tests for graphviz-version: `12.2.1` from usually version `2` before. Changed the output files slightly with the test settings. They look a bit cleaner, see also their https://gitlab.com/graphviz/graphviz/-/blob/main/CHANGELOG.md +- Upgrade code for + - [psycopg](https://pypi.org/project/psycopg)==3.2.3 \ + Update _wait for good connection_, replacing the `good_db_status()` method with **pg.pq.ConnStatus.OK** + - [pytest](https://pypi.org/project/pytest)==8.3.4 +- Updated requirements.txt +- Updated stable-requirements.txt + ## [1.1.9] - 2023-07-27 ### Added + - `python-benedict[xml]` to dependencies + ### Changed + - Updated requirements.txt - Updated stable-requirements.txt - ## [1.1.8] - 2021-05-04 ### Changed + - Updated pyyaml from 5.3.1 to 5.4 [#33] - Updated py from 1.9.0 to 1.10.0 [#35] - Fixed python-app.yml [#34] ## [1.1.7] - 2020-09-13 + ### Added + - Better tests around the (Dimacs) Reader ### Changed + - Improved the SQL queries with explicit formats [#32] - Improved Readme sections for install and usage ## [1.1.6] - 2020-08-01 + ### Added -- Added better property based testing with *hypothesis* [#29] + +- Added better property based testing with _hypothesis_ [#29] - Added jobs and setups to test on macos and windows [#31] ### Changed -- *do_sort_nodes* now sorts in correct numeric order. [commit cdfcf6](https://github.com/VaeterchenFrost/tdvisu/commit/cdfcf6c332a63f05b499fe133fada4473ad7524c ) + +- _do_sort_nodes_ now sorts in correct numeric order. [commit cdfcf6](https://github.com/VaeterchenFrost/tdvisu/commit/cdfcf6c332a63f05b499fe133fada4473ad7524c) - Fixed some import orders ## [1.1.5] - 2020-07-17 + ### Added + - Added many new tests. - Hints for not covered code lines. ### Changed + - Simplified code to parse commandline flags while removing duplications in code. - Entrypoint for modules visualization and construct_dpdb_visu is now in init(). - Some improvements in utilities.solution_node ## [1.1.4] - 2020-07-14 + ### Added + - Added the possibility to specify and create multiple graph-visualizations in one file [#25] -- Added test case *test_vc_multiple_and_join* in [commit aa31901](https://github.com/VaeterchenFrost/tdvisu/commit/aa319016ac71f9a54023474bf820cb30929c52a8 ) -- Added test cases for [construct_dpdb_visu](https://github.com/VaeterchenFrost/tdvisu/blob/master/test/test_construct_dpdb.py ) +- Added test case _test_vc_multiple_and_join_ in [commit aa31901](https://github.com/VaeterchenFrost/tdvisu/commit/aa319016ac71f9a54023474bf820cb30929c52a8) +- Added test cases for [construct_dpdb_visu](https://github.com/VaeterchenFrost/tdvisu/blob/master/test/test_construct_dpdb.py) - Add pytest-mock to tests_require ### Changed -- Simplified and refactored TDVisu.schema.json -- Schema now includes possibility to specify multiple instances of generalGraph and incidenceGraph + +- Simplified and refactored TDVisu.schema.json +- Schema now includes possibility to specify multiple instances of generalGraph and incidenceGraph - Revisited doc/JsonAPI.md to now include all parameters available - Renamed test folder expected_images to expected_files - Updated stable-requirements.txt - Several minor improvements ## [1.1.3] - 2020-07-09 + ### Added -- Added *TDVisu.schema.json* to validate the Json-API for TDVisu [#24] + +- Added _TDVisu.schema.json_ to validate the Json-API for TDVisu [#24] - Added 'col' to allowed formats in tw reader (default string in Mathematica) ### Changed + - Fixed error where database configuration was not found in the directory - Fixed missing double quotation marks in JsonAPI.md -- Moved JsonAPI.md → *doc/JsonAPI.md* +- Moved JsonAPI.md → _doc/JsonAPI.md_ ### Removed -- Removed *generalGraph* and *incidenceGraph* from required arguments in API + +- Removed _generalGraph_ and _incidenceGraph_ from required arguments in API ## [1.1.2] - 2020-06-26 + ### Added + - Tests for visualization.py using graphviz in the Github Action too - Tests for reader.py ### Changed + - Fixed typo that prevented joining SVG in visualization - Fixed cases where logging.yml was not found in the working directory - Now using pathlib.Path for most file-related operations @@ -92,80 +143,98 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html ). - Added more type hints and improved existing ones ## [1.1.1] - 2020-06-25 + ### Added -- Added problem type **Sat** to tdvisu/construct_dpdb_visu.py + +- Added problem type **Sat** to tdvisu/construct_dpdb_visu.py - Added testcases in file test/test_dijkstra.py ### Changed -- JsonAPI.md is now updated with snake_case names and consistent with visualization_data.py -- Fixed default value for svg-join **v_top** to *None* from *'top'* + +- JsonAPI.md is now updated with snake_case names and consistent with visualization_data.py +- Fixed default value for svg-join **v_top** to _None_ from _'top'_ - Improved flexibility in several function parameters - Improved documentation and comments in several places -- Fixed passing parameters to method *setup_tree_dec_graph* +- Fixed passing parameters to method _setup_tree_dec_graph_ ### Removed -- Removed old dependency from tdvisu/dijkstra.py on utilities + +- Removed old dependency from tdvisu/dijkstra.py on utilities ## [1.1.0] - 2020-06-07 + ### Added + - Added file utilities.py with several static or shared things like - - Constants: CFG_EXT, LOGLEVEL_EPILOG, DEFAULT_LOGGING_CFG - - Methods: - - flatten - - read_yml_or_cfg combining yaml, json, cfg reader in one - - logging_cfg configure logging with file or DEFAULT_LOGGING_CFG - - helper convert_to_adj from dijkstra.py - - add_edge_to (edges and adj list) - - gen_arg infinite Generator - - Styles: - - base_style, emphasise_node, style_hide_node, style_hide_edge - - Graph manipulation: - - bag_node - - solution_node + + - Constants: CFG_EXT, LOGLEVEL_EPILOG, DEFAULT_LOGGING_CFG + - Methods: + - flatten + - read_yml_or_cfg combining yaml, json, cfg reader in one + - logging_cfg configure logging with file or DEFAULT_LOGGING_CFG + - helper convert_to_adj from dijkstra.py + - add_edge_to (edges and adj list) + - gen_arg infinite Generator + - Styles: + - base_style, emphasise_node, style_hide_node, style_hide_edge + - Graph manipulation: + - bag_node + - solution_node - Added file logging.yml (and .ini) with logging configuration for the module [#20] - Added half the tests for utilities.py ### Changed + - Changed path of image SharpSatExample to the absolute URL for [PyPI]. - Changed names of loggers to absolute name. Should be easy to adjust if needed. - Changed logging defaults and config in tdvisu/visualization.py and construct_dpdb_visu.py - Updated ArgumentParser help -- Some fixes of code-style or variable names. +- Some fixes of code-style or variable names. ## [1.0.1] - 2020-06-04 + ### Added + - Codecoverage with [Codecov] ### Changed + - Changed path of image SharpSatExample to the absolute URL for [PyPI]. ## [1.0.0] - 2020-06-04 + ### Added + - Added svgjoin parameters to JsonAPI [#6] - Added call to svgjoin from visualization.py - Added workflow to display the sourcecode-files in [DIRECTORY] -### Changed +### Changed + - Moved JsonAPI and conda_packages to /doc - Updated arguments in svgjoin to be more flexible for multiple joins [#11] - Fixed scaling mechanism in svgjoin [#13] - Changed tests from unittest to pytest [#12] ### Removed + - Changelog in JsonAPI.md ## [0.5.1] - 2020-06-01 + ### Added + - Added publishing Action to [PyPI] [#4] ### Changed + - Changed setup.py with more documentation and simpler functionality. - Updated Readme with a guide on how to use construct_dpdb_visu [#2] ### Removed -- Removed publishing Action to testpypi [#4] +- Removed publishing Action to testpypi [#4] ## [0.5.0-dev1] - 2020-06-01 @@ -177,10 +246,12 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html ). - Added README to tdvisu directly ### Changed + - Fixed usage of `__version__` in tdvisu/construct_dpdb_visu.py ### Removed -- Removed individual versioning + +- Removed individual versioning [#1]: https://github.com/VaeterchenFrost/tdvisu/issues/1 [#2]: https://github.com/VaeterchenFrost/tdvisu/issues/2 @@ -199,15 +270,16 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html ). [#33]: https://github.com/VaeterchenFrost/tdvisu/pull/33 [#34]: https://github.com/VaeterchenFrost/tdvisu/pull/34 [#35]: https://github.com/VaeterchenFrost/tdvisu/pull/35 - +[#47]: https://github.com/VaeterchenFrost/tdvisu/pull/47 +[#48]: https://github.com/VaeterchenFrost/tdvisu/issues/48 [@VaeterchenFrost]: https://github.com/VaeterchenFrost [PyPI]: https://pypi.org/project/tdvisu/ [mypy]: https://github.com/python/mypy [DIRECTORY]: https://github.com/VaeterchenFrost/tdvisu/blob/master/DIRECTORY.md [Codecov]: https://codecov.io/gh/VaeterchenFrost/tdvisu - -[Unreleased]: https://github.com/VaeterchenFrost/tdvisu/compare/v1.1.9...master -[1.1.8]: https://github.com/VaeterchenFrost/tdvisu/releases/tag/v1.1.9 +[Unreleased]: https://github.com/VaeterchenFrost/tdvisu/compare/v1.2.0...master +[1.2.0]: https://github.com/VaeterchenFrost/tdvisu/releases/tag/v1.2.0 +[1.1.9]: https://github.com/VaeterchenFrost/tdvisu/releases/tag/v1.1.9 [1.1.8]: https://github.com/VaeterchenFrost/tdvisu/releases/tag/v1.1.8 [1.1.7]: https://github.com/VaeterchenFrost/tdvisu/releases/tag/v1.1.7 [1.1.6]: https://github.com/VaeterchenFrost/tdvisu/releases/tag/v1.1.6 diff --git a/README.md b/README.md index dbdfc21..2f724ef 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,19 @@ # TdVisu + ![Maintenance](https://img.shields.io/badge/Maintained%3F-yes-green.svg) [![PyPI license](https://img.shields.io/pypi/l/tdvisu.svg)](https://pypi.python.org/pypi/tdvisu/) -![Tests](https://github.com/VaeterchenFrost/tdvisu/workflows/Tests/badge.svg) +![Tests](https://github.com/VaeterchenFrost/tdvisu/workflows/Tests/badge.svg) [![codecov](https://codecov.io/gh/VaeterchenFrost/tdvisu/branch/master/graph/badge.svg)](https://codecov.io/gh/VaeterchenFrost/tdvisu) - ![GitHub release (latest SemVer including pre-releases)](https://img.shields.io/github/v/release/vaeterchenfrost/tdvisu?include_prereleases) [![PyPI version fury.io](https://badge.fury.io/py/tdvisu.svg)](https://pypi.python.org/pypi/tdvisu/) +![GitHub commits since latest release (by SemVer)](https://img.shields.io/github/commits-since/VaeterchenFrost/tdvisu/latest) [![PyPI status](https://img.shields.io/pypi/status/tdvisu.svg)](https://pypi.python.org/pypi/tdvisu/) [![PyPI pyversions](https://img.shields.io/pypi/pyversions/tdvisu.svg)](https://pypi.python.org/pypi/tdvisu/) ![PyPI - Wheel](https://img.shields.io/pypi/wheel/tdvisu) -![GitHub repo size](https://img.shields.io/github/repo-size/VaeterchenFrost/tdvisu) ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/VaeterchenFrost/tdvisu) -![GitHub commits since latest release (by SemVer)](https://img.shields.io/github/commits-since/VaeterchenFrost/tdvisu/latest) ----------- +--- Visualization for [dynamic programming](https://en.wikipedia.org/wiki/Dynamic_programming) on [tree decompositions](https://en.wikipedia.org/wiki/Tree_decomposition). @@ -28,55 +27,69 @@ For the portable and light weight '.svg' format, all graphs for a timestep can b With the '.svg' format the images are highly customizable, and even combining several timesteps together using svg [animate](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/animate) would be an option in the future. ----------- +--- # Using -[Alubbock:Graphviz](https://anaconda.org/alubbock/graphviz) (or [Graphviz (>=2.38)](https://graphviz.gitlab.io/download/)) -[python-benedict](https://pypi.org/project/python-benedict/) +> Note: see also the steps prepared in the CI/CD [.github/workflows/python-app.yml](https://github.com/VaeterchenFrost/tdvisu/actions/workflows/python-app.yml): + +[Graphviz (>=2.38)](https://graphviz.gitlab.io/download/). Be aware of changes in default layouts over different major versions of Graphviz. The project currently tests with `graphviz-version: "12.2.1"`. -[for dpdb: psycopg2 (2.8.5)](https://www.psycopg.org/docs/index.html) +[python-benedict[xml]](https://pypi.org/project/python-benedict/) ----------- +PostgreSQL adapter for Python: [psycopg (3)](https://www.psycopg.org/docs/index.html) + +--- # To register the graphviz plugins + https://gitlab.com/graphviz/graphviz/-/issues/1352 + ```shell dot.exe -c ``` # To install -In a command prompt with pip (to get *pip* see: https://pip.pypa.io/en/stable/) installed: +In a command prompt with pip (to get _pip_ see: https://pip.pypa.io/en/stable/) installed: Just run + ```shell pip install -h (for more information on install options) pip install tdvisu ``` To download the latest version from the default branch: + ```shell git clone --depth 1 --single https://github.com/VaeterchenFrost/tdvisu ``` # To isolate the dependencies -With [virtualenv](https://virtualenv.pypa.io/en/latest/installation.html ) on the system installed you can isolate the environment, for example +With [virtualenv](https://virtualenv.pypa.io/en/latest/installation.html) on the system installed you can isolate the environment, for example + ```shell -virtualenv tdvisu_dir -p 3.8 +virtualenv tdvisu_dir -p 3.12 cd tdvisu_dir/bin/ source activate +# Windows: ./tdvisu_dir/Scripts/activate ``` + With [Conda](https://docs.conda.io/en/latest/) on the system installed the dependencies for this project can be automatically installed in a new environment: Go to the projects base directory. -Open a *conda-command-prompt* with admin privileges and run the commands from the project folder +Open a _conda-command-prompt_ with admin privileges and run the commands from the project folder + - to create a new environment with basic dependencies: + ```shell -conda env create -f .\environment.yml +conda env create -f ./environment.yml ``` + - to activate the environment: + ```shell conda activate tdvisu ``` @@ -84,117 +97,130 @@ conda activate tdvisu # Install from source To clone the complete repository: + ```shell git clone https://github.com/VaeterchenFrost/tdvisu ``` To download only the latest version from the default branch: + ```shell git clone --depth 1 --single https://github.com/VaeterchenFrost/tdvisu ``` To install the project from the source folder: + ```shell pip install -h (for more information on install options) pip install . ``` + to confirm that the visualization finds all dependencies: + ```shell -python .\tdvisu\visualization.py -h +python ./tdvisu/visualization.py -h ``` + to run all tests: + ```shell -pip install .[test] -pytest .\test\ +pip install .[test] +pytest ./test/ ``` ----------- +--- # How to use The visualization needs input in the form of the [Json API](https://github.com/VaeterchenFrost/gpusat-VISU/blob/master/JsonAPI_v1.3.md). -The creation of this file is exemplary implemented in *construct_dpdb_visu.py* or the fork [GPUSAT](https://github.com/VaeterchenFrost/GPUSAT) and *--visufile filename* (optionally disabling preprocessing with *-p*). +The creation of this file is exemplary implemented in _construct_dpdb_visu.py_ or the fork [GPUSAT](https://github.com/VaeterchenFrost/GPUSAT) and _--visufile filename_ (optionally disabling preprocessing with _-p_). Run the python file with the above dependencies installed: [visualization.py](https://github.com/VaeterchenFrost/gpusat-VISU/blob/master/satvisualization_repo/satvisu/visualization.py) **visualization.py** takes two parameters, the json-**infile** to read from, and optionally one **outputfolder**. -With both arguments a call from IPython might look like this: +With both arguments a run might look like this: -```python -runfile('visualization.py', -args='visugpusat.json examplefolder') +```shell +python tdvisu/visualization.py visugpusat.json examplefolder ``` For #SAT it produces for example three different graphs suffixed with a running integer to represent timesteps: -+ *TDStep* the tree decomposition with solved nodes -+ *PrimalGraphStep* the primal graph with currently active variables highlighted -+ *IncidenceGraphStep* the bipartite incidence graph with active clauses/variables highlighted +- _TDStep_ the tree decomposition with solved nodes +- _PrimalGraphStep_ the primal graph with currently active variables highlighted +- _IncidenceGraphStep_ the bipartite incidence graph with active clauses/variables highlighted The graphs are images encoded in resolution independent **.svg files** (see https://www.lifewire.com/svg-file-4120603)

## How to use construct_dpdb_visu.py + After installing the project [dp_on_dbs](https://github.com/hmarkus/dp_on_dbs) with the there listed [requirements](https://github.com/hmarkus/dp_on_dbs#requirements), we need to + - edit the [database.ini](https://github.com/VaeterchenFrost/tdvisu/blob/master/tdvisu/database.ini) with our password to [postgresql](https://www.postgresql.org/) - Solve a problem with `python dpdb.py [GENERAL-OPTIONS] -f [PROBLEM-SPECIFIC-OPTIONS]` - - for the problem **VertexCover** + - for the problem **VertexCover** - with flag `--gr-file` to store the htd Input (if the input was in a different format) - for the problem **SharpSat** - with flag `--store-formula` to store the formula in the database -- Run +- Run - **Sat** / **SharpSat**: `python construct_dpdb_visu.py [PROBLEMNUMBER]` - - **VertexCover**: `python construct_dpdb_visu.py [PROBLEMNUMBER] --twfile [TWFILE]` - with the file in DIMACS tw-format containing the edges of the graph. + - **VertexCover**: `python construct_dpdb_visu.py [PROBLEMNUMBER] --twfile [TWFILE]` + with the file in DIMACS tw-format containing the edges of the graph. -# Installation of the psycopg2 package +# Installation of the psycopg package -See https://www.psycopg.org/docs/install.html#build-prerequisites +See https://www.psycopg.org/psycopg3/docs/basic/install.html -For example on linux today this might need -`sudo apt install libpq-dev` -before completion. +**Note** Whatever version of `libpq` psycopg is compiled with, it will be possible to connect to PostgreSQL servers of any [supported version](https://www.psycopg.org/docs/install.html#prerequisites): just install the most recent libpq version or the most practical, without trying to match it to the version of the PostgreSQL server you will have to connect to. ------------------ +--- # New Release ## Version + - Bump `/version.py` according to the changes made -- Change date to the release day, keep format +- Change date to the release day, keep format ## Requirements + In case dependencies have changed, or just to update some, check -- *requirements.txt* -- *stable-requirements.txt* (using `pip freeze`) -- *setup.py* + +- _requirements.txt_ +- _stable-requirements.txt_ (using `pip freeze`) +- _setup.py_ ## Write Changelog.md + - Add tag with link (see bottom for linking examples) -- Add changes, maybe some are already in *Unreleased* -- Update *Unreleased* with **(No) unreleased changes** +- Add changes, maybe some are already in _Unreleased_ +- Update _Unreleased_ with **(No) unreleased changes** ## Review code + - Run tests (pytest) - Check codestyle (pylint) ## Push + - Push changes to master - Wait for all automated checks! (All checks have passed...) ## Create Release -- On the GitHub page go to: Release, **[Draft a new release](https://github.com/VaeterchenFrost/tdvisu/releases/new )** + +- On the GitHub page go to: Release, **[Draft a new release](https://github.com/VaeterchenFrost/tdvisu/releases/new)** - Enter v'YOUR VERSION NUMBER' as the tag. - Add a **Release Title** (could be just the version) - Add some description (like in the CHANGELOG.md) - Click on **Publish release** on the bottom -## Should automatically release to [PyPI](https://pypi.org/project/tdvisu/ ) -- For details see: [Upload Python Package](https://github.com/VaeterchenFrost/tdvisu/blob/master/.github/workflows/python-publish.yml ) +## Should automatically release to [PyPI](https://pypi.org/project/tdvisu/) -**Now you are set for the new release :tada:** +- For details see: [Upload Python Package](https://github.com/VaeterchenFrost/tdvisu/blob/master/.github/workflows/python-publish.yml) ----------- +**Now you are set for the new release :tada:** +--- \ No newline at end of file diff --git a/environment.yml b/environment.yml index 7d705e8..b22d16c 100644 --- a/environment.yml +++ b/environment.yml @@ -1,8 +1,6 @@ name: tdvisu channels: - - alubbock - defaults dependencies: - python>=3.8 - - graphviz - setuptools diff --git a/requirements.txt b/requirements.txt index 7e53bb8..ea8c428 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,8 @@ graphviz~=0.20 -hypothesis~=6.82.0 -psycopg2-binary~=2.9.6 -python-benedict~=0.32.0 -python-benedict[xml]~=0.32.0 -pytest~=7.4.0 -pytest-mock~=3.11.0 +hypothesis~=6.122 +psycopg~=3.2.3 +pytest-mock~=3.14 +pytest~=8.3 +python-benedict[xml]~=0.34.0 PyYAML~=6.0 +xmltodict~=0.14.2 diff --git a/scripts/build_directory_md.py b/scripts/build_directory_md.py index ea2ceed..562bd4c 100644 --- a/scripts/build_directory_md.py +++ b/scripts/build_directory_md.py @@ -32,22 +32,24 @@ URL_BASE = "https://github.com/VaeterchenFrost/tdvisu/blob/master" -AFFECTED_EXT = ('.py', '.ipynb',) +AFFECTED_EXT = ( + ".py", + ".ipynb", +) -EXCLUDED_FILENAMES = ('__init__.py',) +EXCLUDED_FILENAMES = ("__init__.py",) -def good_file_paths(top_dir: str = '.') -> Iterator[str]: +def good_file_paths(top_dir: str = ".") -> Iterator[str]: """Return relative path to files with extension in AFFECTED_EXT.""" for dir_path, dir_names, filenames in os.walk(top_dir): - dir_names[:] = [d for d in dir_names - if d != 'scripts' and d[0] not in '._'] + dir_names[:] = [d for d in dir_names if d != "scripts" and d[0] not in "._"] for filename in filenames: if filename in EXCLUDED_FILENAMES: continue if os.path.splitext(filename)[1] in AFFECTED_EXT: normalized_path = os.path.normpath(dir_path) - if normalized_path != '.': + if normalized_path != ".": yield os.path.join(normalized_path, filename) else: yield filename @@ -68,19 +70,20 @@ def print_path(old_path: str, new_path: str) -> str: return new_path -def print_directory_md(top_dir: str = '.') -> None: +def print_directory_md(top_dir: str = ".") -> None: """Print the markdown for files with selected extensions recursing top_dir.""" - old_path = '' + old_path = "" for filepath in sorted(good_file_paths(top_dir)): filepath, filename = os.path.split(filepath) if filepath != old_path: old_path = print_path(old_path, filepath) indent = (filepath.count(os.sep) + 1) if filepath else 0 - url = '/'.join((URL_BASE, *[quote(part) - for part in (filepath, filename) if part])) - filename = os.path.splitext(filename.replace('_', ' ').title())[0] + url = "/".join( + (URL_BASE, *[quote(part) for part in (filepath, filename) if part]) + ) + filename = os.path.splitext(filename.replace("_", " ").title())[0] print(f"{md_prefix(indent)} [{filename}]({url})") if __name__ == "__main__": - print_directory_md('.') + print_directory_md(".") diff --git a/setup.py b/setup.py index fc50a90..69a2311 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ def read_files(files, delim: str = "\n") -> str: data = [] try: for file in files: - with open(file, encoding='utf-8') as handle: + with open(file, encoding="utf-8") as handle: data.append(handle.read()) except IOError: pass @@ -37,35 +37,43 @@ def read_files(files, delim: str = "\n") -> str: description = "Visualizing Dynamic Programming on Tree Decompositions." -long_description = read_files(['README.md', 'CHANGELOG.md']) +long_description = read_files(["README.md", "CHANGELOG.md"]) classifiers = [ - 'Development Status :: 4 - Beta', - 'Environment :: Console', - 'Intended Audience :: Science/Research', - 'Intended Audience :: Education', - 'Intended Audience :: Developers', - 'Operating System :: OS Independent', - 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.8', - 'Topic :: Scientific/Engineering :: Visualization', - 'Topic :: Multimedia :: Graphics :: Presentation'] + "Development Status :: 4 - Beta", + "Environment :: Console", + "Intended Audience :: Science/Research", + "Intended Audience :: Education", + "Intended Audience :: Developers", + "Operating System :: OS Independent", + "License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.12", + "Topic :: Scientific/Engineering :: Visualization", + "Topic :: Multimedia :: Graphics :: Presentation", +] -tests_require = ['hypothesis', 'pytest', 'pytest-mock'] +tests_require = ["hypothesis", "pytest", "pytest-mock"] -setup(name="tdvisu", - version=version, - description=description, - long_description=long_description, - long_description_content_type='text/markdown', - url="https://github.com/VaeterchenFrost/tdvisu", - author="Martin Röbke", - author_email="martin.roebke@mailbox.tu-dresden.de", - license='GPLv3', - packages=['tdvisu'], - platforms='any', - install_requires=['graphviz', 'psycopg2-binary', 'python-benedict', 'python-benedict[xml]', 'PyYAML'], - extras_require={'test': tests_require}, - classifiers=classifiers, - keywords='graph visualization dynamic-programming msol-solver') +setup( + name="tdvisu", + version=version, + description=description, + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/VaeterchenFrost/tdvisu", + author="Martin Röbke", + author_email="martin.roebke@web.de", + license="GPLv3", + packages=["tdvisu"], + platforms="any", + install_requires=[ + "graphviz", + "psycopg[c]", + "python-benedict[xml]", + "PyYAML", + ], + extras_require={"test": tests_require}, + classifiers=classifiers, + keywords="graph visualization dynamic-programming msol-solver", +) diff --git a/stable-requirements.txt b/stable-requirements.txt index 3cc7f9f..83a9837 100644 --- a/stable-requirements.txt +++ b/stable-requirements.txt @@ -1,22 +1,24 @@ -attrs==23.1.0 -certifi==2024.7.4 -charset-normalizer==3.2.0 +attrs==24.3.0 +certifi==2024.12.14 +charset-normalizer==3.4.0 colorama==0.4.6 -graphviz==0.20.1 -hypothesis==6.82.0 -idna==3.7 +graphviz==0.20.3 +hypothesis==6.122.3 +idna==3.10 iniconfig==2.0.0 -packaging==23.1 -pluggy==1.2.0 -psycopg2==2.9.6 -pytest==7.4.0 -pytest-mock==3.11.1 -python-benedict==0.32.0 -python-fsutil==0.10.0 -python-slugify==8.0.1 -PyYAML==6.0.1 -requests==2.32.2 +packaging==24.2 +pluggy==1.5.0 +psycopg==3.2.3 +pytest==8.3.4 +pytest-mock==3.14.0 +python-benedict==0.34.0 +python-fsutil==0.14.1 +python-slugify==8.0.4 +PyYAML==6.0.2 +requests==2.32.3 sortedcontainers==2.4.0 text-unidecode==1.3 -urllib3==2.2.2 -xmltodict==0.13.0 +typing_extensions==4.12.2 +tzdata==2024.2 +urllib3==2.2.3 +xmltodict==0.14.2 diff --git a/tdvisu/construct_dpdb_visu.py b/tdvisu/construct_dpdb_visu.py index f395f62..11422f6 100644 --- a/tdvisu/construct_dpdb_visu.py +++ b/tdvisu/construct_dpdb_visu.py @@ -34,8 +34,8 @@ from time import sleep from typing import List, Optional, Tuple -import psycopg2 as pg -from psycopg2 import sql +import psycopg as pg +from psycopg import sql from tdvisu.dijkstra import bidirectional_dijkstra as find_path from tdvisu.reader import TwReader @@ -52,35 +52,6 @@ "application_name": "dpdb-admin" } -PSYCOPG2_8_5_TASTATUS = { - pg.extensions.TRANSACTION_STATUS_IDLE: - ('TRANSACTION_STATUS_IDLE ', - '(The session is idle and there is no current transaction.)'), - - pg.extensions.TRANSACTION_STATUS_ACTIVE: - ('TRANSACTION_STATUS_ACTIVE ', - '(A command is currently in progress.)'), - - pg.extensions.TRANSACTION_STATUS_INTRANS: - ('TRANSACTION_STATUS_INTRANS ', - '(The session is idle in a valid transaction block.)'), - - pg.extensions.TRANSACTION_STATUS_INERROR: - ('TRANSACTION_STATUS_INERROR ', - '(The session is idle in a failed transaction block.)'), - - pg.extensions.TRANSACTION_STATUS_UNKNOWN: - ('TRANSACTION_STATUS_UNKNOWN ', - '(Reported if the connection with the server is bad.)') -} - - -def good_db_status() -> tuple: - """Any good db status to proceed.""" - return (pg.extensions.TRANSACTION_STATUS_IDLE, - pg.extensions.TRANSACTION_STATUS_INTRANS) - - def read_cfg(cfg_file, section: str, prefer_cfg: bool = False) -> dict: """Read the config file and return the result of one section.""" try: @@ -247,9 +218,8 @@ def read_timeline(self, edgearray) -> list: class DpdbSharpSatVisu(IDpdbVisuConstruct): """Implementation of the JSON-construction for the SharpSat problem.""" - def __init__(self, db: pg.extensions.connection, - problem: int, intermed_nodes: bool): - """db : psycopg2.connection + def __init__(self, db: pg.Connection, problem: int, intermed_nodes: bool): + """db : psycopg.connection database to read from. problem : int index of the problem. @@ -263,13 +233,14 @@ def __init__(self, db: pg.extensions.connection, self.num_vars = None # wait for good connection - status = db.get_transaction_status() sleeptimer = 0.5 - while status not in good_db_status(): - logging.warning("Waiting %fs for DB connection in status %s", - sleeptimer, PSYCOPG2_8_5_TASTATUS[status]) + while (status:=db.info.status) != pg.pq.ConnStatus.OK: + logging.warning( + "Waiting %.2fs for DB connection in status %s", + sleeptimer, + status._name_, + ) sleep(sleeptimer) - status = db.get_transaction_status() self.connection = db @@ -524,14 +495,14 @@ def construct(self) -> dict: 'treeDecJson': tree_dec_json} -def connect() -> pg.extensions.connection: +def connect(): """Connect to the PostgreSQL database server using the params from config.""" conn = None try: # read connection parameters - params = db_config(filename='Archive/database.json') - db_name = params['database'] + params = db_config(filename="database.ini") + db_name = params["database"] LOGGER.info("Connecting to the PostgreSQL database '%s'...", db_name) conn = pg.connect(**params) with conn.cursor() as cur: # create a cursor diff --git a/tdvisu/dijkstra.py b/tdvisu/dijkstra.py index 338a7f7..8f30497 100644 --- a/tdvisu/dijkstra.py +++ b/tdvisu/dijkstra.py @@ -48,7 +48,7 @@ from itertools import count -def bidirectional_dijkstra(edges, source, target, weight='weight'): +def bidirectional_dijkstra(edges, source, target, weight="weight"): r"""Dijkstra's algorithm for shortest paths using bidirectional search. Parameters @@ -127,7 +127,7 @@ def bidirectional_dijkstra(edges, source, target, weight='weight'): push = heappush pop = heappop # Init: [Forward, Backward] - dists = [{}, {}] # dictionary of final distances + dists = [{}, {}] # dictionary of final distances paths = [{source: [source]}, {target: [target]}] # dictionary of paths fringe = [[], []] # heap of (distance, node) for choosing node to expand seen = [{source: 0}, {target: 0}] # dict of distances to seen nodes @@ -164,8 +164,7 @@ def bidirectional_dijkstra(edges, source, target, weight='weight'): vw_length = dists[direction][v] + weight(w, v, d) if w in dists[direction]: if vw_length < dists[direction][w]: - raise ValueError( - "Contradictory paths found: negative weights?") + raise ValueError("Contradictory paths found: negative weights?") elif w not in seen[direction] or vw_length < seen[direction][w]: # relaxing seen[direction][w] = vw_length @@ -229,12 +228,14 @@ def _weight_function(weight, multigraph: bool = False): return lambda u, v, data: data.get(weight, 1) -if __name__ == "__main__": # pragma: no cover +if __name__ == "__main__": # pragma: no cover # Show one example and print to console - EDGES = {2: {1: {}, 3: {}, 4: {}}, - 1: {2: {}}, - 3: {2: {}}, - 4: {2: {}, 5: {}}, - 5: {4: {}}} + EDGES = { + 2: {1: {}, 3: {}, 4: {}}, + 1: {2: {}}, + 3: {2: {}}, + 4: {2: {}, 5: {}}, + 5: {4: {}}, + } RESULT = bidirectional_dijkstra(EDGES, 3, 5) print(RESULT) diff --git a/tdvisu/logging.yml b/tdvisu/logging.yml index 3c23a55..b486892 100644 --- a/tdvisu/logging.yml +++ b/tdvisu/logging.yml @@ -1,33 +1,29 @@ -version: 1 +--- formatters: - simple: - format: "%(asctime)s %(levelname)s %(message)s" - datefmt: "%H:%M:%S" full: - format: "%(asctime)s,%(msecs)d %(levelname)s[%(filename)s:%(lineno)d] %(message)s" - datefmt: "%Y-%m-%d %H:%M:%S" + datefmt: '%Y-%m-%d %H:%M:%S' + format: '%(asctime)s,%(msecs)d %(levelname)s[%(filename)s:%(lineno)d] %(message)s' + simple: + datefmt: '%H:%M:%S' + format: '%(asctime)s %(levelname)s %(message)s' handlers: console: class: logging.StreamHandler - level: WARNING formatter: full + level: WARNING stream: ext://sys.stdout loggers: - visualization.py: - level: NOTSET - - svgjoin.py: + construct_dpdb_visu.py: level: NOTSET - reader.py: level: NOTSET - - construct_dpdb_visu.py: + svgjoin.py: level: NOTSET - utilities.py: level: NOTSET - + visualization.py: + level: NOTSET root: - level: WARNING handlers: [console] + level: WARNING +version: 1 \ No newline at end of file diff --git a/tdvisu/reader.py b/tdvisu/reader.py index bee21ee..c377624 100644 --- a/tdvisu/reader.py +++ b/tdvisu/reader.py @@ -46,11 +46,12 @@ def main() from tdvisu.utilities import add_edge_to -logger = logging.getLogger('reader.py') +logger = logging.getLogger("reader.py") -class Reader(): +class Reader: """Base class for string-readers.""" + @classmethod def from_filename(cls, fname) -> Reader: with open(fname, "r") as file: @@ -150,7 +151,7 @@ def store_problem_vars(self): def body(self, lines) -> None: """Store the content from the given lines in the edges and adjacency_dict.""" - if self.format not in ('col', 'tw'): + if self.format not in ("col", "tw"): logger.error("Not a tw file!") sys.exit(1) @@ -163,7 +164,8 @@ def body(self, lines) -> None: logger.warning( "Expected exactly 2 vertices at line %d, but %d found", lineno, - len(line)) + len(line), + ) vertex1 = int(line[0]) vertex2 = int(line[1]) @@ -171,5 +173,7 @@ def body(self, lines) -> None: if len(self.edges) != self.num_edges: logger.warning( - "Number of edges mismatch preamble (%d vs %d)", len( - self.edges), self.num_edges) + "Number of edges mismatch preamble (%d vs %d)", + len(self.edges), + self.num_edges, + ) diff --git a/tdvisu/svgjoin.py b/tdvisu/svgjoin.py index 660246e..ea06ab8 100644 --- a/tdvisu/svgjoin.py +++ b/tdvisu/svgjoin.py @@ -30,7 +30,7 @@ from tdvisu.utilities import gen_arg -LOGGER = logging.getLogger('svg_join.py') +LOGGER = logging.getLogger("svg_join.py") # indices @@ -41,19 +41,20 @@ def test_viewbox(viewbox: List[float]): """Should be of form [0, 0, +x, +y]""" assert len(viewbox) == 4, "viewbox should have exactly 4 values" - assert viewbox[:2] == [0., 0.], "[min-x,min-y] should be zero." + assert viewbox[:2] == [0.0, 0.0], "[min-x,min-y] should be zero." assert viewbox[WIDTH] > 0, "should have positive width" assert viewbox[HEIGHT] > 0, "should have positive height" def append_svg( - first_dict: dict, - snd_dict: dict, - centerpad: float = 0., - v_bottom: float = None, - v_top: float = None, - scale2: float = 1, - ndigits: int = 3) -> dict: + first_dict: dict, + snd_dict: dict, + centerpad: float = 0.0, + v_bottom: float = None, + v_top: float = None, + scale2: float = 1, + ndigits: int = 3, +) -> dict: """Modifies the first of two xml-svg dictionary containing a viewbox to append the second svg to the right of the first image. @@ -89,8 +90,8 @@ def append_svg( """ - first_svg = first_dict['svg'] - second_svg = snd_dict['svg'] + first_svg = first_dict["svg"] + second_svg = snd_dict["svg"] # The value of the viewBox attribute is a list of four numbers: # min-x, min-y, width and height. @@ -100,53 +101,54 @@ def append_svg( # See also # https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/viewBox - pattern = re.compile(r'\s*,\s*|\s+') - viewbox1: List[float] = list( - map(float, re.split(pattern, first_svg['@viewBox']))) - viewbox2: List[float] = list( - map(float, re.split(pattern, second_svg['@viewBox']))) + pattern = re.compile(r"\s*,\s*|\s+") + viewbox1: List[float] = list(map(float, re.split(pattern, first_svg["@viewBox"]))) + viewbox2: List[float] = list(map(float, re.split(pattern, second_svg["@viewBox"]))) test_viewbox(viewbox1) # viewbox1 validation test_viewbox(viewbox2) # viewbox2 validation trafo_result = f_transform( - viewbox1[HEIGHT], viewbox2[HEIGHT], v_bottom, v_top, scale2) - vertical_snd = trafo_result['vertical_snd'] - combine_height = trafo_result['combine_height'] - scale2 = trafo_result['scale2'] + viewbox1[HEIGHT], viewbox2[HEIGHT], v_bottom, v_top, scale2 + ) + vertical_snd = trafo_result["vertical_snd"] + combine_height = trafo_result["combine_height"] + scale2 = trafo_result["scale2"] LOGGER.info( "Transformed with vertical_snd=%s combine_height=%s scale2=%s", - *(vertical_snd, combine_height, scale2)) + *(vertical_snd, combine_height, scale2), + ) viewbox1[HEIGHT] = round(combine_height - 0.5) h_displacement = float(viewbox1[WIDTH]) + centerpad - viewbox1[WIDTH] = round(max(float(viewbox1[WIDTH]), - h_displacement + scale2 * viewbox2[WIDTH]) - 0.5) + viewbox1[WIDTH] = round( + max(float(viewbox1[WIDTH]), h_displacement + scale2 * viewbox2[WIDTH]) - 0.5 + ) # new viewbox - first_svg['@viewBox'] = ' '.join(map(str, viewbox1)) # new viewbox + first_svg["@viewBox"] = " ".join(map(str, viewbox1)) # new viewbox # update width and height - first_svg['@width'] = f"{viewbox1[WIDTH]}pt" - first_svg['@height'] = f"{viewbox1[HEIGHT]}pt" + first_svg["@width"] = f"{viewbox1[WIDTH]}pt" + first_svg["@height"] = f"{viewbox1[HEIGHT]}pt" # move second image group next to first v_transform = round(max(0, vertical_snd), ndigits) transform = f"translate({h_displacement} {v_transform}) scale({scale2}) " # now scales with scale2 - transform += second_svg['g'].get('@transform', '') - second_svg['g']['@transform'] = transform + transform += second_svg["g"].get("@transform", "") + second_svg["g"]["@transform"] = transform if vertical_snd < 0: # move first image, add after other transform - transform = first_svg['g'].get('@transform', '') + transform = first_svg["g"].get("@transform", "") transform += f" translate(0 {round(-vertical_snd,ndigits)})" - first_svg['g']['@transform'] = transform + first_svg["g"]["@transform"] = transform # add group to list of 'g' - if isinstance(first_svg['g'], list): - first_svg['g'].append(second_svg['g']) + if isinstance(first_svg["g"], list): + first_svg["g"].append(second_svg["g"]) else: - first_svg['g'] = [first_svg['g'], second_svg['g']] + first_svg["g"] = [first_svg["g"], second_svg["g"]] return first_dict @@ -165,10 +167,13 @@ def append_svg( """ -def f_transform(h_one_, h_two_, - v_bottom: Union[float, str, None] = None, - v_top: Union[float, str, None] = None, - scale2: float = 1) -> Dict[str, float]: +def f_transform( + h_one_, + h_two_, + v_bottom: Union[float, str, None] = None, + v_top: Union[float, str, None] = None, + scale2: float = 1, +) -> Dict[str, float]: """Calculate vertical position and scaling of second image. The input for v_bottom, v_top is in units from\n @@ -197,14 +202,20 @@ def f_transform(h_one_, h_two_, 'vertical_snd','combine_height','scale2' """ - v_displacement = 0. + v_displacement = 0.0 # cast to float h_one = float(h_one_) h_two = float(h_two_) LOGGER.debug("Calculating with h_one=%f h_two=%f", h_one, h_two) # normalize values - conversion = {'bottom': 1, 'center': 0.5, 'top': 0, 'inf': 0, - -float('inf'): 1, float('inf'): 0} + conversion = { + "bottom": 1, + "center": 0.5, + "top": 0, + "inf": 0, + -float("inf"): 1, + float("inf"): 0, + } v_bottom = conversion.get(v_bottom, v_bottom) if isinstance(v_bottom, str): raise ValueError(f"Encountered {v_bottom=} not in {conversion=}") @@ -227,7 +238,9 @@ def f_transform(h_one_, h_two_, # moving the centerline according to value and scaling LOGGER.info( "The values of 'v_top', 'v_bottom' are both interpreted " - "as %f - interpreting as centerline!", v_top) + "as %f - interpreting as centerline!", + v_top, + ) half = size2 / h_one / 2 v_top = v_top - half v_bottom = v_bottom + half @@ -245,22 +258,25 @@ def f_transform(h_one_, h_two_, # bottom - top combine_height = (max(1, v_bottom) - min(0, v_top)) * h_one - return {'vertical_snd': v_displacement, - 'combine_height': combine_height, - 'scale2': scale2} + return { + "vertical_snd": v_displacement, + "combine_height": combine_height, + "scale2": scale2, + } def svg_join( - base_names: list, - folder: str = '', - num_images: int = 1, - outname: str = 'combined', - suffix: str = '%d.svg', - preserve_aspectratio: str = 'xMinYMin', - padding: Union[int, Iterable[int]] = 0, - scale2: Union[float, Iterable[float]] = 1, - v_top: Union[None, float, str, Iterable[Union[None, float, str]]] = None, - v_bottom: Union[None, float, str, Iterable[Union[None, float, str]]] = None): + base_names: list, + folder: str = "", + num_images: int = 1, + outname: str = "combined", + suffix: str = "%d.svg", + preserve_aspectratio: str = "xMinYMin", + padding: Union[int, Iterable[int]] = 0, + scale2: Union[float, Iterable[float]] = 1, + v_top: Union[None, float, str, Iterable[Union[None, float, str]]] = None, + v_bottom: Union[None, float, str, Iterable[Union[None, float, str]]] = None, +): """ Joines different svg-images from tdvisu placed in 'folder' for every timestep in the horizontal order specified in 'in_names'. @@ -309,7 +325,7 @@ def svg_join( LOGGER.warning("svg_join called with one file - nothing to join!") return # use path library for normalizing the path - folder = Path(folder if folder else '') + folder = Path(folder if folder else "") resultname = str(folder / outname) + suffix names = [str(folder / name) + suffix for name in base_names] @@ -326,20 +342,30 @@ def svg_join( with open(names[1] % step) as file: im_2 = benedict.from_xml(file.read()) - result = append_svg(im_1, im_2, centerpad=next(gen_padding), - v_bottom=next(gen_v_bottom), - v_top=next(gen_v_top), scale2=next(gen_scale2)) + result = append_svg( + im_1, + im_2, + centerpad=next(gen_padding), + v_bottom=next(gen_v_bottom), + v_top=next(gen_v_top), + scale2=next(gen_scale2), + ) # rest: for name in names[2:]: with open(name % step) as file: image = benedict.from_xml(file.read()) - result = append_svg(result, image, centerpad=next(gen_padding), - v_bottom=next(gen_v_bottom), - v_top=next(gen_v_top), scale2=next(gen_scale2)) - - result['svg']['@preserveAspectRatio'] = preserve_aspectratio - with open(resultname % step, 'w') as file: + result = append_svg( + result, + image, + centerpad=next(gen_padding), + v_bottom=next(gen_v_bottom), + v_top=next(gen_v_top), + scale2=next(gen_scale2), + ) + + result["svg"]["@preserveAspectRatio"] = preserve_aspectratio + with open(resultname % step, "w") as file: result.to_xml(output=file, pretty=True) if step < 10: @@ -349,9 +375,11 @@ def svg_join( if __name__ == "__main__": # pragma: no cover logging.basicConfig(level=logging.DEBUG) - svg_join(['TDStep', 'graph'], - 'Archive/WheelGraph7', - outname="default_06sc15_rise", - v_bottom=[1, .85, .7, .55, .4], - scale2=1.5, - num_images=5) + svg_join( + ["TDStep", "graph"], + "Archive/WheelGraph7", + outname="default_06sc15_rise", + v_bottom=[1, 0.85, 0.7, 0.55, 0.4], + scale2=1.5, + num_images=5, + ) diff --git a/tdvisu/utilities.py b/tdvisu/utilities.py index 03c2218..ba9b6e3 100644 --- a/tdvisu/utilities.py +++ b/tdvisu/utilities.py @@ -25,21 +25,22 @@ import logging import logging.config from collections.abc import Iterable as iter_type -from configparser import ConfigParser, Error as CfgError, ParsingError +from configparser import ConfigParser +from configparser import Error as CfgError +from configparser import ParsingError from itertools import chain from pathlib import Path -from typing import (Any, Generator, Iterable, Iterator, - List, Tuple, TypeVar, Union) - -from tdvisu.version import __date__, __version__ +from typing import Any, Generator, Iterable, Iterator, List, Tuple, TypeVar, Union import yaml -LOGGER = logging.getLogger('utilities.py') +from tdvisu.version import __date__, __version__ -CFG_EXT = ('.ini', '.cfg', '.conf', '.config') +LOGGER = logging.getLogger("utilities.py") + +CFG_EXT = (".ini", ".cfg", ".conf", ".config") LOGLEVEL_EPILOG = """ -Logging levels for python 3.8.2: +Logging levels for Python: CRITICAL: 50 ERROR: 40 WARNING: 30 @@ -48,43 +49,43 @@ NOTSET: 0 (will traverse the logging hierarchy until a value is found) """ DEFAULT_LOGGING_CFG = { - 'version': 1, - 'formatters': { - 'simple': { - 'format': '%(asctime)s %(levelname)s %(message)s', - 'datefmt': '%H:%M:%S'}}, - 'handlers': { - 'console': { - 'class': 'logging.StreamHandler', - 'level': 'WARNING', - 'formatter': 'simple', - 'stream': 'ext://sys.stdout'}}, - 'loggers': { - 'visualization.py': { - 'level': 'NOTSET', - 'handlers': ['console'], - 'propagate': False}, - 'svgjoin.py': { - 'level': 'NOTSET', - 'handlers': ['console'], - 'propagate': False}, - 'reader.py': { - 'level': 'NOTSET', - 'handlers': ['console'], - 'propagate': False}, - 'construct_dpdb_visu.py': { - 'level': 'NOTSET', - 'handlers': ['console'], - 'propagate': False}}, - 'root': { - 'level': 'WARNING', - 'handlers': ['console']}} - -_T = TypeVar('_T') + "version": 1, + "formatters": { + "simple": { + "format": "%(asctime)s %(levelname)s %(message)s", + "datefmt": "%H:%M:%S", + } + }, + "handlers": { + "console": { + "class": "logging.StreamHandler", + "level": "WARNING", + "formatter": "simple", + "stream": "ext://sys.stdout", + } + }, + "loggers": { + "visualization.py": { + "level": "NOTSET", + "handlers": ["console"], + "propagate": False, + }, + "svgjoin.py": {"level": "NOTSET", "handlers": ["console"], "propagate": False}, + "reader.py": {"level": "NOTSET", "handlers": ["console"], "propagate": False}, + "construct_dpdb_visu.py": { + "level": "NOTSET", + "handlers": ["console"], + "propagate": False, + }, + }, + "root": {"level": "WARNING", "handlers": ["console"]}, +} + +_T = TypeVar("_T") def flatten(iterable: Iterable[Iterable[_T]]) -> Iterator[_T]: - """ Flatten at first level. + """Flatten at first level. Turn ex=[[1,2],[3,4]] into [1, 2, 3, 4] @@ -94,8 +95,9 @@ def flatten(iterable: Iterable[Iterable[_T]]) -> Iterator[_T]: return chain.from_iterable(iterable) -def read_yml_or_cfg(file: Union[str, Path], prefer_cfg: bool = False, - cfg_ext=CFG_EXT) -> Any: +def read_yml_or_cfg( + file: Union[str, Path], prefer_cfg: bool = False, cfg_ext=CFG_EXT +) -> Any: """ Read the file and return its content as a python object. @@ -115,8 +117,10 @@ def read_yml_or_cfg(file: Union[str, Path], prefer_cfg: bool = False, But maybe just a list or a single object. """ - err_str = ("utilities.read_yml_or_cfg encountered '{}' while " - "reading config from '{}' and prefer_cfg={}") + err_str = ( + "utilities.read_yml_or_cfg encountered '{}' while " + "reading config from '{}' and prefer_cfg={}" + ) file = Path(file) if not file.exists(): @@ -153,8 +157,9 @@ def read_yml_or_cfg(file: Union[str, Path], prefer_cfg: bool = False, return dict() -def logging_cfg(filename: str, prefer_cfg: bool = False, - loglevel: Union[None, int, str] = None) -> None: +def logging_cfg( + filename: str, prefer_cfg: bool = False, loglevel: Union[None, int, str] = None +) -> None: """Configure logging for this module""" logging.basicConfig() read_err = "could not read configuration from '%s'" @@ -168,7 +173,7 @@ def logging_cfg(filename: str, prefer_cfg: bool = False, except ValueError: loglevel = loglevel.upper() - if prefer_cfg or file.suffix.lower() in CFG_EXT: # .config + if prefer_cfg or file.suffix.lower() in CFG_EXT: # .config try: logging.config.fileConfig(file, defaults=DEFAULT_LOGGING_CFG) if loglevel is not None: @@ -181,7 +186,7 @@ def logging_cfg(filename: str, prefer_cfg: bool = False, LOGGER.error(read_err, file.resolve(), exc_info=True) except ValueError: LOGGER.error(config_err, file.resolve(), exc_info=True) - try: # dict + try: # dict file_content = read_yml_or_cfg(file, prefer_cfg=prefer_cfg) logging.config.dictConfig(file_content) if loglevel is not None: @@ -196,8 +201,7 @@ def logging_cfg(filename: str, prefer_cfg: bool = False, LOGGER.error(config_err, file.resolve(), exc_info=True) -def convert_to_adj( - edgelist: Iterable[Tuple[int, int]], directed: bool = False) -> dict: +def convert_to_adj(edgelist: Iterable[Tuple[int, int]], directed: bool = False) -> dict: """ Helper function to convert the edgelist into the adj-format from NetworkX. @@ -218,7 +222,7 @@ def convert_to_adj( https://networkx.github.io/documentation/networkx-2.1/_modules/networkx/classes/graph.html """ adj = dict() - for (source, target) in edgelist: + for source, target in edgelist: if source not in adj: adj[source] = {} adj[source][target] = {} @@ -230,11 +234,7 @@ def convert_to_adj( return adj -def add_edge_to( - edges: set, - adjacency_dict: dict, - vertex1: Any, - vertex2: Any) -> None: +def add_edge_to(edges: set, adjacency_dict: dict, vertex1: Any, vertex2: Any) -> None: """ Adding (undirected) edge from 'vertex1' to 'vertex2' to the edges and adjacency-list. @@ -293,17 +293,14 @@ def gen_arg(arg_or_iter: Any) -> Generator: yield item -def base_style( - graph, - node: str, - color: str = 'white', - penwidth: float = 1.0) -> None: +def base_style(graph, node: str, color: str = "white", penwidth: float = 1.0) -> None: """Style the node with default fillcolor and penwidth.""" graph.node(node, fillcolor=color, penwidth=str(penwidth)) -def emphasise_node(graph, node: str, color: str = 'yellow', - penwidth: float = 2.5) -> None: +def emphasise_node( + graph, node: str, color: str = "yellow", penwidth: float = 2.5 +) -> None: """Emphasise node with a different fillcolor (default:'yellow') and penwidth (default:2.5). """ @@ -315,22 +312,23 @@ def emphasise_node(graph, node: str, color: str = 'yellow', def style_hide_node(graph, node: str) -> None: """Make the node invisible during drawing.""" - graph.node(node, style='invis') + graph.node(node, style="invis") def style_hide_edge(graph, source: str, target: str) -> None: """Make the edge source->target invisible during drawing.""" - graph.edge(source, target, style='invis') + graph.edge(source, target, style="invis") def bag_node( - head, - tail, - anchor: str = 'anchor', - headcolor: str = 'white', - tableborder: int = 0, - cellborder: int = 0, - cellspacing: int = 0) -> str: + head, + tail, + anchor: str = "anchor", + headcolor: str = "white", + tableborder: int = 0, + cellborder: int = 0, + cellspacing: int = 0, +) -> str: """HTML format with 'head' as the first label, then appending further labels. @@ -356,13 +354,14 @@ def bag_node( def solution_node( - solution_table: Iterable[List[str]], - toplabel: str = '', - bottomlabel: str = '', - transpose: bool = False, - linesmax: int = 1000, - columnsmax: int = 50, - fillstr: str = '...') -> str: + solution_table: Iterable[List[str]], + toplabel: str = "", + bottomlabel: str = "", + transpose: bool = False, + linesmax: int = 1000, + columnsmax: int = 50, + fillstr: str = "...", +) -> str: """Fill the node from the 2D-matrix 'solution_table' COLUMNBASED!. Optionally add a line above and/or below the table for labels. The size of the result can be limited by using linesmax and columnsmax. @@ -398,62 +397,64 @@ def solution_node( | botlabel | |----------| """ - result = '' + result = "" if toplabel: - result += toplabel + '|' + result += toplabel + "|" if len(solution_table) == 0: - result += 'empty' + result += "empty" else: if transpose: solution_table = list(zip(*solution_table)) # limit lines backwards from length of column - vslice = (min(-1, linesmax - len(solution_table[0])) - if linesmax > 0 else -1) + vslice = min(-1, linesmax - len(solution_table[0])) if linesmax > 0 else -1 # limit columns forwards minus one - hslice = (min(len(solution_table), columnsmax) - if columnsmax > 0 else len(solution_table)) - 1 + hslice = ( + min(len(solution_table), columnsmax) + if columnsmax > 0 + else len(solution_table) + ) - 1 - result += '{' # insert table + result += "{" # insert table for column in solution_table[:hslice]: - result += '{' # start column + result += "{" # start column for row in column[:vslice]: - result += str(row) + '|' - if vslice < -1: # add one indicator of shortening - result += fillstr + '|' + result += str(row) + "|" + if vslice < -1: # add one indicator of shortening + result += fillstr + "|" for row in column[-1:]: result += str(row) - result += '}|' # sep. between columns + result += "}|" # sep. between columns # adding one column-skipping indicator if hslice < len(solution_table) - 1: - result += '{' # start column + result += "{" # start column for row in column[:vslice]: - result += fillstr + '|' - if vslice < -1: # add one indicator of shortening - result += fillstr + '|' + result += fillstr + "|" + if vslice < -1: # add one indicator of shortening + result += fillstr + "|" for row in column[-1:]: result += fillstr - result += '}|' # sep. between columns + result += "}|" # sep. between columns # last column (usually a summary of the previous cols) for column in solution_table[-1:]: - result += '{' # start column + result += "{" # start column for row in column[:vslice]: - result += str(row) + '|' - if vslice < -1: # add one indicator of shortening - result += fillstr + '|' + result += str(row) + "|" + if vslice < -1: # add one indicator of shortening + result += fillstr + "|" for row in column[-1:]: result += str(row) - result += '}' # sep. between columns - result += '}' # close table + result += "}" # sep. between columns + result += "}" # close table if bottomlabel: - result += '|' + bottomlabel + result += "|" + bottomlabel - return '{' + result + '}' + return "{" + result + "}" -def get_parser(extra_desc: str = '') -> argparse.ArgumentParser: +def get_parser(extra_desc: str = "") -> argparse.ArgumentParser: """ Prepare an argument parser for TDVisu scripts. @@ -475,11 +476,16 @@ def get_parser(extra_desc: str = '') -> argparse.ArgumentParser: This is free software, and you are welcome to redistribute it under certain conditions; see COPYING for more information. """ - + "\n" + extra_desc, + + "\n" + + extra_desc, epilog=LOGLEVEL_EPILOG, - formatter_class=argparse.RawDescriptionHelpFormatter) - - parser.add_argument('--version', action='version', - version='%(prog)s ' + __version__ + ', ' + __date__) - parser.add_argument('--loglevel', help="set the minimal loglevel for root") + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + + parser.add_argument( + "--version", + action="version", + version="%(prog)s " + __version__ + ", " + __date__, + ) + parser.add_argument("--loglevel", help="set the minimal loglevel for root") return parser diff --git a/tdvisu/version.py b/tdvisu/version.py index f32f803..aa0ef0a 100644 --- a/tdvisu/version.py +++ b/tdvisu/version.py @@ -4,7 +4,7 @@ """ # Base version. -__version__ = '1.1.9' +__version__ = "1.2.0" # Year-Month-Day of last version change -__date__ = '2023-07-27' +__date__ = "2024-12-24" diff --git a/tdvisu/visualization.py b/tdvisu/visualization.py index e2409a1..c0f3e1f 100644 --- a/tdvisu/visualization.py +++ b/tdvisu/visualization.py @@ -39,15 +39,27 @@ from graphviz import Digraph, Graph from tdvisu.svgjoin import svg_join -from tdvisu.utilities import (bag_node, base_style, emphasise_node, flatten, - get_parser, logging_cfg, solution_node, - style_hide_edge, style_hide_node) -from tdvisu.visualization_data import (GeneralGraphData, IncidenceGraphData, - SvgJoinData, VisualizationData) - -LOGGER = logging.getLogger('visualization.py') - -StrOrIo = NewType('StrOrIo', Union[str, io.TextIOWrapper]) +from tdvisu.utilities import ( + bag_node, + base_style, + emphasise_node, + flatten, + get_parser, + logging_cfg, + solution_node, + style_hide_edge, + style_hide_node, +) +from tdvisu.visualization_data import ( + GeneralGraphData, + IncidenceGraphData, + SvgJoinData, + VisualizationData, +) + +LOGGER = logging.getLogger("visualization.py") + +StrOrIo = NewType("StrOrIo", Union[str, io.TextIOWrapper]) def read_json(json_data: StrOrIo) -> dict: @@ -97,9 +109,9 @@ def inspect_json(self, infile: StrOrIo) -> VisualizationData: LOGGER.debug("Found keys: %s", visudata.keys()) try: - _incid = visudata.get('incidenceGraph', None) - _general_graph = visudata.get('generalGraph', None) - _svg_join = visudata.get('svgjoin', None) + _incid = visudata.get("incidenceGraph", None) + _general_graph = visudata.get("generalGraph", None) + _svg_join = visudata.get("svgjoin", None) incid_data: List[IncidenceGraphData] = list() if _incid: @@ -108,10 +120,9 @@ def inspect_json(self, infile: StrOrIo) -> VisualizationData: # unwrap as list: for data in _incid: # add object to incid_data - data['edges'] = [[x['id'], x['list']] - for x in data['edges']] + data["edges"] = [[x["id"], x["list"]] for x in data["edges"]] incid_data += [IncidenceGraphData(**data)] - visudata.pop('incidenceGraph', None) + visudata.pop("incidenceGraph", None) general_graph_data: List[GeneralGraphData] = list() if _general_graph: @@ -119,36 +130,39 @@ def inspect_json(self, infile: StrOrIo) -> VisualizationData: _general_graph = [_general_graph] for data in _general_graph: general_graph_data += [GeneralGraphData(**data)] - visudata.pop('generalGraph', None) + visudata.pop("generalGraph", None) svg_join_data: Optional[SvgJoinData] = None if _svg_join: svg_join_data = SvgJoinData(**_svg_join) - if 'svgjoin' in visudata: - visudata.pop('svgjoin') - - self.timeline = visudata['tdTimeline'] - visudata.pop('tdTimeline') - self.tree_dec = visudata['treeDecJson'] - self.bagpre = self.tree_dec['bagpre'] - self.joinpre = self.tree_dec.get('joinpre', 'Join %d~%d') - self.solpre = self.tree_dec.get('solpre', 'sol%d') - self.soljoinpre = self.tree_dec.get('soljoinpre', 'solJoin%d~%d') - visudata.pop('treeDecJson') + if "svgjoin" in visudata: + visudata.pop("svgjoin") + + self.timeline = visudata["tdTimeline"] + visudata.pop("tdTimeline") + self.tree_dec = visudata["treeDecJson"] + self.bagpre = self.tree_dec["bagpre"] + self.joinpre = self.tree_dec.get("joinpre", "Join %d~%d") + self.solpre = self.tree_dec.get("solpre", "sol%d") + self.soljoinpre = self.tree_dec.get("soljoinpre", "solJoin%d~%d") + visudata.pop("treeDecJson") except KeyError as err: raise KeyError(f"Key {err} not found in the input Json.") - return VisualizationData(incidence_graphs=incid_data, - general_graphs=general_graph_data, - svg_join=svg_join_data, - **visudata) + return VisualizationData( + incidence_graphs=incid_data, + general_graphs=general_graph_data, + svg_join=svg_join_data, + **visudata, + ) def setup_tree_dec_graph( - self, - rankdir: str = 'BT', - shape: str = 'box', - fillcolor: str = 'white', - style: str = 'rounded,filled', - margin: str = '0.11,0.01') -> None: + self, + rankdir: str = "BT", + shape: str = "box", + fillcolor: str = "white", + style: str = "rounded,filled", + margin: str = "0.11,0.01", + ) -> None: """Create self.tree_dec_digraph strict means not a multigraph - equal edges get merged. @@ -156,74 +170,89 @@ def setup_tree_dec_graph( - normally Bottom-Top or Top-Bottom. """ self.tree_dec_digraph = Digraph( - 'Tree-Decomposition', strict=True, - graph_attr={'rankdir': rankdir}, + "Tree-Decomposition", + strict=True, + graph_attr={"rankdir": rankdir}, node_attr={ - 'shape': shape, - 'fillcolor': fillcolor, - 'style': style, - 'margin': margin}) + "shape": shape, + "fillcolor": fillcolor, + "style": style, + "margin": margin, + }, + ) def basic_tdg(self) -> None: """Create basic bag structure in tree_dec_digraph.""" - for item in self.tree_dec['labeldict']: - bagname = self.bagpre % str(item['id']) - self.tree_dec_digraph.node(bagname, - bag_node(bagname, item['labels'])) - - self.tree_dec_digraph.edges([(self.bagpre % str(first), self.bagpre % str( - second)) for (first, second) in self.tree_dec['edgearray']]) - - def forward_iterate_tdg( - self, - joinpre: str, - solpre: str, - soljoinpre: str) -> None: + for item in self.tree_dec["labeldict"]: + bagname = self.bagpre % str(item["id"]) + self.tree_dec_digraph.node(bagname, bag_node(bagname, item["labels"])) + + self.tree_dec_digraph.edges( + [ + (self.bagpre % str(first), self.bagpre % str(second)) + for (first, second) in self.tree_dec["edgearray"] + ] + ) + + def forward_iterate_tdg(self, joinpre: str, solpre: str, soljoinpre: str) -> None: """Create the final positions of all nodes with solutions.""" - tdg = self.tree_dec_digraph # shorten name + tdg = self.tree_dec_digraph # shorten name - for i, node in enumerate(self.timeline): # Create the positions + for i, node in enumerate(self.timeline): # Create the positions if len(node) > 1: # solution to be displayed id_inv_bags = node[0] if isinstance(id_inv_bags, int): last_sol = solpre % id_inv_bags - tdg.node(last_sol, solution_node( - *(node[1])), shape='record') + tdg.node(last_sol, solution_node(*(node[1])), shape="record") tdg.edge(self.bagpre % id_inv_bags, last_sol) - else: # joined node with 2 bags - suc = self.timeline[i + 1][0] # get the joined bags + else: # joined node with 2 bags + suc = self.timeline[i + 1][0] # get the joined bags - LOGGER.debug('joining %s to %s ', node[0], suc) + LOGGER.debug("joining %s to %s ", node[0], suc) id_inv_bags = tuple(id_inv_bags) last_sol = soljoinpre % id_inv_bags - tdg.node(last_sol, solution_node( - *(node[1])), shape='record') + tdg.node(last_sol, solution_node(*(node[1])), shape="record") tdg.edge(joinpre % id_inv_bags, last_sol) # edges for child in id_inv_bags: # basically "remove" current # TODO check where 2 args are possibly occuring tdg.edge( - self.bagpre % child - if isinstance(child, int) else joinpre % child, - self.bagpre % suc - if isinstance(suc, int) else joinpre % suc, - style='invis', - constraint='false') - tdg.edge(self.bagpre % child if isinstance(child, int) - else joinpre % child, - joinpre % id_inv_bags) - tdg.edge(joinpre % id_inv_bags, self.bagpre % suc - if isinstance(suc, int) else joinpre % suc) - - def backwards_iterate_tdg(self, joinpre: str, solpre: str, soljoinpre: str, - view: bool = False) -> None: + ( + self.bagpre % child + if isinstance(child, int) + else joinpre % child + ), + ( + self.bagpre % suc + if isinstance(suc, int) + else joinpre % suc + ), + style="invis", + constraint="false", + ) + tdg.edge( + ( + self.bagpre % child + if isinstance(child, int) + else joinpre % child + ), + joinpre % id_inv_bags, + ) + tdg.edge( + joinpre % id_inv_bags, + self.bagpre % suc if isinstance(suc, int) else joinpre % suc, + ) + + def backwards_iterate_tdg( + self, joinpre: str, solpre: str, soljoinpre: str, view: bool = False + ) -> None: """Cut the single steps back and update emphasis acordingly.""" - tdg = self.tree_dec_digraph # shorten name + tdg = self.tree_dec_digraph # shorten name last_sol = "" _filename = self.outfolder / self.data.td_file for i, node in enumerate(reversed(self.timeline)): @@ -234,11 +263,10 @@ def backwards_iterate_tdg(self, joinpre: str, solpre: str, soljoinpre: str, # Delete previous emphasis prevhead = self.timeline[len(self.timeline) - i][0] bag = ( - self.bagpre % - prevhead if isinstance( - prevhead, - int) else joinpre % - tuple(prevhead)) + self.bagpre % prevhead + if isinstance(prevhead, int) + else joinpre % tuple(prevhead) + ) base_style(tdg, bag) if last_sol: style_hide_node(tdg, last_sol) @@ -256,36 +284,39 @@ def backwards_iterate_tdg(self, joinpre: str, solpre: str, soljoinpre: str, last_sol = soljoinpre % id_inv_bags emphasise_node(tdg, last_sol) - emphasise_node(tdg, - self.bagpre % - id_inv_bags if isinstance( - id_inv_bags, - int) else joinpre % - id_inv_bags) + emphasise_node( + tdg, + ( + self.bagpre % id_inv_bags + if isinstance(id_inv_bags, int) + else joinpre % id_inv_bags + ), + ) tdg.render( - view=view, format='svg', - filename=str(_filename) + str(len(self.timeline) - i) + view=view, + format="svg", + filename=str(_filename) + str(len(self.timeline) - i), ) def tree_dec_timeline(self, view: bool = False) -> None: """Main-method for handling all construction of the timeline.""" self.setup_tree_dec_graph( - rankdir=self.data.orientation, - fillcolor=self.data.bagcolor) + rankdir=self.data.orientation, fillcolor=self.data.bagcolor + ) self.basic_tdg() # Iterate labeldict self.forward_iterate_tdg( - joinpre=self.joinpre, - solpre=self.solpre, - soljoinpre=self.soljoinpre) + joinpre=self.joinpre, solpre=self.solpre, soljoinpre=self.soljoinpre + ) self.backwards_iterate_tdg( view=view, joinpre=self.joinpre, solpre=self.solpre, - soljoinpre=self.soljoinpre) + soljoinpre=self.soljoinpre, + ) # Prepare supporting graph timeline @@ -296,8 +327,13 @@ def tree_dec_timeline(self, view: bool = False) -> None: elif isinstance(step[0], int): _timeline.append( next( - (item.get('items') for item in self.tree_dec['labeldict'] - if item['id'] == step[0]))) + ( + item.get("items") + for item in self.tree_dec["labeldict"] + if item["id"] == step[0] + ) + ) + ) else: # Join operation - no clauses involved in computation _timeline.append(None) @@ -305,15 +341,15 @@ def tree_dec_timeline(self, view: bool = False) -> None: if self.data.incidence_graphs: for incidence_data in self.data.incidence_graphs: self.prepare_incidence(incidence_data, _timeline, view) - LOGGER.info("Created incidence-graph for file='%s'", - incidence_data.inc_file) + LOGGER.info( + "Created incidence-graph for file='%s'", incidence_data.inc_file + ) if self.data.general_graphs: for graph_data in self.data.general_graphs: - self.general_graph(timeline=_timeline, view=view, - **asdict(graph_data)) + self.general_graph(timeline=_timeline, view=view, **asdict(graph_data)) LOGGER.info( - "Created general-graph for file='%s'", - graph_data.file_basename) + "Created general-graph for file='%s'", graph_data.file_basename + ) if self.data.svg_join: self.call_svgjoin() @@ -321,18 +357,21 @@ def prepare_incidence(self, incid, _timeline, view): """Prepare incidence construction.""" if incid.infer_primal or incid.infer_dual: # prepare incid edges with abs: - abs_clauses = [[cl[0], list(map(abs, cl[1]))] - for cl in incid.edges] + abs_clauses = [[cl[0], list(map(abs, cl[1]))] for cl in incid.edges] if incid.infer_primal: # vertex for each variable + edge if the variables # occur in the same clause: - primal_edges = set(flatten( # remove duplicates - [itertools.combinations(cl[1], 2) - for cl in abs_clauses])) + primal_edges = set( + flatten( # remove duplicates + [itertools.combinations(cl[1], 2) for cl in abs_clauses] + ) + ) # check if any node is really isolated: - isolated = [cl[1][0] for cl in abs_clauses - if len(cl[1]) == 1 and - not any(cl[1][0] in sl for sl in primal_edges)] + isolated = [ + cl[1][0] + for cl in abs_clauses + if len(cl[1]) == 1 and not any(cl[1][0] in sl for sl in primal_edges) + ] self.general_graph( timeline=_timeline, @@ -340,18 +379,22 @@ def prepare_incidence(self, incid, _timeline, view): extra_nodes=set(isolated), graph_name=incid.primal_file, file_basename=incid.primal_file, - var_name=incid.var_name_two) + var_name=incid.var_name_two, + ) LOGGER.info("Created infered primal-graph") if incid.infer_dual: # Edge, if clauses share the same variable - dual_edges = [(cl[0], other[0]) - for i, cl in enumerate(abs_clauses) - for other in abs_clauses[i + 1:] # no multiples - if any(var in cl[1] for var in other[1])] + dual_edges = [ + (cl[0], other[0]) + for i, cl in enumerate(abs_clauses) + for other in abs_clauses[i + 1 :] # no multiples + if any(var in cl[1] for var in other[1]) + ] # check if any clause is isolated: - isolated = [cl[0] for cl in abs_clauses - if not any(cl[0] in sl for sl in dual_edges)] + isolated = [ + cl[0] for cl in abs_clauses if not any(cl[0] in sl for sl in dual_edges) + ] self.general_graph( timeline=_timeline, @@ -359,41 +402,45 @@ def prepare_incidence(self, incid, _timeline, view): extra_nodes=set(isolated), graph_name=incid.dual_file, file_basename=incid.dual_file, - var_name=incid.var_name_one) + var_name=incid.var_name_one, + ) LOGGER.info("Created infered dual-graph") self.incidence( edges=incid.edges, timeline=_timeline, inc_file=incid.inc_file, - num_vars=self.tree_dec['num_vars'], - colors=self.data.colors, view=view, + num_vars=self.tree_dec["num_vars"], + colors=self.data.colors, + view=view, fontsize=incid.fontsize, penwidth=incid.penwidth, basefill=self.data.bagcolor, var_name_one=incid.var_name_one, var_name_two=incid.var_name_two, - column_distance=incid.column_distance) + column_distance=incid.column_distance, + ) def general_graph( - self, - timeline: Iterable[Optional[List[int]]], - edges: Iterable[Iterable[int]], - extra_nodes: Iterable[int] = tuple(), - view: bool = False, - fontsize: int = 20, - fontcolor: str = 'black', - penwidth: float = 2.2, - first_color: str = 'yellow', - first_style: str = 'filled', - second_color: str = 'green', - second_style: str = 'dotted,filled', - third_color: str = 'red', - graph_name: str = 'graph', - file_basename: str = 'graph', - do_sort_nodes: bool = True, - do_adj_nodes: bool = True, - var_name: str = '') -> None: + self, + timeline: Iterable[Optional[List[int]]], + edges: Iterable[Iterable[int]], + extra_nodes: Iterable[int] = tuple(), + view: bool = False, + fontsize: int = 20, + fontcolor: str = "black", + penwidth: float = 2.2, + first_color: str = "yellow", + first_style: str = "filled", + second_color: str = "green", + second_style: str = "dotted,filled", + third_color: str = "red", + graph_name: str = "graph", + file_basename: str = "graph", + do_sort_nodes: bool = True, + do_adj_nodes: bool = True, + var_name: str = "", + ) -> None: """ Creates one graph emphasized for the given timeline. @@ -423,113 +470,117 @@ def general_graph( """ _filename = self.outfolder / file_basename LOGGER.info("Generating general-graph for '%s'", file_basename) - vartag_n: str = var_name + '%d' + vartag_n: str = var_name + "%d" # sfdp http://yifanhu.net/SOFTWARE/SFDP/index.html - default_engine = 'sfdp' + default_engine = "sfdp" graph = Graph( graph_name, strict=True, engine=default_engine, graph_attr={ - 'fontsize': str(fontsize), - 'overlap': 'false', - 'outputorder': 'edgesfirst', - 'K': '2'}, + "fontsize": str(fontsize), + "overlap": "false", + "outputorder": "edgesfirst", + "K": "2", + }, node_attr={ - 'fontcolor': str(fontcolor), - 'penwidth': str(penwidth), - 'style': 'filled', - 'fillcolor': 'white'}) + "fontcolor": str(fontcolor), + "penwidth": str(penwidth), + "style": "filled", + "fillcolor": "white", + }, + ) if do_sort_nodes: bodybaselen = len(graph.body) # 1: layout with circo - graph.engine = 'circo' + graph.engine = "circo" # 2: nodes in edges+extra_nodes make a circle - nodes = sorted([vartag_n % n for n in set( - itertools.chain(flatten(edges), extra_nodes))], - key=lambda x: (len(x), x)) + nodes = sorted( + [ + vartag_n % n + for n in set(itertools.chain(flatten(edges), extra_nodes)) + ], + key=lambda x: (len(x), x), + ) for i, node in enumerate(nodes): graph.edge(str(nodes[i - 1]), str(node)) # 3: reads in bytes! - code_lines = graph.pipe('plain').splitlines() + code_lines = graph.pipe("plain").splitlines() # 4: save the (sorted) positions - assert code_lines[0].startswith(b'graph') - node_positions = [line.split()[1:4] for line in code_lines[1:] - if line.startswith(b'node')] + assert code_lines[0].startswith(b"graph") + node_positions = [ + line.split()[1:4] for line in code_lines[1:] if line.startswith(b"node") + ] # 5: cut layout graph.body = graph.body[:bodybaselen] for line in node_positions: - graph.node(line[0].decode(), - pos='%f,%f!' % (float(line[1]), float(line[2]))) + graph.node( + line[0].decode(), pos="%f,%f!" % (float(line[1]), float(line[2])) + ) # 6: Engine uses previous positions - graph.engine = 'neato' + graph.engine = "neato" - for (src, tar) in edges: + for src, tar in edges: graph.edge(vartag_n % src, vartag_n % tar) for nodeid in extra_nodes: graph.node(vartag_n % nodeid) bodybaselen = len(graph.body) - for i, variables in enumerate(timeline, start=1): # all timesteps + for i, variables in enumerate(timeline, start=1): # all timesteps # reset highlighting graph.body = graph.body[:bodybaselen] if variables is None: - graph.render( - view=view, - format='svg', - filename=str(_filename) + str(i)) + graph.render(view=view, format="svg", filename=str(_filename) + str(i)) continue for var in variables: - graph.node( - vartag_n % var, - fillcolor=first_color, - style=first_style) + graph.node(vartag_n % var, fillcolor=first_color, style=first_style) # highlight edges between variables - for (s, t) in edges: + for s, t in edges: if s in variables and t in variables: graph.edge( vartag_n % s, vartag_n % t, color=third_color, - penwidth=str(penwidth)) + penwidth=str(penwidth), + ) if do_adj_nodes: # set.difference accepts list as argument, "-" does not. edges = [set(edge) for edge in edges] adjacent = { - edge.difference(variables).pop() for edge in edges if len( - edge.difference(variables)) == 1} + edge.difference(variables).pop() + for edge in edges + if len(edge.difference(variables)) == 1 + } for var in adjacent: - graph.node(vartag_n % var, - color=second_color, - style=second_style) + graph.node(vartag_n % var, color=second_color, style=second_style) - graph.render(view=view, format='svg', - filename=str(_filename) + str(i)) + graph.render(view=view, format="svg", filename=str(_filename) + str(i)) def incidence( - self, - timeline: Iterable[Optional[List[int]]], - num_vars: int, - colors: List, - edges: List, - inc_file: str = 'IncidenceGraphStep', - view: bool = False, - fontsize: Union[str, int] = 16, - penwidth: float = 2.2, - basefill: str = 'white', - sndshape: str = 'diamond', - neg_tail: str = 'odot', - var_name_one: str = '', - var_name_two: str = '', - column_distance: float = 0.5) -> None: + self, + timeline: Iterable[Optional[List[int]]], + num_vars: int, + colors: List, + edges: List, + inc_file: str = "IncidenceGraphStep", + view: bool = False, + fontsize: Union[str, int] = 16, + penwidth: float = 2.2, + basefill: str = "white", + sndshape: str = "diamond", + neg_tail: str = "odot", + var_name_one: str = "", + var_name_two: str = "", + column_distance: float = 0.5, + ) -> None: """ Creates the incidence graph emphasized for the given timeline. @@ -566,125 +617,135 @@ def incidence( """ _filename = self.outfolder / inc_file - clausetag_n = var_name_one + '%d' - vartag_n = var_name_two + '%d' + clausetag_n = var_name_one + "%d" + vartag_n = var_name_two + "%d" g_incid = Graph( inc_file, strict=True, graph_attr={ - 'splines': 'false', - 'ranksep': '0.2', - 'nodesep': str(float(column_distance)), - 'fontsize': str( - int(fontsize)), - 'compound': 'true'}, + "splines": "false", + "ranksep": "0.2", + "nodesep": str(float(column_distance)), + "fontsize": str(int(fontsize)), + "compound": "true", + }, edge_attr={ - 'penwidth': str(float(penwidth)), - 'dir': 'back', - 'arrowtail': 'none'}) - with g_incid.subgraph(name='cluster_clause', - edge_attr={'style': 'invis'}, - node_attr={'style': 'rounded,filled', - 'fillcolor': basefill}) as clauses: - clauses.attr(label='clauses') - clauses.edges([(clausetag_n % (i + 1), clausetag_n % (i + 2)) - for i in range(len(edges) - 1)]) - - g_incid.attr('node', shape=sndshape, - penwidth=str(float(penwidth)), - style='dotted') - with g_incid.subgraph(name='cluster_ivar', - edge_attr={'style': 'invis'}) as ivars: - ivars.attr(label='variables') - ivars.edges([(vartag_n % (i + 1), vartag_n % (i + 2)) - for i in range(num_vars - 1)]) + "penwidth": str(float(penwidth)), + "dir": "back", + "arrowtail": "none", + }, + ) + with g_incid.subgraph( + name="cluster_clause", + edge_attr={"style": "invis"}, + node_attr={"style": "rounded,filled", "fillcolor": basefill}, + ) as clauses: + clauses.attr(label="clauses") + clauses.edges( + [ + (clausetag_n % (i + 1), clausetag_n % (i + 2)) + for i in range(len(edges) - 1) + ] + ) + + g_incid.attr( + "node", shape=sndshape, penwidth=str(float(penwidth)), style="dotted" + ) + with g_incid.subgraph( + name="cluster_ivar", edge_attr={"style": "invis"} + ) as ivars: + ivars.attr(label="variables") + ivars.edges( + [(vartag_n % (i + 1), vartag_n % (i + 2)) for i in range(num_vars - 1)] + ) for i in range(num_vars): - g_incid.node(vartag_n % - (i + 1), vartag_n % - (i + 1), color=colors[(i + 1) % - len(colors)]) + g_incid.node( + vartag_n % (i + 1), + vartag_n % (i + 1), + color=colors[(i + 1) % len(colors)], + ) - g_incid.attr('edge', constraint='false') + g_incid.attr("edge", constraint="false") for clause in edges: for var in clause[1]: if var >= 0: - g_incid.edge(clausetag_n % clause[0], - vartag_n % var, - color=colors[var % len(colors)]) + g_incid.edge( + clausetag_n % clause[0], + vartag_n % var, + color=colors[var % len(colors)], + ) else: - g_incid.edge(clausetag_n % clause[0], - vartag_n % -var, - color=colors[-var % len(colors)], - arrowtail=neg_tail) + g_incid.edge( + clausetag_n % clause[0], + vartag_n % -var, + color=colors[-var % len(colors)], + arrowtail=neg_tail, + ) # make edgelist variable-based (varX, clauseY), ... # var_cl_iter [(1, 1), (4, 1), ... - var_cl_iter = tuple(flatten([[(x, y[0]) for x in y[1]] - for y in edges])) + var_cl_iter = tuple(flatten([[(x, y[0]) for x in y[1]] for y in edges])) bodybaselen = len(g_incid.body) - for i, variables in enumerate(timeline, start=1): # all timesteps + for i, variables in enumerate(timeline, start=1): # all timesteps # reset highlighting g_incid.body = g_incid.body[:bodybaselen] if variables is None: - g_incid.render(view=view, format='svg', - filename=str(_filename) + str(i)) + g_incid.render( + view=view, format="svg", filename=str(_filename) + str(i) + ) continue emp_clause = { - var_cl[1] for var_cl in var_cl_iter if abs( - var_cl[0]) in variables} + var_cl[1] for var_cl in var_cl_iter if abs(var_cl[0]) in variables + } - emp_var = {abs(var_cl[0]) - for var_cl in var_cl_iter if var_cl[1] in emp_clause} + emp_var = { + abs(var_cl[0]) for var_cl in var_cl_iter if var_cl[1] in emp_clause + } for var in emp_var: _vartag = vartag_n % abs(var) - _style = 'solid,filled' if var in variables else 'dotted,filled' - g_incid.node( - _vartag, - _vartag, - style=_style, - fillcolor='yellow') + _style = "solid,filled" if var in variables else "dotted,filled" + g_incid.node(_vartag, _vartag, style=_style, fillcolor="yellow") for clause in emp_clause: g_incid.node( - clausetag_n % clause, - clausetag_n % clause, - fillcolor='yellow') + clausetag_n % clause, clausetag_n % clause, fillcolor="yellow" + ) for edge in var_cl_iter: (var, clause) = edge - _style = 'solid' if clause in emp_clause else 'dotted' + _style = "solid" if clause in emp_clause else "dotted" _vartag = vartag_n % abs(var) if var >= 0: - g_incid.edge(clausetag_n % clause, - _vartag, - color=colors[var % len(colors)], - style=_style) - else: # negated variable - g_incid.edge(clausetag_n % clause, - _vartag, - color=colors[-var % len(colors)], - arrowtail='odot', - style=_style) - - g_incid.render( - view=view, - format='svg', - filename=str(_filename) + str(i)) + g_incid.edge( + clausetag_n % clause, + _vartag, + color=colors[var % len(colors)], + style=_style, + ) + else: # negated variable + g_incid.edge( + clausetag_n % clause, + _vartag, + color=colors[-var % len(colors)], + arrowtail="odot", + style=_style, + ) + + g_incid.render(view=view, format="svg", filename=str(_filename) + str(i)) def call_svgjoin(self) -> None: """Analyzes content in data.svg_join for the call to svg_join.""" sj_data = self.data.svg_join if not sj_data.base_names: - LOGGER.warning( - "svg_join data in JsonAPI contains no file-names to join.") + LOGGER.warning("svg_join data in JsonAPI contains no file-names to join.") return if isinstance(sj_data.base_names, str): sj_data.base_names = [sj_data.base_names] @@ -712,31 +773,30 @@ def main(args: List[str]) -> None: ------- None """ - parser = get_parser( - "Visualizing Dynamic Programming on Tree-Decompositions.") + parser = get_parser("Visualizing Dynamic Programming on Tree-Decompositions.") # possible to use stdin for the file. - parser.add_argument('infile', nargs='?', - type=argparse.FileType('r', encoding='UTF-8'), - default=sys.stdin, - help="Input file for the visualization " - "must conform with the 'JsonAPI.md'") - parser.add_argument('outfolder', - help="Folder to output the visualization results") + parser.add_argument( + "infile", + nargs="?", + type=argparse.FileType("r", encoding="UTF-8"), + default=sys.stdin, + help="Input file for the visualization " "must conform with the 'JsonAPI.md'", + ) + parser.add_argument("outfolder", help="Folder to output the visualization results") # get cmd-arguments options = parser.parse_args(args) - logging_cfg(filename='logging.yml', loglevel=options.loglevel) + logging_cfg(filename="logging.yml", loglevel=options.loglevel) LOGGER.info("Called with '%s'", options) infile = options.infile outfolder = options.outfolder if not outfolder: - outfolder = 'outfolder' + outfolder = "outfolder" outfolder = Path(outfolder).resolve() - LOGGER.info("Will read from '%s' and write to folder '%s'", - infile.name, outfolder) + LOGGER.info("Will read from '%s' and write to folder '%s'", infile.name, outfolder) visu = Visualization(infile=infile, outfolder=outfolder) visu.tree_dec_timeline() diff --git a/tdvisu/visualization_data.py b/tdvisu/visualization_data.py index 439debe..8f2c9ee 100644 --- a/tdvisu/visualization_data.py +++ b/tdvisu/visualization_data.py @@ -28,11 +28,12 @@ @dataclass class SvgJoinData: """Class for holding different parameters to join the results.""" + base_names: Union[str, Iterable[str]] folder: Optional[str] = None - outname: str = 'combined' - suffix: str = '%d.svg' - preserve_aspectratio: str = 'xMinYMin' + outname: str = "combined" + suffix: str = "%d.svg" + preserve_aspectratio: str = "xMinYMin" num_images: int = 1 padding: Union[int, Iterable[int]] = 0 scale2: Union[float, Iterable[float]] = 1.0 @@ -43,38 +44,40 @@ class SvgJoinData: @dataclass class IncidenceGraphData: """Class holding different parameters for the incidence graph.""" + edges: list - subgraph_name_one: str = 'clauses' - subgraph_name_two: str = 'variables' - var_name_one: str = '' - var_name_two: str = '' + subgraph_name_one: str = "clauses" + subgraph_name_two: str = "variables" + var_name_one: str = "" + var_name_two: str = "" infer_primal: bool = False infer_dual: bool = False - primal_file: str = 'PrimalGraphStep' - inc_file: str = 'IncidenceGraphStep' - dual_file: str = 'DualGraphStep' + primal_file: str = "PrimalGraphStep" + inc_file: str = "IncidenceGraphStep" + dual_file: str = "DualGraphStep" fontsize: int = 16 penwidth: float = 2.2 - second_shape: str = 'diamond' + second_shape: str = "diamond" column_distance: float = 0.5 @dataclass class GeneralGraphData: """Class holding different parameters for the general graph.""" + edges: list extra_nodes: Optional[list] = None - graph_name: str = 'graph' - file_basename: str = 'graph' - var_name: str = '' + graph_name: str = "graph" + file_basename: str = "graph" + var_name: str = "" do_sort_nodes: bool = False do_adj_nodes: bool = False fontsize: int = 20 - first_color: str = 'yellow' - first_style: str = 'filled' - second_color: str = 'green' - second_style: str = 'dotted,filled' - third_color: str = 'red' + first_color: str = "yellow" + first_style: str = "filled" + second_color: str = "green" + second_style: str = "dotted,filled" + third_color: str = "red" def __post_init__(self): if self.extra_nodes is None: @@ -84,46 +87,51 @@ def __post_init__(self): @dataclass class VisualizationData: """Class holding different parameters for Visualization.""" + incidence_graphs: Optional[List[IncidenceGraphData]] = None general_graphs: Optional[List[GeneralGraphData]] = None svg_join: Optional[SvgJoinData] = None - td_file: str = 'TDStep' + td_file: str = "TDStep" colors: Optional[list] = None - orientation: str = 'BT' + orientation: str = "BT" linesmax: int = 100 columnsmax: int = 20 - bagcolor: str = 'white' + bagcolor: str = "white" fontsize: int = 20 penwidth: float = 2.2 - fontcolor: str = 'black' + fontcolor: str = "black" emphasis: Optional[dict] = None def __post_init__(self): if self.colors is None: self.colors = [ - '#0073a1', - '#b14923', - '#244320', - '#b1740f', - '#a682ff', - '#004066', - '#0d1321', - '#da1167', - '#604909', - '#0073a1', - '#b14923', - '#244320', - '#b1740f', - '#a682ff'] + "#0073a1", + "#b14923", + "#244320", + "#b1740f", + "#a682ff", + "#004066", + "#0d1321", + "#da1167", + "#604909", + "#0073a1", + "#b14923", + "#244320", + "#b1740f", + "#a682ff", + ] if self.emphasis is None: self.emphasis = dict() # merge input over defaults: - self.emphasis = {**{"firstcolor": 'yellow', - "secondcolor": 'green', - "firststyle": 'filled', - "secondstyle": 'dotted,filled' - }, - **self.emphasis} + self.emphasis = { + **{ + "firstcolor": "yellow", + "secondcolor": "green", + "firststyle": "filled", + "secondstyle": "dotted,filled", + }, + **self.emphasis, + } if __name__ == "__main__": # pragma: no cover diff --git a/test/expected_files/test_sat_and_join/PrimalGraphStep1 b/test/expected_files/test_sat_and_join/PrimalGraphStep1 index 059bc8e..0180a69 100644 --- a/test/expected_files/test_sat_and_join/PrimalGraphStep1 +++ b/test/expected_files/test_sat_and_join/PrimalGraphStep1 @@ -1,14 +1,14 @@ strict graph PrimalGraphStep { graph [K=2 fontsize=20 outputorder=edgesfirst overlap=false] node [fillcolor=white fontcolor=black penwidth=2.2 style=filled] - v_8 [pos="2.454100,0.606720!"] - v_1 [pos="1.592900,0.250000!"] - v_2 [pos="0.731720,0.606720!"] - v_3 [pos="0.375000,1.467900!"] - v_4 [pos="0.731720,2.329100!"] - v_5 [pos="1.592900,2.685800!"] - v_6 [pos="2.454100,2.329100!"] - v_7 [pos="2.810800,1.467900!"] + v_8 [pos="5.225600,1.082200!"] + v_1 [pos="3.216400,0.250000!"] + v_2 [pos="1.207200,1.082200!"] + v_3 [pos="0.375000,3.091400!"] + v_4 [pos="1.207200,5.100600!"] + v_5 [pos="3.216400,5.932800!"] + v_6 [pos="5.225600,5.100600!"] + v_7 [pos="6.057800,3.091400!"] v_3 -- v_8 v_1 -- v_5 v_4 -- v_6 diff --git a/test/expected_files/test_sat_and_join/PrimalGraphStep2 b/test/expected_files/test_sat_and_join/PrimalGraphStep2 index 0effa1e..437d49b 100644 --- a/test/expected_files/test_sat_and_join/PrimalGraphStep2 +++ b/test/expected_files/test_sat_and_join/PrimalGraphStep2 @@ -1,14 +1,14 @@ strict graph PrimalGraphStep { graph [K=2 fontsize=20 outputorder=edgesfirst overlap=false] node [fillcolor=white fontcolor=black penwidth=2.2 style=filled] - v_8 [pos="2.454100,0.606720!"] - v_1 [pos="1.592900,0.250000!"] - v_2 [pos="0.731720,0.606720!"] - v_3 [pos="0.375000,1.467900!"] - v_4 [pos="0.731720,2.329100!"] - v_5 [pos="1.592900,2.685800!"] - v_6 [pos="2.454100,2.329100!"] - v_7 [pos="2.810800,1.467900!"] + v_8 [pos="5.225600,1.082200!"] + v_1 [pos="3.216400,0.250000!"] + v_2 [pos="1.207200,1.082200!"] + v_3 [pos="0.375000,3.091400!"] + v_4 [pos="1.207200,5.100600!"] + v_5 [pos="3.216400,5.932800!"] + v_6 [pos="5.225600,5.100600!"] + v_7 [pos="6.057800,3.091400!"] v_3 -- v_8 v_1 -- v_5 v_4 -- v_6 diff --git a/test/expected_files/test_sat_and_join/PrimalGraphStep3 b/test/expected_files/test_sat_and_join/PrimalGraphStep3 index dbc4132..8c369fd 100644 --- a/test/expected_files/test_sat_and_join/PrimalGraphStep3 +++ b/test/expected_files/test_sat_and_join/PrimalGraphStep3 @@ -1,14 +1,14 @@ strict graph PrimalGraphStep { graph [K=2 fontsize=20 outputorder=edgesfirst overlap=false] node [fillcolor=white fontcolor=black penwidth=2.2 style=filled] - v_8 [pos="2.454100,0.606720!"] - v_1 [pos="1.592900,0.250000!"] - v_2 [pos="0.731720,0.606720!"] - v_3 [pos="0.375000,1.467900!"] - v_4 [pos="0.731720,2.329100!"] - v_5 [pos="1.592900,2.685800!"] - v_6 [pos="2.454100,2.329100!"] - v_7 [pos="2.810800,1.467900!"] + v_8 [pos="5.225600,1.082200!"] + v_1 [pos="3.216400,0.250000!"] + v_2 [pos="1.207200,1.082200!"] + v_3 [pos="0.375000,3.091400!"] + v_4 [pos="1.207200,5.100600!"] + v_5 [pos="3.216400,5.932800!"] + v_6 [pos="5.225600,5.100600!"] + v_7 [pos="6.057800,3.091400!"] v_3 -- v_8 v_1 -- v_5 v_4 -- v_6 diff --git a/test/expected_files/test_sat_and_join/PrimalGraphStep4 b/test/expected_files/test_sat_and_join/PrimalGraphStep4 index 110797e..3ba1703 100644 --- a/test/expected_files/test_sat_and_join/PrimalGraphStep4 +++ b/test/expected_files/test_sat_and_join/PrimalGraphStep4 @@ -1,14 +1,14 @@ strict graph PrimalGraphStep { graph [K=2 fontsize=20 outputorder=edgesfirst overlap=false] node [fillcolor=white fontcolor=black penwidth=2.2 style=filled] - v_8 [pos="2.454100,0.606720!"] - v_1 [pos="1.592900,0.250000!"] - v_2 [pos="0.731720,0.606720!"] - v_3 [pos="0.375000,1.467900!"] - v_4 [pos="0.731720,2.329100!"] - v_5 [pos="1.592900,2.685800!"] - v_6 [pos="2.454100,2.329100!"] - v_7 [pos="2.810800,1.467900!"] + v_8 [pos="5.225600,1.082200!"] + v_1 [pos="3.216400,0.250000!"] + v_2 [pos="1.207200,1.082200!"] + v_3 [pos="0.375000,3.091400!"] + v_4 [pos="1.207200,5.100600!"] + v_5 [pos="3.216400,5.932800!"] + v_6 [pos="5.225600,5.100600!"] + v_7 [pos="6.057800,3.091400!"] v_3 -- v_8 v_1 -- v_5 v_4 -- v_6 diff --git a/test/expected_files/test_sat_and_join/PrimalGraphStep5 b/test/expected_files/test_sat_and_join/PrimalGraphStep5 index c296a41..6bc3ba7 100644 --- a/test/expected_files/test_sat_and_join/PrimalGraphStep5 +++ b/test/expected_files/test_sat_and_join/PrimalGraphStep5 @@ -1,14 +1,14 @@ strict graph PrimalGraphStep { graph [K=2 fontsize=20 outputorder=edgesfirst overlap=false] node [fillcolor=white fontcolor=black penwidth=2.2 style=filled] - v_8 [pos="2.454100,0.606720!"] - v_1 [pos="1.592900,0.250000!"] - v_2 [pos="0.731720,0.606720!"] - v_3 [pos="0.375000,1.467900!"] - v_4 [pos="0.731720,2.329100!"] - v_5 [pos="1.592900,2.685800!"] - v_6 [pos="2.454100,2.329100!"] - v_7 [pos="2.810800,1.467900!"] + v_8 [pos="5.225600,1.082200!"] + v_1 [pos="3.216400,0.250000!"] + v_2 [pos="1.207200,1.082200!"] + v_3 [pos="0.375000,3.091400!"] + v_4 [pos="1.207200,5.100600!"] + v_5 [pos="3.216400,5.932800!"] + v_6 [pos="5.225600,5.100600!"] + v_7 [pos="6.057800,3.091400!"] v_3 -- v_8 v_1 -- v_5 v_4 -- v_6 diff --git a/test/expected_files/test_sat_and_join/PrimalGraphStep6 b/test/expected_files/test_sat_and_join/PrimalGraphStep6 index 9a442ff..bcbd712 100644 --- a/test/expected_files/test_sat_and_join/PrimalGraphStep6 +++ b/test/expected_files/test_sat_and_join/PrimalGraphStep6 @@ -1,14 +1,14 @@ strict graph PrimalGraphStep { graph [K=2 fontsize=20 outputorder=edgesfirst overlap=false] node [fillcolor=white fontcolor=black penwidth=2.2 style=filled] - v_8 [pos="2.454100,0.606720!"] - v_1 [pos="1.592900,0.250000!"] - v_2 [pos="0.731720,0.606720!"] - v_3 [pos="0.375000,1.467900!"] - v_4 [pos="0.731720,2.329100!"] - v_5 [pos="1.592900,2.685800!"] - v_6 [pos="2.454100,2.329100!"] - v_7 [pos="2.810800,1.467900!"] + v_8 [pos="5.225600,1.082200!"] + v_1 [pos="3.216400,0.250000!"] + v_2 [pos="1.207200,1.082200!"] + v_3 [pos="0.375000,3.091400!"] + v_4 [pos="1.207200,5.100600!"] + v_5 [pos="3.216400,5.932800!"] + v_6 [pos="5.225600,5.100600!"] + v_7 [pos="6.057800,3.091400!"] v_3 -- v_8 v_1 -- v_5 v_4 -- v_6 diff --git a/test/expected_files/test_vc_multiple_and_join/TDStep1.svg b/test/expected_files/test_vc_multiple_and_join/TDStep1.svg index a9bad39..f572ba3 100644 --- a/test/expected_files/test_vc_multiple_and_join/TDStep1.svg +++ b/test/expected_files/test_vc_multiple_and_join/TDStep1.svg @@ -1,73 +1,73 @@ - - - + + Tree-Decomposition - + bag 1 - - -bag 1 -[1, 3, 5, 6] -dtime=0.0009s + + +bag 1 +[1, 3, 5, 6] +dtime=0.0009s bag 2 - - -bag 2 -[1, 3, 4, 5] -dtime=0.0026s + + +bag 2 +[1, 3, 4, 5] +dtime=0.0026s bag 2->bag 1 - - + + bag 3 - - -bag 3 -[1, 2, 3, 6] -dtime=0.0020s + + +bag 3 +[1, 2, 3, 6] +dtime=0.0020s bag 3->bag 1 - - + + bag 4 - - -bag 4 -[1, 2, 6, 7] -dtime=0.0017s + + +bag 4 +[1, 2, 6, 7] +dtime=0.0017s bag 4->bag 3 - - + + diff --git a/test/expected_files/test_vc_multiple_and_join/TDStep2.svg b/test/expected_files/test_vc_multiple_and_join/TDStep2.svg index 9951c19..5d8bea3 100644 --- a/test/expected_files/test_vc_multiple_and_join/TDStep2.svg +++ b/test/expected_files/test_vc_multiple_and_join/TDStep2.svg @@ -1,133 +1,133 @@ - - - + + Tree-Decomposition - + bag 1 - - -bag 1 -[1, 3, 5, 6] -dtime=0.0009s + + +bag 1 +[1, 3, 5, 6] +dtime=0.0009s bag 2 - - -bag 2 -[1, 3, 4, 5] -dtime=0.0026s + + +bag 2 +[1, 3, 4, 5] +dtime=0.0026s bag 2->bag 1 - - + + sol2 - -sol bag 2 - -v1 - -1 - -1 - -1 - -1 - -0 - -v3 - -1 - -0 - -0 - -1 - -1 - -v5 - -0 - -1 - -0 - -1 - -1 - -size - -3 - -3 - -2 - -3 - -3 - -min-size: 2 + +sol bag 2 + +v1 + +1 + +1 + +1 + +1 + +0 + +v3 + +1 + +0 + +0 + +1 + +1 + +v5 + +0 + +1 + +0 + +1 + +1 + +size + +3 + +3 + +2 + +3 + +3 + +min-size: 2 bag 2->sol2 - - + + bag 3 - - -bag 3 -[1, 2, 3, 6] -dtime=0.0020s + + +bag 3 +[1, 2, 3, 6] +dtime=0.0020s bag 3->bag 1 - - + + bag 4 - - -bag 4 -[1, 2, 6, 7] -dtime=0.0017s + + +bag 4 +[1, 2, 6, 7] +dtime=0.0017s bag 4->bag 3 - - + + diff --git a/test/expected_files/test_vc_multiple_and_join/TDStep3.svg b/test/expected_files/test_vc_multiple_and_join/TDStep3.svg index 3785276..7321e21 100644 --- a/test/expected_files/test_vc_multiple_and_join/TDStep3.svg +++ b/test/expected_files/test_vc_multiple_and_join/TDStep3.svg @@ -1,195 +1,195 @@ - - - + + Tree-Decomposition - + bag 1 - - -bag 1 -[1, 3, 5, 6] -dtime=0.0009s + + +bag 1 +[1, 3, 5, 6] +dtime=0.0009s bag 2 - - -bag 2 -[1, 3, 4, 5] -dtime=0.0026s + + +bag 2 +[1, 3, 4, 5] +dtime=0.0026s bag 2->bag 1 - - + + sol2 - -sol bag 2 - -v1 - -1 - -1 - -1 - -1 - -0 - -v3 - -1 - -0 - -0 - -1 - -1 - -v5 - -0 - -1 - -0 - -1 - -1 - -size - -3 - -3 - -2 - -3 - -3 - -min-size: 2 + +sol bag 2 + +v1 + +1 + +1 + +1 + +1 + +0 + +v3 + +1 + +0 + +0 + +1 + +1 + +v5 + +0 + +1 + +0 + +1 + +1 + +size + +3 + +3 + +2 + +3 + +3 + +min-size: 2 bag 2->sol2 - - + + bag 3 - - -bag 3 -[1, 2, 3, 6] -dtime=0.0020s + + +bag 3 +[1, 2, 3, 6] +dtime=0.0020s bag 3->bag 1 - - + + bag 4 - - -bag 4 -[1, 2, 6, 7] -dtime=0.0017s + + +bag 4 +[1, 2, 6, 7] +dtime=0.0017s bag 4->bag 3 - - + + sol4 - -sol bag 4 - -v1 - -1 - -1 - -1 - -1 - -0 - -v2 - -1 - -0 - -0 - -1 - -1 - -v6 - -0 - -1 - -0 - -1 - -1 - -size - -3 - -3 - -2 - -3 - -3 - -min-size: 2 + +sol bag 4 + +v1 + +1 + +1 + +1 + +1 + +0 + +v2 + +1 + +0 + +0 + +1 + +1 + +v6 + +0 + +1 + +0 + +1 + +1 + +size + +3 + +3 + +2 + +3 + +3 + +min-size: 2 bag 4->sol4 - - + + diff --git a/test/expected_files/test_vc_multiple_and_join/TDStep4.svg b/test/expected_files/test_vc_multiple_and_join/TDStep4.svg index 756dec9..2c2f6de 100644 --- a/test/expected_files/test_vc_multiple_and_join/TDStep4.svg +++ b/test/expected_files/test_vc_multiple_and_join/TDStep4.svg @@ -1,255 +1,255 @@ - - - + + Tree-Decomposition - + bag 1 - - -bag 1 -[1, 3, 5, 6] -dtime=0.0009s + + +bag 1 +[1, 3, 5, 6] +dtime=0.0009s bag 2 - - -bag 2 -[1, 3, 4, 5] -dtime=0.0026s + + +bag 2 +[1, 3, 4, 5] +dtime=0.0026s bag 2->bag 1 - - + + sol2 - -sol bag 2 - -v1 - -1 - -1 - -1 - -1 - -0 - -v3 - -1 - -0 - -0 - -1 - -1 - -v5 - -0 - -1 - -0 - -1 - -1 - -size - -3 - -3 - -2 - -3 - -3 - -min-size: 2 + +sol bag 2 + +v1 + +1 + +1 + +1 + +1 + +0 + +v3 + +1 + +0 + +0 + +1 + +1 + +v5 + +0 + +1 + +0 + +1 + +1 + +size + +3 + +3 + +2 + +3 + +3 + +min-size: 2 bag 2->sol2 - - + + bag 3 - - -bag 3 -[1, 2, 3, 6] -dtime=0.0020s + + +bag 3 +[1, 2, 3, 6] +dtime=0.0020s bag 3->bag 1 - - + + sol3 - -sol bag 3 - -v1 - -1 - -1 - -1 - -1 - -0 - -v3 - -1 - -0 - -1 - -0 - -1 - -v6 - -0 - -1 - -1 - -0 - -1 - -size - -3 - -3 - -4 - -3 - -4 - -min-size: 3 + +sol bag 3 + +v1 + +1 + +1 + +1 + +1 + +0 + +v3 + +1 + +0 + +1 + +0 + +1 + +v6 + +0 + +1 + +1 + +0 + +1 + +size + +3 + +3 + +4 + +3 + +4 + +min-size: 3 bag 3->sol3 - - + + bag 4 - - -bag 4 -[1, 2, 6, 7] -dtime=0.0017s + + +bag 4 +[1, 2, 6, 7] +dtime=0.0017s bag 4->bag 3 - - + + sol4 - -sol bag 4 - -v1 - -1 - -1 - -1 - -1 - -0 - -v2 - -1 - -0 - -0 - -1 - -1 - -v6 - -0 - -1 - -0 - -1 - -1 - -size - -3 - -3 - -2 - -3 - -3 - -min-size: 2 + +sol bag 4 + +v1 + +1 + +1 + +1 + +1 + +0 + +v2 + +1 + +0 + +0 + +1 + +1 + +v6 + +0 + +1 + +0 + +1 + +1 + +size + +3 + +3 + +2 + +3 + +3 + +min-size: 2 bag 4->sol4 - - + + diff --git a/test/expected_files/test_vc_multiple_and_join/TDStep5.svg b/test/expected_files/test_vc_multiple_and_join/TDStep5.svg index 9e477db..e7ccc47 100644 --- a/test/expected_files/test_vc_multiple_and_join/TDStep5.svg +++ b/test/expected_files/test_vc_multiple_and_join/TDStep5.svg @@ -1,347 +1,347 @@ - - - + + Tree-Decomposition - + bag 1 - - -bag 1 -[1, 3, 5, 6] -dtime=0.0009s + + +bag 1 +[1, 3, 5, 6] +dtime=0.0009s sol1 - -sol bag 1 - -v1 - -1 - -1 - -1 - -0 - -1 - -1 - -1 - -v3 - -0 - -1 - -0 - -1 - -0 - -1 - -1 - -v5 - -1 - -0 - -0 - -1 - -1 - -1 - -1 - -v6 - -1 - -1 - -1 - -1 - -0 - -1 - -0 - -size - -5 - -5 - -4 - -6 - -5 - -5 - -4 - -min-size: 4 + +sol bag 1 + +v1 + +1 + +1 + +1 + +0 + +1 + +1 + +1 + +v3 + +0 + +1 + +0 + +1 + +0 + +1 + +1 + +v5 + +1 + +0 + +0 + +1 + +1 + +1 + +1 + +v6 + +1 + +1 + +1 + +1 + +0 + +1 + +0 + +size + +5 + +5 + +4 + +6 + +5 + +5 + +4 + +min-size: 4 bag 1->sol1 - - + + bag 2 - - -bag 2 -[1, 3, 4, 5] -dtime=0.0026s + + +bag 2 +[1, 3, 4, 5] +dtime=0.0026s bag 2->bag 1 - - + + sol2 - -sol bag 2 - -v1 - -1 - -1 - -1 - -1 - -0 - -v3 - -1 - -0 - -0 - -1 - -1 - -v5 - -0 - -1 - -0 - -1 - -1 - -size - -3 - -3 - -2 - -3 - -3 - -min-size: 2 + +sol bag 2 + +v1 + +1 + +1 + +1 + +1 + +0 + +v3 + +1 + +0 + +0 + +1 + +1 + +v5 + +0 + +1 + +0 + +1 + +1 + +size + +3 + +3 + +2 + +3 + +3 + +min-size: 2 bag 2->sol2 - - + + bag 3 - - -bag 3 -[1, 2, 3, 6] -dtime=0.0020s + + +bag 3 +[1, 2, 3, 6] +dtime=0.0020s bag 3->bag 1 - - + + sol3 - -sol bag 3 - -v1 - -1 - -1 - -1 - -1 - -0 - -v3 - -1 - -0 - -1 - -0 - -1 - -v6 - -0 - -1 - -1 - -0 - -1 - -size - -3 - -3 - -4 - -3 - -4 - -min-size: 3 + +sol bag 3 + +v1 + +1 + +1 + +1 + +1 + +0 + +v3 + +1 + +0 + +1 + +0 + +1 + +v6 + +0 + +1 + +1 + +0 + +1 + +size + +3 + +3 + +4 + +3 + +4 + +min-size: 3 bag 3->sol3 - - + + bag 4 - - -bag 4 -[1, 2, 6, 7] -dtime=0.0017s + + +bag 4 +[1, 2, 6, 7] +dtime=0.0017s bag 4->bag 3 - - + + sol4 - -sol bag 4 - -v1 - -1 - -1 - -1 - -1 - -0 - -v2 - -1 - -0 - -0 - -1 - -1 - -v6 - -0 - -1 - -0 - -1 - -1 - -size - -3 - -3 - -2 - -3 - -3 - -min-size: 2 + +sol bag 4 + +v1 + +1 + +1 + +1 + +1 + +0 + +v2 + +1 + +0 + +0 + +1 + +1 + +v6 + +0 + +1 + +0 + +1 + +1 + +size + +3 + +3 + +2 + +3 + +3 + +min-size: 2 bag 4->sol4 - - + + diff --git a/test/expected_files/test_vc_multiple_and_join/combined1.svg b/test/expected_files/test_vc_multiple_and_join/combined1.svg index 3935313..7751b40 100644 --- a/test/expected_files/test_vc_multiple_and_join/combined1.svg +++ b/test/expected_files/test_vc_multiple_and_join/combined1.svg @@ -1,228 +1,228 @@ - - + + Tree-Decomposition - + bag 1 - - - bag 1 - [1, 3, 5, 6] - dtime=0.0009s + + + bag 1 + [1, 3, 5, 6] + dtime=0.0009s bag 2 - - - bag 2 - [1, 3, 4, 5] - dtime=0.0026s + + + bag 2 + [1, 3, 4, 5] + dtime=0.0026s bag 2->bag 1 - - + + bag 3 - - - bag 3 - [1, 2, 3, 6] - dtime=0.0020s + + + bag 3 + [1, 2, 3, 6] + dtime=0.0020s bag 3->bag 1 - - + + bag 4 - - - bag 4 - [1, 2, 6, 7] - dtime=0.0017s + + + bag 4 + [1, 2, 6, 7] + dtime=0.0017s bag 4->bag 3 - - + + - + graph - + 1--2 - + 1--3 - + 1--4 - + 1--7 - + 1--5 - + 1--6 - + 2--3 - + 2--7 - + 3--4 - + 4--5 - + 5--6 - + 6--7 - + 1 - - 1 + + 1 2 - - 2 + + 2 3 - - 3 + + 3 4 - - 4 + + 4 7 - - 7 + + 7 5 - - 5 + + 5 6 - - 6 + + 6 - + graph_sorted - + 1--7 - + 1--2 - + 1--3 - + 1--4 - + 1--5 - + 1--6 - + 2--7 - + 2--3 - + 3--4 - + 4--5 - + 5--6 - + 6--7 - + 7 - - 7 + + 7 1 - - 1 + + 1 2 - - 2 + + 2 3 - - 3 + + 3 4 - - 4 + + 4 5 - - 5 + + 5 6 - - 6 + + 6 \ No newline at end of file diff --git a/test/expected_files/test_vc_multiple_and_join/combined2.svg b/test/expected_files/test_vc_multiple_and_join/combined2.svg index 045271b..8080e79 100644 --- a/test/expected_files/test_vc_multiple_and_join/combined2.svg +++ b/test/expected_files/test_vc_multiple_and_join/combined2.svg @@ -1,288 +1,288 @@ - - + + Tree-Decomposition - + bag 1 - - - bag 1 - [1, 3, 5, 6] - dtime=0.0009s + + + bag 1 + [1, 3, 5, 6] + dtime=0.0009s bag 2 - - - bag 2 - [1, 3, 4, 5] - dtime=0.0026s + + + bag 2 + [1, 3, 4, 5] + dtime=0.0026s bag 2->bag 1 - - + + sol2 - - sol bag 2 - v1 - 1 - 1 - 1 - 1 - 0 - v3 - 1 - 0 - 0 - 1 - 1 - v5 - 0 - 1 - 0 - 1 - 1 - size - 3 - 3 - 2 - 3 - 3 - min-size: 2 - - - - - - - - - - - - - - - - - - - - - - - - - + + sol bag 2 + v1 + 1 + 1 + 1 + 1 + 0 + v3 + 1 + 0 + 0 + 1 + 1 + v5 + 0 + 1 + 0 + 1 + 1 + size + 3 + 3 + 2 + 3 + 3 + min-size: 2 + + + + + + + + + + + + + + + + + + + + + + + + + bag 2->sol2 - - + + bag 3 - - - bag 3 - [1, 2, 3, 6] - dtime=0.0020s + + + bag 3 + [1, 2, 3, 6] + dtime=0.0020s bag 3->bag 1 - - + + bag 4 - - - bag 4 - [1, 2, 6, 7] - dtime=0.0017s + + + bag 4 + [1, 2, 6, 7] + dtime=0.0017s bag 4->bag 3 - - + + - + graph - + 1--2 - + 1--3 - + 1--4 - + 1--7 - + 1--5 - + 1--6 - + 2--3 - + 2--7 - + 3--4 - + 4--5 - + 5--6 - + 6--7 - + 1 - - 1 + + 1 2 - - 2 + + 2 3 - - 3 + + 3 4 - - 4 + + 4 7 - - 7 + + 7 5 - - 5 + + 5 6 - - 6 + + 6 - + graph_sorted - + 1--7 - + 1--2 - + 1--3 - + 1--4 - + 1--5 - + 1--6 - + 2--7 - + 2--3 - + 3--4 - + 4--5 - + 5--6 - + 6--7 - + 7 - - 7 + + 7 1 - - 1 + + 1 2 - - 2 + + 2 3 - - 3 + + 3 4 - - 4 + + 4 5 - - 5 + + 5 6 - - 6 + + 6 \ No newline at end of file diff --git a/test/expected_files/test_vc_multiple_and_join/combined3.svg b/test/expected_files/test_vc_multiple_and_join/combined3.svg index 3fec518..cd52f24 100644 --- a/test/expected_files/test_vc_multiple_and_join/combined3.svg +++ b/test/expected_files/test_vc_multiple_and_join/combined3.svg @@ -1,348 +1,348 @@ - - + + Tree-Decomposition - + bag 1 - - - bag 1 - [1, 3, 5, 6] - dtime=0.0009s + + + bag 1 + [1, 3, 5, 6] + dtime=0.0009s bag 2 - - - bag 2 - [1, 3, 4, 5] - dtime=0.0026s + + + bag 2 + [1, 3, 4, 5] + dtime=0.0026s bag 2->bag 1 - - + + sol2 - - sol bag 2 - v1 - 1 - 1 - 1 - 1 - 0 - v3 - 1 - 0 - 0 - 1 - 1 - v5 - 0 - 1 - 0 - 1 - 1 - size - 3 - 3 - 2 - 3 - 3 - min-size: 2 - - - - - - - - - - - - - - - - - - - - - - - - - + + sol bag 2 + v1 + 1 + 1 + 1 + 1 + 0 + v3 + 1 + 0 + 0 + 1 + 1 + v5 + 0 + 1 + 0 + 1 + 1 + size + 3 + 3 + 2 + 3 + 3 + min-size: 2 + + + + + + + + + + + + + + + + + + + + + + + + + bag 2->sol2 - - + + bag 3 - - - bag 3 - [1, 2, 3, 6] - dtime=0.0020s + + + bag 3 + [1, 2, 3, 6] + dtime=0.0020s bag 3->bag 1 - - + + bag 4 - - - bag 4 - [1, 2, 6, 7] - dtime=0.0017s + + + bag 4 + [1, 2, 6, 7] + dtime=0.0017s bag 4->bag 3 - - + + sol4 - - sol bag 4 - v1 - 1 - 1 - 1 - 1 - 0 - v2 - 1 - 0 - 0 - 1 - 1 - v6 - 0 - 1 - 0 - 1 - 1 - size - 3 - 3 - 2 - 3 - 3 - min-size: 2 - - - - - - - - - - - - - - - - - - - - - - - - - + + sol bag 4 + v1 + 1 + 1 + 1 + 1 + 0 + v2 + 1 + 0 + 0 + 1 + 1 + v6 + 0 + 1 + 0 + 1 + 1 + size + 3 + 3 + 2 + 3 + 3 + min-size: 2 + + + + + + + + + + + + + + + + + + + + + + + + + bag 4->sol4 - - + + - + graph - + 1--2 - + 1--3 - + 1--4 - + 1--7 - + 1--5 - + 1--6 - + 2--3 - + 2--7 - + 3--4 - + 4--5 - + 5--6 - + 6--7 - + 1 - - 1 + + 1 2 - - 2 + + 2 3 - - 3 + + 3 4 - - 4 + + 4 7 - - 7 + + 7 5 - - 5 + + 5 6 - - 6 + + 6 - + graph_sorted - + 1--7 - + 1--2 - + 1--3 - + 1--4 - + 1--5 - + 1--6 - + 2--7 - + 2--3 - + 3--4 - + 4--5 - + 5--6 - + 6--7 - + 7 - - 7 + + 7 1 - - 1 + + 1 2 - - 2 + + 2 3 - - 3 + + 3 4 - - 4 + + 4 5 - - 5 + + 5 6 - - 6 + + 6 \ No newline at end of file diff --git a/test/expected_files/test_vc_multiple_and_join/combined4.svg b/test/expected_files/test_vc_multiple_and_join/combined4.svg index 8e179c8..d51ed35 100644 --- a/test/expected_files/test_vc_multiple_and_join/combined4.svg +++ b/test/expected_files/test_vc_multiple_and_join/combined4.svg @@ -1,408 +1,408 @@ - - + + Tree-Decomposition - + bag 1 - - - bag 1 - [1, 3, 5, 6] - dtime=0.0009s + + + bag 1 + [1, 3, 5, 6] + dtime=0.0009s bag 2 - - - bag 2 - [1, 3, 4, 5] - dtime=0.0026s + + + bag 2 + [1, 3, 4, 5] + dtime=0.0026s bag 2->bag 1 - - + + sol2 - - sol bag 2 - v1 - 1 - 1 - 1 - 1 - 0 - v3 - 1 - 0 - 0 - 1 - 1 - v5 - 0 - 1 - 0 - 1 - 1 - size - 3 - 3 - 2 - 3 - 3 - min-size: 2 - - - - - - - - - - - - - - - - - - - - - - - - - + + sol bag 2 + v1 + 1 + 1 + 1 + 1 + 0 + v3 + 1 + 0 + 0 + 1 + 1 + v5 + 0 + 1 + 0 + 1 + 1 + size + 3 + 3 + 2 + 3 + 3 + min-size: 2 + + + + + + + + + + + + + + + + + + + + + + + + + bag 2->sol2 - - + + bag 3 - - - bag 3 - [1, 2, 3, 6] - dtime=0.0020s + + + bag 3 + [1, 2, 3, 6] + dtime=0.0020s bag 3->bag 1 - - + + sol3 - - sol bag 3 - v1 - 1 - 1 - 1 - 1 - 0 - v3 - 1 - 0 - 1 - 0 - 1 - v6 - 0 - 1 - 1 - 0 - 1 - size - 3 - 3 - 4 - 3 - 4 - min-size: 3 - - - - - - - - - - - - - - - - - - - - - - - - - + + sol bag 3 + v1 + 1 + 1 + 1 + 1 + 0 + v3 + 1 + 0 + 1 + 0 + 1 + v6 + 0 + 1 + 1 + 0 + 1 + size + 3 + 3 + 4 + 3 + 4 + min-size: 3 + + + + + + + + + + + + + + + + + + + + + + + + + bag 3->sol3 - - + + bag 4 - - - bag 4 - [1, 2, 6, 7] - dtime=0.0017s + + + bag 4 + [1, 2, 6, 7] + dtime=0.0017s bag 4->bag 3 - - + + sol4 - - sol bag 4 - v1 - 1 - 1 - 1 - 1 - 0 - v2 - 1 - 0 - 0 - 1 - 1 - v6 - 0 - 1 - 0 - 1 - 1 - size - 3 - 3 - 2 - 3 - 3 - min-size: 2 - - - - - - - - - - - - - - - - - - - - - - - - - + + sol bag 4 + v1 + 1 + 1 + 1 + 1 + 0 + v2 + 1 + 0 + 0 + 1 + 1 + v6 + 0 + 1 + 0 + 1 + 1 + size + 3 + 3 + 2 + 3 + 3 + min-size: 2 + + + + + + + + + + + + + + + + + + + + + + + + + bag 4->sol4 - - + + - + graph - + 1--2 - + 1--3 - + 1--4 - + 1--7 - + 1--5 - + 1--6 - + 2--3 - + 2--7 - + 3--4 - + 4--5 - + 5--6 - + 6--7 - + 1 - - 1 + + 1 2 - - 2 + + 2 3 - - 3 + + 3 4 - - 4 + + 4 7 - - 7 + + 7 5 - - 5 + + 5 6 - - 6 + + 6 - + graph_sorted - + 1--7 - + 1--2 - + 1--3 - + 1--4 - + 1--5 - + 1--6 - + 2--7 - + 2--3 - + 3--4 - + 4--5 - + 5--6 - + 6--7 - + 7 - - 7 + + 7 1 - - 1 + + 1 2 - - 2 + + 2 3 - - 3 + + 3 4 - - 4 + + 4 5 - - 5 + + 5 6 - - 6 + + 6 \ No newline at end of file diff --git a/test/expected_files/test_vc_multiple_and_join/combined5.svg b/test/expected_files/test_vc_multiple_and_join/combined5.svg index ec73d6a..68173f4 100644 --- a/test/expected_files/test_vc_multiple_and_join/combined5.svg +++ b/test/expected_files/test_vc_multiple_and_join/combined5.svg @@ -1,500 +1,500 @@ - - + + Tree-Decomposition - + bag 1 - - - bag 1 - [1, 3, 5, 6] - dtime=0.0009s + + + bag 1 + [1, 3, 5, 6] + dtime=0.0009s sol1 - - sol bag 1 - v1 - 1 - 1 - 1 - 0 - 1 - 1 - 1 - v3 - 0 - 1 - 0 - 1 - 0 - 1 - 1 - v5 - 1 - 0 - 0 - 1 - 1 - 1 - 1 - v6 - 1 - 1 - 1 - 1 - 0 - 1 - 0 - size - 5 - 5 - 4 - 6 - 5 - 5 - 4 - min-size: 4 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + sol bag 1 + v1 + 1 + 1 + 1 + 0 + 1 + 1 + 1 + v3 + 0 + 1 + 0 + 1 + 0 + 1 + 1 + v5 + 1 + 0 + 0 + 1 + 1 + 1 + 1 + v6 + 1 + 1 + 1 + 1 + 0 + 1 + 0 + size + 5 + 5 + 4 + 6 + 5 + 5 + 4 + min-size: 4 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + bag 1->sol1 - - + + bag 2 - - - bag 2 - [1, 3, 4, 5] - dtime=0.0026s + + + bag 2 + [1, 3, 4, 5] + dtime=0.0026s bag 2->bag 1 - - + + sol2 - - sol bag 2 - v1 - 1 - 1 - 1 - 1 - 0 - v3 - 1 - 0 - 0 - 1 - 1 - v5 - 0 - 1 - 0 - 1 - 1 - size - 3 - 3 - 2 - 3 - 3 - min-size: 2 - - - - - - - - - - - - - - - - - - - - - - - - - + + sol bag 2 + v1 + 1 + 1 + 1 + 1 + 0 + v3 + 1 + 0 + 0 + 1 + 1 + v5 + 0 + 1 + 0 + 1 + 1 + size + 3 + 3 + 2 + 3 + 3 + min-size: 2 + + + + + + + + + + + + + + + + + + + + + + + + + bag 2->sol2 - - + + bag 3 - - - bag 3 - [1, 2, 3, 6] - dtime=0.0020s + + + bag 3 + [1, 2, 3, 6] + dtime=0.0020s bag 3->bag 1 - - + + sol3 - - sol bag 3 - v1 - 1 - 1 - 1 - 1 - 0 - v3 - 1 - 0 - 1 - 0 - 1 - v6 - 0 - 1 - 1 - 0 - 1 - size - 3 - 3 - 4 - 3 - 4 - min-size: 3 - - - - - - - - - - - - - - - - - - - - - - - - - + + sol bag 3 + v1 + 1 + 1 + 1 + 1 + 0 + v3 + 1 + 0 + 1 + 0 + 1 + v6 + 0 + 1 + 1 + 0 + 1 + size + 3 + 3 + 4 + 3 + 4 + min-size: 3 + + + + + + + + + + + + + + + + + + + + + + + + + bag 3->sol3 - - + + bag 4 - - - bag 4 - [1, 2, 6, 7] - dtime=0.0017s + + + bag 4 + [1, 2, 6, 7] + dtime=0.0017s bag 4->bag 3 - - + + sol4 - - sol bag 4 - v1 - 1 - 1 - 1 - 1 - 0 - v2 - 1 - 0 - 0 - 1 - 1 - v6 - 0 - 1 - 0 - 1 - 1 - size - 3 - 3 - 2 - 3 - 3 - min-size: 2 - - - - - - - - - - - - - - - - - - - - - - - - - + + sol bag 4 + v1 + 1 + 1 + 1 + 1 + 0 + v2 + 1 + 0 + 0 + 1 + 1 + v6 + 0 + 1 + 0 + 1 + 1 + size + 3 + 3 + 2 + 3 + 3 + min-size: 2 + + + + + + + + + + + + + + + + + + + + + + + + + bag 4->sol4 - - + + - + graph - + 1--2 - + 1--3 - + 1--4 - + 1--7 - + 1--5 - + 1--6 - + 2--3 - + 2--7 - + 3--4 - + 4--5 - + 5--6 - + 6--7 - + 1 - - 1 + + 1 2 - - 2 + + 2 3 - - 3 + + 3 4 - - 4 + + 4 7 - - 7 + + 7 5 - - 5 + + 5 6 - - 6 + + 6 - + graph_sorted - + 1--7 - + 1--2 - + 1--3 - + 1--4 - + 1--5 - + 1--6 - + 2--7 - + 2--3 - + 3--4 - + 4--5 - + 5--6 - + 6--7 - + 7 - - 7 + + 7 1 - - 1 + + 1 2 - - 2 + + 2 3 - - 3 + + 3 4 - - 4 + + 4 5 - - 5 + + 5 6 - - 6 + + 6 \ No newline at end of file diff --git a/test/expected_files/test_vc_multiple_and_join/graph1.svg b/test/expected_files/test_vc_multiple_and_join/graph1.svg index 548b301..6d3efe8 100644 --- a/test/expected_files/test_vc_multiple_and_join/graph1.svg +++ b/test/expected_files/test_vc_multiple_and_join/graph1.svg @@ -1,96 +1,115 @@ - - - + + graph - + -1--2 - + +1--2 + -1--3 - + +1--3 + -1--4 - + +1--4 + -1--7 - + +1--7 + -1--5 - + +1--5 + -1--6 - + +1--6 + -2--3 - + +2--3 + -2--7 - + +2--7 + -3--4 - + +3--4 + -4--5 - + +4--5 + -5--6 - + +5--6 + -6--7 - + +6--7 + -1 - -1 + +1 + +1 -2 - -2 + +2 + +2 -3 - -3 + +3 + +3 -4 - -4 + +4 + +4 -7 - -7 + +7 + +7 -5 - -5 + +5 + +5 -6 - -6 + +6 + +6 diff --git a/test/expected_files/test_vc_multiple_and_join/graph2.svg b/test/expected_files/test_vc_multiple_and_join/graph2.svg index f0ba461..3ed11d9 100644 --- a/test/expected_files/test_vc_multiple_and_join/graph2.svg +++ b/test/expected_files/test_vc_multiple_and_join/graph2.svg @@ -1,96 +1,115 @@ - - - + + graph - + -1--2 - + +1--2 + -1--3 - + +1--3 + -1--4 - + +1--4 + -1--7 - + +1--7 + -1--5 - + +1--5 + -1--6 - + +1--6 + -2--3 - + +2--3 + -2--7 - + +2--7 + -3--4 - + +3--4 + -4--5 - + +4--5 + -5--6 - + +5--6 + -6--7 - + +6--7 + -1 - -1 + +1 + +1 -2 - -2 + +2 + +2 -3 - -3 + +3 + +3 -4 - -4 + +4 + +4 -7 - -7 + +7 + +7 -5 - -5 + +5 + +5 -6 - -6 + +6 + +6 diff --git a/test/expected_files/test_vc_multiple_and_join/graph3.svg b/test/expected_files/test_vc_multiple_and_join/graph3.svg index 96d1005..191faf2 100644 --- a/test/expected_files/test_vc_multiple_and_join/graph3.svg +++ b/test/expected_files/test_vc_multiple_and_join/graph3.svg @@ -1,96 +1,115 @@ - - - + + graph - + -1--2 - + +1--2 + -1--3 - + +1--3 + -1--4 - + +1--4 + -1--7 - + +1--7 + -1--5 - + +1--5 + -1--6 - + +1--6 + -2--3 - + +2--3 + -2--7 - + +2--7 + -3--4 - + +3--4 + -4--5 - + +4--5 + -5--6 - + +5--6 + -6--7 - + +6--7 + -1 - -1 + +1 + +1 -2 - -2 + +2 + +2 -3 - -3 + +3 + +3 -4 - -4 + +4 + +4 -7 - -7 + +7 + +7 -5 - -5 + +5 + +5 -6 - -6 + +6 + +6 diff --git a/test/expected_files/test_vc_multiple_and_join/graph4.svg b/test/expected_files/test_vc_multiple_and_join/graph4.svg index d36118b..385570f 100644 --- a/test/expected_files/test_vc_multiple_and_join/graph4.svg +++ b/test/expected_files/test_vc_multiple_and_join/graph4.svg @@ -1,96 +1,115 @@ - - - + + graph - + -1--2 - + +1--2 + -1--3 - + +1--3 + -1--4 - + +1--4 + -1--7 - + +1--7 + -1--5 - + +1--5 + -1--6 - + +1--6 + -2--3 - + +2--3 + -2--7 - + +2--7 + -3--4 - + +3--4 + -4--5 - + +4--5 + -5--6 - + +5--6 + -6--7 - + +6--7 + -1 - -1 + +1 + +1 -2 - -2 + +2 + +2 -3 - -3 + +3 + +3 -4 - -4 + +4 + +4 -7 - -7 + +7 + +7 -5 - -5 + +5 + +5 -6 - -6 + +6 + +6 diff --git a/test/expected_files/test_vc_multiple_and_join/graph5.svg b/test/expected_files/test_vc_multiple_and_join/graph5.svg index e4e0160..5734227 100644 --- a/test/expected_files/test_vc_multiple_and_join/graph5.svg +++ b/test/expected_files/test_vc_multiple_and_join/graph5.svg @@ -1,96 +1,115 @@ - - - + + graph - + -1--2 - + +1--2 + -1--3 - + +1--3 + -1--4 - + +1--4 + -1--7 - + +1--7 + -1--5 - + +1--5 + -1--6 - + +1--6 + -2--3 - + +2--3 + -2--7 - + +2--7 + -3--4 - + +3--4 + -4--5 - + +4--5 + -5--6 - + +5--6 + -6--7 - + +6--7 + -1 - -1 + +1 + +1 -2 - -2 + +2 + +2 -3 - -3 + +3 + +3 -4 - -4 + +4 + +4 -7 - -7 + +7 + +7 -5 - -5 + +5 + +5 -6 - -6 + +6 + +6 diff --git a/test/expected_files/test_vc_multiple_and_join/graph_sorted1 b/test/expected_files/test_vc_multiple_and_join/graph_sorted1 index c0cdf42..bc6e88a 100644 --- a/test/expected_files/test_vc_multiple_and_join/graph_sorted1 +++ b/test/expected_files/test_vc_multiple_and_join/graph_sorted1 @@ -1,13 +1,13 @@ strict graph graph_sorted { graph [K=2 fontsize=18 outputorder=edgesfirst overlap=false] node [fillcolor=white fontcolor=black penwidth=2.2 style=filled] - 7 [pos="2.097100,0.468120!"] - 1 [pos="1.141400,0.250000!"] - 2 [pos="0.375000,0.861170!"] - 3 [pos="0.375000,1.841400!"] - 4 [pos="1.141400,2.452600!"] - 5 [pos="2.097100,2.234500!"] - 6 [pos="2.522400,1.351300!"] + 7 [pos="4.592900,0.784260!"] + 1 [pos="2.252100,0.250000!"] + 2 [pos="0.375000,1.747000!"] + 3 [pos="0.375000,4.147900!"] + 4 [pos="2.252100,5.644900!"] + 5 [pos="4.592900,5.110600!"] + 6 [pos="5.634600,2.947400!"] 1 -- 2 3 -- 4 2 -- 7 diff --git a/test/expected_files/test_vc_multiple_and_join/graph_sorted1.svg b/test/expected_files/test_vc_multiple_and_join/graph_sorted1.svg index 7048b11..a6ea1d3 100644 --- a/test/expected_files/test_vc_multiple_and_join/graph_sorted1.svg +++ b/test/expected_files/test_vc_multiple_and_join/graph_sorted1.svg @@ -1,96 +1,115 @@ - - - + + graph_sorted - + -1--7 - + +1--7 + -1--2 - + +1--2 + -1--3 - + +1--3 + -1--4 - + +1--4 + -1--5 - + +1--5 + -1--6 - + +1--6 + -2--7 - + +2--7 + -2--3 - + +2--3 + -3--4 - + +3--4 + -4--5 - + +4--5 + -5--6 - + +5--6 + -6--7 - + +6--7 + -7 - -7 + +7 + +7 -1 - -1 + +1 + +1 -2 - -2 + +2 + +2 -3 - -3 + +3 + +3 -4 - -4 + +4 + +4 -5 - -5 + +5 + +5 -6 - -6 + +6 + +6 diff --git a/test/expected_files/test_vc_multiple_and_join/graph_sorted2 b/test/expected_files/test_vc_multiple_and_join/graph_sorted2 index 2ebaf9e..8e80553 100644 --- a/test/expected_files/test_vc_multiple_and_join/graph_sorted2 +++ b/test/expected_files/test_vc_multiple_and_join/graph_sorted2 @@ -1,13 +1,13 @@ strict graph graph_sorted { graph [K=2 fontsize=18 outputorder=edgesfirst overlap=false] node [fillcolor=white fontcolor=black penwidth=2.2 style=filled] - 7 [pos="2.097100,0.468120!"] - 1 [pos="1.141400,0.250000!"] - 2 [pos="0.375000,0.861170!"] - 3 [pos="0.375000,1.841400!"] - 4 [pos="1.141400,2.452600!"] - 5 [pos="2.097100,2.234500!"] - 6 [pos="2.522400,1.351300!"] + 7 [pos="4.592900,0.784260!"] + 1 [pos="2.252100,0.250000!"] + 2 [pos="0.375000,1.747000!"] + 3 [pos="0.375000,4.147900!"] + 4 [pos="2.252100,5.644900!"] + 5 [pos="4.592900,5.110600!"] + 6 [pos="5.634600,2.947400!"] 1 -- 2 3 -- 4 2 -- 7 diff --git a/test/expected_files/test_vc_multiple_and_join/graph_sorted2.svg b/test/expected_files/test_vc_multiple_and_join/graph_sorted2.svg index 2acef2b..6d71370 100644 --- a/test/expected_files/test_vc_multiple_and_join/graph_sorted2.svg +++ b/test/expected_files/test_vc_multiple_and_join/graph_sorted2.svg @@ -1,96 +1,115 @@ - - - + + graph_sorted - + -1--7 - + +1--7 + -1--2 - + +1--2 + -1--3 - + +1--3 + -1--4 - + +1--4 + -1--5 - + +1--5 + -1--6 - + +1--6 + -2--7 - + +2--7 + -2--3 - + +2--3 + -3--4 - + +3--4 + -4--5 - + +4--5 + -5--6 - + +5--6 + -6--7 - + +6--7 + -7 - -7 + +7 + +7 -1 - -1 + +1 + +1 -2 - -2 + +2 + +2 -3 - -3 + +3 + +3 -4 - -4 + +4 + +4 -5 - -5 + +5 + +5 -6 - -6 + +6 + +6 diff --git a/test/expected_files/test_vc_multiple_and_join/graph_sorted3 b/test/expected_files/test_vc_multiple_and_join/graph_sorted3 index 3b7d32e..ab397ea 100644 --- a/test/expected_files/test_vc_multiple_and_join/graph_sorted3 +++ b/test/expected_files/test_vc_multiple_and_join/graph_sorted3 @@ -1,13 +1,13 @@ strict graph graph_sorted { graph [K=2 fontsize=18 outputorder=edgesfirst overlap=false] node [fillcolor=white fontcolor=black penwidth=2.2 style=filled] - 7 [pos="2.097100,0.468120!"] - 1 [pos="1.141400,0.250000!"] - 2 [pos="0.375000,0.861170!"] - 3 [pos="0.375000,1.841400!"] - 4 [pos="1.141400,2.452600!"] - 5 [pos="2.097100,2.234500!"] - 6 [pos="2.522400,1.351300!"] + 7 [pos="4.592900,0.784260!"] + 1 [pos="2.252100,0.250000!"] + 2 [pos="0.375000,1.747000!"] + 3 [pos="0.375000,4.147900!"] + 4 [pos="2.252100,5.644900!"] + 5 [pos="4.592900,5.110600!"] + 6 [pos="5.634600,2.947400!"] 1 -- 2 3 -- 4 2 -- 7 diff --git a/test/expected_files/test_vc_multiple_and_join/graph_sorted3.svg b/test/expected_files/test_vc_multiple_and_join/graph_sorted3.svg index 70ff87a..935c43d 100644 --- a/test/expected_files/test_vc_multiple_and_join/graph_sorted3.svg +++ b/test/expected_files/test_vc_multiple_and_join/graph_sorted3.svg @@ -1,96 +1,115 @@ - - - + + graph_sorted - + -1--7 - + +1--7 + -1--2 - + +1--2 + -1--3 - + +1--3 + -1--4 - + +1--4 + -1--5 - + +1--5 + -1--6 - + +1--6 + -2--7 - + +2--7 + -2--3 - + +2--3 + -3--4 - + +3--4 + -4--5 - + +4--5 + -5--6 - + +5--6 + -6--7 - + +6--7 + -7 - -7 + +7 + +7 -1 - -1 + +1 + +1 -2 - -2 + +2 + +2 -3 - -3 + +3 + +3 -4 - -4 + +4 + +4 -5 - -5 + +5 + +5 -6 - -6 + +6 + +6 diff --git a/test/expected_files/test_vc_multiple_and_join/graph_sorted4 b/test/expected_files/test_vc_multiple_and_join/graph_sorted4 index 2c39ccb..70f8b9a 100644 --- a/test/expected_files/test_vc_multiple_and_join/graph_sorted4 +++ b/test/expected_files/test_vc_multiple_and_join/graph_sorted4 @@ -1,13 +1,13 @@ strict graph graph_sorted { graph [K=2 fontsize=18 outputorder=edgesfirst overlap=false] node [fillcolor=white fontcolor=black penwidth=2.2 style=filled] - 7 [pos="2.097100,0.468120!"] - 1 [pos="1.141400,0.250000!"] - 2 [pos="0.375000,0.861170!"] - 3 [pos="0.375000,1.841400!"] - 4 [pos="1.141400,2.452600!"] - 5 [pos="2.097100,2.234500!"] - 6 [pos="2.522400,1.351300!"] + 7 [pos="4.592900,0.784260!"] + 1 [pos="2.252100,0.250000!"] + 2 [pos="0.375000,1.747000!"] + 3 [pos="0.375000,4.147900!"] + 4 [pos="2.252100,5.644900!"] + 5 [pos="4.592900,5.110600!"] + 6 [pos="5.634600,2.947400!"] 1 -- 2 3 -- 4 2 -- 7 diff --git a/test/expected_files/test_vc_multiple_and_join/graph_sorted4.svg b/test/expected_files/test_vc_multiple_and_join/graph_sorted4.svg index e0feb09..4604411 100644 --- a/test/expected_files/test_vc_multiple_and_join/graph_sorted4.svg +++ b/test/expected_files/test_vc_multiple_and_join/graph_sorted4.svg @@ -1,96 +1,115 @@ - - - + + graph_sorted - + -1--7 - + +1--7 + -1--2 - + +1--2 + -1--3 - + +1--3 + -1--4 - + +1--4 + -1--5 - + +1--5 + -1--6 - + +1--6 + -2--7 - + +2--7 + -2--3 - + +2--3 + -3--4 - + +3--4 + -4--5 - + +4--5 + -5--6 - + +5--6 + -6--7 - + +6--7 + -7 - -7 + +7 + +7 -1 - -1 + +1 + +1 -2 - -2 + +2 + +2 -3 - -3 + +3 + +3 -4 - -4 + +4 + +4 -5 - -5 + +5 + +5 -6 - -6 + +6 + +6 diff --git a/test/expected_files/test_vc_multiple_and_join/graph_sorted5 b/test/expected_files/test_vc_multiple_and_join/graph_sorted5 index 49751a1..a780f61 100644 --- a/test/expected_files/test_vc_multiple_and_join/graph_sorted5 +++ b/test/expected_files/test_vc_multiple_and_join/graph_sorted5 @@ -1,13 +1,13 @@ strict graph graph_sorted { graph [K=2 fontsize=18 outputorder=edgesfirst overlap=false] node [fillcolor=white fontcolor=black penwidth=2.2 style=filled] - 7 [pos="2.097100,0.468120!"] - 1 [pos="1.141400,0.250000!"] - 2 [pos="0.375000,0.861170!"] - 3 [pos="0.375000,1.841400!"] - 4 [pos="1.141400,2.452600!"] - 5 [pos="2.097100,2.234500!"] - 6 [pos="2.522400,1.351300!"] + 7 [pos="4.592900,0.784260!"] + 1 [pos="2.252100,0.250000!"] + 2 [pos="0.375000,1.747000!"] + 3 [pos="0.375000,4.147900!"] + 4 [pos="2.252100,5.644900!"] + 5 [pos="4.592900,5.110600!"] + 6 [pos="5.634600,2.947400!"] 1 -- 2 3 -- 4 2 -- 7 diff --git a/test/expected_files/test_vc_multiple_and_join/graph_sorted5.svg b/test/expected_files/test_vc_multiple_and_join/graph_sorted5.svg index 144fc9b..9db0ad9 100644 --- a/test/expected_files/test_vc_multiple_and_join/graph_sorted5.svg +++ b/test/expected_files/test_vc_multiple_and_join/graph_sorted5.svg @@ -1,96 +1,115 @@ - - - + + graph_sorted - + -1--7 - + +1--7 + -1--2 - + +1--2 + -1--3 - + +1--3 + -1--4 - + +1--4 + -1--5 - + +1--5 + -1--6 - + +1--6 + -2--7 - + +2--7 + -2--3 - + +2--3 + -3--4 - + +3--4 + -4--5 - + +4--5 + -5--6 - + +5--6 + -6--7 - + +6--7 + -7 - -7 + +7 + +7 -1 - -1 + +1 + +1 -2 - -2 + +2 + +2 -3 - -3 + +3 + +3 -4 - -4 + +4 + +4 -5 - -5 + +5 + +5 -6 - -6 + +6 + +6 diff --git a/test/test_construct_dpdb.py b/test/test_construct_dpdb.py index 2e991de..6bfd653 100644 --- a/test/test_construct_dpdb.py +++ b/test/test_construct_dpdb.py @@ -23,8 +23,9 @@ import datetime from pathlib import Path +from unittest.mock import PropertyMock -import psycopg2 as pg +import psycopg as pg from tdvisu import construct_dpdb_visu as module from tdvisu.construct_dpdb_visu import ( @@ -35,48 +36,52 @@ IDpdbVisuConstruct, db_config, main, - read_cfg) + read_cfg, +) DIR = Path(__file__).parent -SECTION = 'postgresql' +SECTION = "postgresql" def test_db_config(): """Test reading the database configuration from the test file.""" - result = read_cfg(DIR / 'database.ini', SECTION, True) - assert result == {'application_name': 'dpdb-admin', - 'host': 'localhost', - 'password': 'XXX', - 'port': '123', - 'user': 'postgres' - }, "should be able to read from ini file" - - result = db_config(DIR / 'database.ini', SECTION) - assert result == {'application_name': 'dpdb-admin', - 'database': DEFAULT_DBCONFIG['database'], - 'host': 'localhost', - 'password': 'XXX', - 'port': '123', - 'user': 'postgres' - }, "should complete 'database' with default." + result = read_cfg(DIR / "database.ini", SECTION, True) + assert result == { + "application_name": "dpdb-admin", + "host": "localhost", + "password": "XXX", + "port": "123", + "user": "postgres", + }, "should be able to read from ini file" + + result = db_config(DIR / "database.ini", SECTION) + assert result == { + "application_name": "dpdb-admin", + "database": DEFAULT_DBCONFIG["database"], + "host": "localhost", + "password": "XXX", + "port": "123", + "user": "postgres", + }, "should complete 'database' with default." def test_db2_config(): """Test should use defaults when file not found.""" - fixed_defaults = db_config(DIR / 'database2.ini', SECTION) + fixed_defaults = db_config(DIR / "database2.ini", SECTION) assert fixed_defaults == DEFAULT_DBCONFIG def test_db_passwd_config(): """Test should add password from file to defaults.""" - insert_passwd = db_config(DIR / 'db_password.ini', SECTION) - assert insert_passwd == {'application_name': 'dpdb-admin', - 'database': 'logicsem', - 'host': 'localhost', - 'password': 'XXX', - 'port': 5432, - 'user': 'postgres' - } + insert_passwd = db_config(DIR / "db_password.ini", SECTION) + assert insert_passwd == { + "application_name": "dpdb-admin", + "database": "logicsem", + "host": "localhost", + "password": "XXX", + "port": 5432, + "user": "postgres", + } def test_problem_interface(): @@ -89,64 +94,76 @@ def test_problem_interface(): def test_main(mocker, tmp_path): """Test behaviour of construct_dpdb_visu.main""" - mock_connect = mocker.patch('tdvisu.construct_dpdb_visu.pg.connect') - ta_status = mock_connect.return_value.__enter__.return_value.get_transaction_status - ta_status.return_value = pg.extensions.TRANSACTION_STATUS_IDLE + mock_connect = mocker.patch("tdvisu.construct_dpdb_visu.pg.connect") + mock_status = mocker.patch( + "tdvisu.construct_dpdb_visu.pg.connect.return_value.__enter__.return_value.info" + ) + type(mock_status).status = PropertyMock( + side_effect=[pg.pq.ConnStatus.BAD, pg.pq.ConnStatus.OK] + ) query_problem = mocker.patch( - 'tdvisu.construct_dpdb_visu.query_problem', - return_value='Sat') - query_num_vars = mocker.patch('tdvisu.construct_dpdb_visu.query_num_vars', - return_value=8) + "tdvisu.construct_dpdb_visu.query_problem", return_value="Sat" + ) + query_num_vars = mocker.patch( + "tdvisu.construct_dpdb_visu.query_num_vars", return_value=8 + ) query_td_node_status_ordered = mocker.patch( - 'tdvisu.construct_dpdb_visu.query_td_node_status_ordered', - return_value=[(3,), (5,), (4,), (2,), (1,)]) + "tdvisu.construct_dpdb_visu.query_td_node_status_ordered", + return_value=[(3,), (5,), (4,), (2,), (1,)], + ) query_sat_clause = mocker.patch( - 'tdvisu.construct_dpdb_visu.query_sat_clause', - return_value=[(True, None, None, True, None, True, None, None, None, None), - (True, None, None, None, False, - None, None, None, None, None), - (False, None, None, None, None, - None, True, None, None, None), - (None, True, True, None, None, None, None, None, None, None), - (None, True, None, None, True, None, None, None, None, None), - (None, True, None, None, None, - False, None, None, None, None), - (None, None, True, None, None, - None, None, False, None, None), - (None, None, None, True, None, - None, None, False, None, None), - (None, None, None, False, None, - True, None, None, None, None), - (None, None, None, False, None, None, True, None, None, None)]) + "tdvisu.construct_dpdb_visu.query_sat_clause", + return_value=[ + (True, None, None, True, None, True, None, None, None, None), + (True, None, None, None, False, None, None, None, None, None), + (False, None, None, None, None, None, True, None, None, None), + (None, True, True, None, None, None, None, None, None, None), + (None, True, None, None, True, None, None, None, None, None), + (None, True, None, None, None, False, None, None, None, None), + (None, None, True, None, None, None, None, False, None, None), + (None, None, None, True, None, None, None, False, None, None), + (None, None, None, False, None, True, None, None, None, None), + (None, None, None, False, None, None, True, None, None, None), + ], + ) query_td_bag_grouped = mocker.patch( - 'tdvisu.construct_dpdb_visu.query_td_bag_grouped', return_value=[[1, 2, 3, 4, 5]]) + "tdvisu.construct_dpdb_visu.query_td_bag_grouped", + return_value=[[1, 2, 3, 4, 5]], + ) query_td_node_status = mocker.patch( - 'tdvisu.construct_dpdb_visu.query_td_node_status', + "tdvisu.construct_dpdb_visu.query_td_node_status", return_value=( "2020-07-13 02:06:18.053880", - datetime.timedelta( - microseconds=768))) - query_td_bag = mocker.patch('tdvisu.construct_dpdb_visu.query_td_bag', - return_value=[(1,), (2,), (4,), (6,)]) + datetime.timedelta(microseconds=768), + ), + ) + query_td_bag = mocker.patch( + "tdvisu.construct_dpdb_visu.query_td_bag", return_value=[(1,), (2,), (4,), (6,)] + ) query_column_name = mocker.patch( - 'tdvisu.construct_dpdb_visu.query_column_name', return_value=[ - ('v1',), ('v2',), ('v4',), ('v6',)]) + "tdvisu.construct_dpdb_visu.query_column_name", + return_value=[("v1",), ("v2",), ("v4",), ("v6",)], + ) query_bag = mocker.patch( - 'tdvisu.construct_dpdb_visu.query_bag', - return_value=[(False, None, False, None), - (True, None, True, None), - (False, None, True, None), - (True, None, False, None)]) + "tdvisu.construct_dpdb_visu.query_bag", + return_value=[ + (False, None, False, None), + (True, None, True, None), + (False, None, True, None), + (True, None, False, None), + ], + ) query_edgearray = mocker.patch( - 'tdvisu.construct_dpdb_visu.query_edgearray', return_value=[ - (2, 1), (3, 2), (4, 2), (5, 4)]) + "tdvisu.construct_dpdb_visu.query_edgearray", + return_value=[(2, 1), (3, 2), (4, 2), (5, 4)], + ) # set cmd-arguments - outfile = str(tmp_path / 'test_main.json') + outfile = str(tmp_path / "test_main.json") # one mocked run - main(['1', '--outfile', outfile]) + main(["1", "--outfile", outfile]) # Assertions mock_connect.assert_called_once() @@ -166,7 +183,7 @@ def test_init(mocker): """Test that main is called correctly if called as __main__.""" expected = -1000 main = mocker.patch.object(module, "main", return_value=expected) - mock_exit = mocker.patch.object(module.sys, 'exit') + mock_exit = mocker.patch.object(module.sys, "exit") mocker.patch.object(module, "__name__", "__main__") module.init() diff --git a/test/test_reader.py b/test/test_reader.py index 50bb064..fb7934f 100644 --- a/test/test_reader.py +++ b/test/test_reader.py @@ -21,7 +21,6 @@ """ import logging - from pathlib import Path import pytest @@ -31,7 +30,7 @@ def test_reader_valid_input(): """Create and test the reader on valid input from a file.""" - twfile = Path(__file__).parent / 'grda16.tw' + twfile = Path(__file__).parent / "grda16.tw" # from filename reader = TwReader.from_filename(twfile) _reader_assertions(reader) @@ -51,7 +50,7 @@ def test_reader_valid_input(): def test_reader_commented_body(): """Create and test the reader on valid input from a file with comments in the body.""" - twfile = Path(__file__).parent / 'grda16_comments.tw' + twfile = Path(__file__).parent / "grda16_comments.tw" # from filename reader = TwReader.from_filename(twfile) _reader_assertions(reader) @@ -77,7 +76,7 @@ def test_dimacsreader_has_body(): def test_reader_inval_preamble(caplog): """Test message when a unexpected token in the preamble was encountered.""" - twfile = Path(__file__).parent / 'grda16.tw' + twfile = Path(__file__).parent / "grda16.tw" # from string with open(twfile) as file: content = "invalid preamble\n" + file.read() @@ -86,7 +85,8 @@ def test_reader_inval_preamble(caplog): assert ( "reader.py", logging.WARN, - "Invalid content in preamble at line 0: invalid preamble") in caplog.record_tuples + "Invalid content in preamble at line 0: invalid preamble", + ) in caplog.record_tuples def test_reader_no_type_found(caplog): @@ -94,9 +94,11 @@ def test_reader_no_type_found(caplog): content = "c no preamble\n" with pytest.raises(SystemExit) as pytest_wrapped_e: TwReader.from_string(content) # should raise SystemExit - assert ("reader.py", - logging.ERROR, - "No type found in DIMACS file!") in caplog.record_tuples + assert ( + "reader.py", + logging.ERROR, + "No type found in DIMACS file!", + ) in caplog.record_tuples assert pytest_wrapped_e.type == SystemExit assert pytest_wrapped_e.value.code == 1 @@ -106,26 +108,26 @@ def test_dimacs_wrong_type_found(caplog): content = "p wrong 16 31\n1 2\n2 1\n" with pytest.raises(SystemExit) as pytest_wrapped_e: TwReader.from_string(content) # should raise SystemExit - assert ("reader.py", - logging.ERROR, - "Not a tw file!") in caplog.record_tuples + assert ("reader.py", logging.ERROR, "Not a tw file!") in caplog.record_tuples assert pytest_wrapped_e.type == SystemExit assert pytest_wrapped_e.value.code == 1 def test_dimacs_col_long_line(caplog): """Test message when long line was read in the body.""" - colfile = Path(__file__).parent / 'col_with_long_line.col' + colfile = Path(__file__).parent / "col_with_long_line.col" TwReader.from_filename(colfile) # should raise SystemExit assert ( "reader.py", logging.WARN, - "Expected exactly 2 vertices at line 1, but 3 found") in caplog.record_tuples + "Expected exactly 2 vertices at line 1, but 3 found", + ) in caplog.record_tuples assert ( "reader.py", logging.WARN, - "Expected exactly 2 vertices at line 1, but 3 found") in caplog.record_tuples + "Expected exactly 2 vertices at line 1, but 3 found", + ) in caplog.record_tuples def test_dimacs_fewer_edges(caplog): @@ -135,7 +137,8 @@ def test_dimacs_fewer_edges(caplog): assert ( "reader.py", logging.WARN, - "Number of edges mismatch preamble (2 vs 3)") in caplog.record_tuples + "Number of edges mismatch preamble (2 vs 3)", + ) in caplog.record_tuples assert reader.num_vertices == 3 assert reader.num_edges == 3 assert len(reader.edges) == 2 @@ -146,18 +149,63 @@ def _reader_assertions(reader: TwReader): as well as the number of vertices and number of edges. """ - expected_edges = {(1, 2), (2, 1), (2, 3), (3, 2), (3, 4), (3, 5), - (4, 3), (4, 5), (4, 6), (5, 3), (5, 4), (6, 4), - (6, 7), (6, 15), (7, 6), (7, 8), (7, 14), (8, 7), - (8, 9), (9, 8), (9, 10), (9, 11), (10, 9), - (11, 9), (11, 12), (11, 14), (12, 11), (12, 13), - (12, 14), (13, 12), (14, 7), (14, 11), (14, 12), - (15, 6), (15, 16), (16, 15)} - - expected_adj = {1: {2}, 2: {1, 3}, 3: {2, 4, 5}, 4: {3, 5, 6}, - 5: {3, 4}, 6: {4, 7, 15}, 7: {6, 8, 14}, 8: {7, 9}, - 9: {8, 10, 11}, 10: {9}, 11: {9, 12, 14}, 12: {11, 13, 14}, - 13: {12}, 14: {7, 11, 12}, 15: {6, 16}, 16: {15}} + expected_edges = { + (1, 2), + (2, 1), + (2, 3), + (3, 2), + (3, 4), + (3, 5), + (4, 3), + (4, 5), + (4, 6), + (5, 3), + (5, 4), + (6, 4), + (6, 7), + (6, 15), + (7, 6), + (7, 8), + (7, 14), + (8, 7), + (8, 9), + (9, 8), + (9, 10), + (9, 11), + (10, 9), + (11, 9), + (11, 12), + (11, 14), + (12, 11), + (12, 13), + (12, 14), + (13, 12), + (14, 7), + (14, 11), + (14, 12), + (15, 6), + (15, 16), + (16, 15), + } + + expected_adj = { + 1: {2}, + 2: {1, 3}, + 3: {2, 4, 5}, + 4: {3, 5, 6}, + 5: {3, 4}, + 6: {4, 7, 15}, + 7: {6, 8, 14}, + 8: {7, 9}, + 9: {8, 10, 11}, + 10: {9}, + 11: {9, 12, 14}, + 12: {11, 13, 14}, + 13: {12}, + 14: {7, 11, 12}, + 15: {6, 16}, + 16: {15}, + } assert reader.num_vertices == 16 assert reader.num_edges == 36