Repository: tulip-control/dd Branch: main Commit: 2596de454f95 Files: 68 Total size: 753.2 KB Directory structure: gitextract__x1aidr9/ ├── .gitattributes ├── .github/ │ └── workflows/ │ ├── main.yml │ └── setup_build_env.sh ├── .gitignore ├── AUTHORS ├── CHANGES.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── dd/ │ ├── __init__.py │ ├── _abc.py │ ├── _copy.py │ ├── _parser.py │ ├── _utils.py │ ├── autoref.py │ ├── bdd.py │ ├── buddy.pyx │ ├── buddy_.pxd │ ├── c_sylvan.pxd │ ├── cudd.pyx │ ├── cudd_zdd.pyx │ ├── dddmp.py │ ├── mdd.py │ ├── py.typed │ └── sylvan.pyx ├── doc.md ├── download.py ├── examples/ │ ├── README.md │ ├── _test_examples.py │ ├── bdd_traversal.py │ ├── boolean_satisfiability.py │ ├── cudd_configure_reordering.py │ ├── cudd_memory_limits.py │ ├── cudd_statistics.py │ ├── cudd_zdd.py │ ├── good_vs_bad_variable_order.py │ ├── install_dd_buddy.sh │ ├── install_dd_cudd.sh │ ├── install_dd_sylvan.sh │ ├── json_example.py │ ├── json_load.py │ ├── np.py │ ├── queens.py │ ├── reachability.py │ ├── reordering.py │ ├── transfer_bdd.py │ ├── variable_substitution.py │ └── what_is_a_bdd.py ├── setup.py └── tests/ ├── .coveragerc ├── README.md ├── autoref_test.py ├── bdd_test.py ├── common.py ├── common_bdd.py ├── common_cudd.py ├── copy_test.py ├── cudd_test.py ├── cudd_zdd_test.py ├── dddmp_test.py ├── inspect_cython_signatures.py ├── iterative_recursive_flattener.py ├── mdd_test.py ├── parser_test.py ├── pytest.ini ├── regressions_test.py └── sylvan_test.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ *.py eol=lf *.pyx eol=lf *.pxd eol=lf *.c eol=lf *.txt eol=lf *.md eol=lf *.yml eol=lf * filter=trimWhitespace ================================================ FILE: .github/workflows/main.yml ================================================ --- # configuration for GitHub Actions name: dd tests on: push: pull_request: schedule: - cron: '37 5 5 * *' jobs: build: name: Build runs-on: ubuntu-24.04 strategy: matrix: python-version: [ '3.11', '3.12', '3.13', '3.14', ] steps: - uses: actions/checkout@v5 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - name: Prepare installation environment run: | ./.github/workflows/setup_build_env.sh - name: Install `dd` run: | set -o posix echo 'Exported environment variables:' export -p export \ DD_FETCH=1 \ DD_CUDD=1 \ DD_CUDD_ZDD=1 \ DD_SYLVAN=1 pip install . \ --verbose \ --use-pep517 \ --no-build-isolation - name: Install test dependencies run: | pip install pytest - name: Run `dd` tests run: | set -o posix echo 'Exported environment variables:' export -p # run tests make test - name: Run `dd` examples run: | pushd examples/ python _test_examples.py popd ================================================ FILE: .github/workflows/setup_build_env.sh ================================================ #!/usr/bin/env bash # Prepare environment for building `dd`. set -x set -e sudo apt install \ graphviz dot -V pip install --upgrade \ pip \ setuptools \ wheel # note that installing from `requirements.txt` # would also install packages that # may be absent from where `dd` will be installed pip install cython # # install `sylvan` # download curl -L \ https://github.com/utwente-fmt/sylvan/tarball/v1.0.0 \ -o sylvan.tar.gz # checksum echo "9877fe07a8cfe9889152e29624a4c5b283\ cb34672ec524ccb3edb313b3057fbf8ef45622a4\ 9796fae17aa24e0baea5ccfa18f1bc5923e3c552\ 45ab3e3c1927c8 sylvan.tar.gz" | shasum -a 512 -c - # unpack mkdir sylvan tar xzf sylvan.tar.gz -C sylvan --strip=1 pushd sylvan autoreconf -fi ./configure make export LD_LIBRARY_PATH=`pwd`/src/.libs:$LD_LIBRARY_PATH echo $LD_LIBRARY_PATH if [[ -z "${DEPLOY_ENV}" ]]; then # store values to use in later steps for # environment variables echo "LD_LIBRARY_PATH=$LD_LIBRARY_PATH" \ >> $GITHUB_ENV fi popd ================================================ FILE: .gitignore ================================================ .coverage .cache .mypy_cache/ .pytype/ .pytest_cache/ todo.txt dd/_version.py cython_debug/* cudd*/* extern/ dd/CUDD_LICENSE dd/GLIBC_COPYING.LIB dd/GLIBC_LICENSES dd/PYTHON_LICENSE buddy-*/ sylvan/ .DS_Store ._.DS_Store *.out *_state_machine.py *.c __pycache__ *.pyc *.whl *.pdf *.png *.svg *.txt *.p *.json *.dddmp *.so *.html *.tar.gz dist/* build/* *.egg-info *.swp *.tags *.xml ================================================ FILE: AUTHORS ================================================ People that have authored commits to `dd`. Ioannis Filippidis Sofie Haesaert Scott C. Livingston Mario Wenzel ================================================ FILE: CHANGES.md ================================================ # dd changelog ## 0.6.1 - DEP: rm module `dd._compat` ## 0.6.0 - REL: require Python >= 3.11 - REL: require `cython >= 3.0.0` - REL: require `astutils >= 0.0.5` - DEP: deprecate hidden module `dd._compat` API: - use TLA+ syntax for comments: - `(* this is a doubly-delimited comment *)` - `\* this is a trailing comment` - use symbol `#` as operator that means the logical negation of `<=>`. The symbol `#` no longer signifies comments. - return `list` of loaded roots of BDDs, when loading BDDs from Pickle files in: - `dd.autoref.BDD.load()` - `dd.bdd.BDD.load()` - in `dd.cudd`, `dd.cudd_zdd` check available memory only in systems where both `SC_PAGE_SIZE` and `SC_PHYS_PAGES` are defined (read `sysconf(3)`). - raise `ValueError` from: - `dd.autoref.BDD.level_of_var()` - `dd.bdd.BDD.level_of_var()` whenever unknown values are given as arguments - rename: - method `dd.buddy.BDD.level()` to `dd.buddy.BDD.level_of_var()` - method `dd.buddy.BDD.at_level()` to `dd.buddy.BDD.var_at_level()` as the naming convention in other `dd` modules - raise `ValueError` from: - `dd.buddy.BDD.level_of_var()` - `dd.buddy.BDD.var_at_level()` whenever unknown values are given as arguments, as done in other `dd` modules too - several `assert` statements replaced by `raise`, with more specific exceptions, e.g., `ValueError`, `TypeError`, `RuntimeError` - strings returned by methods: - `dd.cudd.Function.__repr__()` - `dd.cudd_zdd.Function.__repr__()` changed to follow specification of `object.__repr__()` (delimited by `<` and `>`). Now also includes the object `id` as `hex` number. ## 0.5.7 - require `pytest >= 4.6.11`, instead of `nose`, for Python 3.10 compatibility - support for dumping and loading BDDs to and from JSON files now requires Python 3 - test using GitHub Actions API: - return memory size in bytes from methods `dd.cudd.BDD.statistics` and `dd.cudd_zdd.ZDD.statistics` (the value of key `'mem'` in the returned `dict`) - print used memory in bytes in the methods `dd.cudd.BDD.__str__` and `dd.cudd_zdd.ZDD.__str__` - remove the now obsolete constants `dd.cudd.GB`, `dd.cudd_zdd.GB` - remove the unused constant `dd.sylvan.GB` - method `dd.cudd_zdd.ZDD.dump`: - support PNG and SVG formats, in addition to PDF - allow more than one references to ZDD nodes in the argument `roots` - add method `apply` to the class `dd.mdd.MDD` - several `assert` statements replaced by `raise` with exceptions more specific than `AssertionError` - set `dd.cudd.Function.node` and `dd.cudd_zdd.Function.node` to `NULL` when the (local) reference count becomes zero ## 0.5.6 - distribute `manylinux2014_x86_64` wheel via PyPI API: - require `cython >= 0.29.15` - add module `dd.cudd_zdd` - allow empty support variable names in DDDMP files in function `dd.dddmp.load` - methods `dump` and `load` of the classes `dd.cudd.BDD` and `dd.autoref.BDD`: - add JSON to file types - load by file extension - change return type of method `dd.cudd.BDD.load` to `list` of `dd.cudd.Function` - multiple roots supported in `dd.cudd.BDD.dump` for file types other than DDDMP - method `count` of the classes `dd.cudd.BDD` and `dd.cudd_zdd.ZDD`: - make optional the argument `nvars` - `dd.autoref.BDD.load`: require file extension `.p` for pickle files ## 0.5.5 API: - require `networkx <= 2.2` on Python 2 - class `dd.bdd.BDD`: - remove argument `debug` from method `_next_free_int` - add method `undeclare_variables` - plot nodes for external BDD references in function `dd.bdd.to_pydot`, which is used by the methods `BDD.dump` of the modules `dd.cudd`, `dd.autoref`, and `dd.bdd` - function `dd._copy.load_json`: rename argument from `keep_order` to `load_order` - add unused keyword arguments to method `autoref.BDD.decref` ## 0.5.4 - enable `KeyboardInterrupt` on POSIX systems for `cudd` when `cysignals >= 1.7.0` is present at installation API: - change signature of method `cudd.BDD.dump` - add GraphViz as an option of `cudd.BDD.dump` - allow copying between managers with different variable orders - allow simultaneous substitution in `bdd.BDD.let` - add property `BDD.var_levels` - add method `BDD.reorder` to `cudd` and `autoref` - add method `cudd.BDD.group` for grouping variables - add `autoref.BDD` methods `incref` and `decref` - change signatures of `cudd.BDD` methods `incref` and `decref` - change default to `recursive=False` in method `cudd.BDD.decref` - add property `Function.dag_size` - add module `dd._copy` - rm function `dd.bdd.copy_vars`, use method `BDD.declare` instead, and separately copy variable order, if needed. This function has moved to `_copy.copy_vars`. - rm method `bdd.BDD.evaluate`, use method `dd.BDD.let` ## 0.5.3 - distribute `manylinux1_x86_64` wheel via PyPI API: - update to `networkx >= 2.0` (works with `< 2.0` too) - class `BDD` in modules `autoref`, `bdd`, `cudd`, `sylvan`: - remove deprecated methods (where present): `compose`, `cofactor`, `rename`, `evaluate`, `sat_iter`, `sat_len` ## 0.5.2 API: - require `networkx < 2.0.0` - add module `dd._abc` that defines API implemented by other modules. - add method `declare` to `BDD` classes - add methods `implies` and `equiv` to class `cudd.Function` - change BDD node reference syntax to "@ INTEGER" - change `Function.__str__` to include `@` in modules `cudd` and `autoref` - deprecate `BDD` methods `compose`, `cofactor`, `rename`, `evaluate`, instead use `BDD.let` - class `BDD` in modules `autoref`, `bdd`, `cudd`, `sylvan`: - methods `pick`, `pick_iter`: rename argument from `care_bits` to `care_vars` - class `BDD` in modules `autoref`, `bdd`: - method `count`: rename argument from `n` to `nvars` - class `BDD` in modules `bdd`, `cudd`: - allow swapping variables in method `rename`, accept only variable names, not levels - rm argument `bdd` from functions: - `image`, `preimage` in module `autoref` - `and_exists`, `or_forall`, `dump` in module `cudd` - `and_exists`, `or_forall` in module `sylvan` - rm argument `roots` from method `autoref.BDD.collect_garbage` - rm argument `source` from function: `copy_bdd` in modules `autoref`, `cudd` - rm function `cudd.rename`, use method `cudd.BDD.let` - rm function `autoref.rename`, use method `autoref.BDD.let` - rm method `autoref.Function.__xor__` - add TLA constants "TRUE" and "FALSE" to syntax, use these in method `BDD.to_expr` ## 0.5.1 API: - classes `cudd.BDD`, `autoref.BDD`, `bdd.BDD`: - add method `let`, which will replace `compose`, `cofactor`, `rename` - add method `pick` - add method `pick_iter`, deprecate `sat_iter` - add method `count`, deprecate `sat_len` - allow copying node to same manager, but log warning - class `sylvan.BDD`: - add method `let` - classes `cudd.Function`, `autoref.Function`: - implement all comparison methods (`__le__`, `__lt__`) ## 0.5.0 API: - dynamic variable reordering in `dd.bdd.BDD` (by default disabled) - method `bdd.BDD.sat_len`: count only levels in support (similar to CUDD) - class `autoref.Function`: - rename attribute `bdd` to `manager` - classes `cudd.Function`, `autoref.Function`, `sylvan.Function`: - add attributes `var, support, bdd` - add method `__hash__` - classes `cudd.Function` and `sylvan.Function`: - hide attribute `index` as `_index` - classes `cudd.BDD` and `sylvan.BDD`: - do not memoize attributes `false` and `true` - classes `cudd.BDD` and `autoref.BDD`: - add method `find_or_add` - method `BDD.sat_iter`: - rm arg `full` - `care_bits = support` as default - `care_bits < support` allowed - function `bdd.to_pydot`: plot only levels in support of given node - add function `autoref.reorder` ## 0.4.3 API: - build `dd.cudd` using CUDD v3.0.0 (an older CUDD via an older `download.py` should work too) ## 0.4.2 API: - classes `bdd.BDD`, `autoref.BDD`: - rm attribute `ordering`, use `vars` - rename `__init__` argument `ordering` to `levels` - allow passing path to CUDD during installation via `--cudd` ## 0.4.1 - add Cython interface `dd.sylvan` to Sylvan - support TLA+ syntax BUG: - in Python 2 use `sys.maxint` for `bdd.BDD.max_nodes` API: - classes `bdd.BDD` and `cudd.BDD`: - method `apply`: rm `"bimplies"` value - raise `AssertionError` if `care_bits < support` in method `sat_iter` - rm unused operator `!=` from parser grammar - class `autoref.Function`: - rename method `bimplies` to `equiv` ## 0.4.0 - require `pydot >= 1.2.2` API: - change quantification syntax to `\E x, y: x` - add renaming syntax `\S x / y, z / w: y & w` - class `BDD` in `dd.bdd`, `dd.autoref`, `dd.cudd`: - add operators `'ite', '\E', '\A'` to method `apply` - add methods `forall` and `exist` as wrappers of `quantify` - add method `_add_int` for checking presence of a BDD node represented as an integer - add method `succ` to obtain `(level, low, high)` - class `cudd.BDD`: - add method `compose` - add method `ite` - add method `to_expr` - class `cudd.Function`: - add method `__int__` to represent CUDD nodes uniquely as integers (by shifting the C pointer value to avoid possible conflicts with reserved values) - add method `__str__` to return integer repr as `str` - add attribute `level` - add attribute `negated` - module `cudd`: - add function `restrict` - add function `count_nodes` - remove "extra" named `dot`, because `pydot` is now required ## 0.3.1 BUG: - `dd.bdd.BDD.dump`: if argument `roots is None` (default), then dump all nodes - `dd.autoref.BDD.compose`: call wrapped method correctly ## 0.3.0 API: - `dd.bdd.BDD.rename`, `dd.bdd.image`, `dd.bdd.preimage`: allow non-adjacent variable levels - `dd.bdd.BDD.descendants`: - arg `roots` instead of single node `u` - iteration instead of recursion - breadth-first instead of depth-first search - `dd.bdd.BDD.dump`: - dump nodes reachable from given roots - dump only variable levels and nodes to pickle file - correct error that ignored explicit file type for PDF, PNG, SVG - `dd.bdd.BDD.load`: - instance method to load nodes - `dd.bdd.to_pydot`: - add arg `roots` - hide methods that dump and load entire manager - `dd.bdd.BDD._dump_manager` and `_load_manager` - remove `dd.autoref.Function.from_expr` ## 0.2.2 - install without extensions by default - try to read git information, but assume release if this fails for any reason ## 0.2.1 - optionally import `gitpython` in `setup.py` to retrieve version info from `git` repo. - version identifier when `git` available: `X.Y.Z.dev0+SHA[.dirty]` - require `psutil >= 3.2.2` - require `setuptools >= 19.6` to avoid `cython` affecting `psutil` build - detect 64-bit system using `ctypes.sizeof` for CUDD flags API: - `dd.cudd.BDD.__cinit__`: - rename arg `memory` -> `memory_estimate` - assert memory estimate less than `psutil.virtual_memory().total` - add arg `initial_cache_size` - `dd.cudd.BDD.statistics`: - distinguish between peak and live nodes - cache statistics - unique table statistics - read node count without removing unused nodes - `dd.cudd.BDD.configure`: - accept keyword args, instead of `dict` - first read config (returned `dict`), then set given values - reordering - garbage collection - max cache soft - max swaps - max variables per reordering - `dd.bdd`, `dd.autoref`, `dd.cudd`: - add method `BDD.copy` for copying nodes between managers - add method `BDD.rename` for substituting variables - deprecate functions `rename` and `copy_bdd` - add method `dd.cudd.BDD.sat_iter` - add function `dd.cudd.count_nodes_per_level` - add functions that track variable order when saving: - `dd.cudd.dump` - `dd.cudd.load` ## 0.2.0 - add user documentation - support Python 3 - require `pydot3k` in Python 3, `pydot` in Python 2 - expose more control over CUDD configuration API: - add `dd.cudd.BDD.configure` - do not set manager parameters in `__cinit__` - rename `BDD.False` -> `BDD.false` (same for “true”), to avoid syntax errors in Python 3 - remove `dd.bdd.BDD.add_ast` - `dd.cudd.reorder` invokes sifting if variable order is `None` - default to pickle protocol 2 ## 0.1.3 Bugfix release to add file `download.py` missing from MANIFEST. API: - add `dd.cudd.BDD.statistics` - add functions `copy_vars` and `copy_bdd` - remove `dd.bdd.BDD.level_to_variable` ## 0.1.2 - add Cython interface `dd.cudd` to CUDD - add Cython interface `dd.buddy` to BuDDy ## 0.1.1 - dynamic variable addition in `dd.bdd.BDD` - add `dd.autoref` wrapper around `dd.bdd` - avoid randomization inside `sat_iter` API: - add `BDD.True` and `BDD.False` - move `Function` interface to `dd.autoref` - move parser to `dd._parser` - rename `BDD.level_to_variable` -> `var_at_level` - deprecate `BDD.ordering` in favor of `BDD.vars` ## 0.0.4 - add `dd.mdd` for multi-terminal decision diagrams - add quantifiers to syntax - add complemented edges to syntax - require `networkx` API: - add `dd.bdd.BDD.cube` - add `dd.bdd.BDD.descendants` - add function `reorder_pairs` ## 0.0.3 - add PLY parser for Boolean expressions - require `astutils` API: - add `dd.bdd.BDD.ref` - assign `bool` as model values ## 0.0.2 - test on Travis API: - add `"diff"` operator to `dd.bdd.BDD.apply` ## 0.0.1 Initial release. ================================================ FILE: LICENSE ================================================ Copyright (c) 2014-2022 by California Institute of Technology All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the California Institute of Technology nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL CALTECH OR THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: MANIFEST.in ================================================ include README.md include LICENSE include doc.md include CHANGES.md include AUTHORS include requirements.txt include download.py include tests/README.md include tests/pytest.ini include tests/inspect_cython_signatures.py include tests/common.py include tests/common_bdd.py include tests/common_cudd.py include tests/iterative_recursive_flattener.py include tests/*_test.py include tests/sample*.dddmp include examples/README.md include examples/*.py include examples/*.sh include dd/py.typed include dd/*.pyx include dd/*.pxd include dd/*.c include dd/CUDD_LICENSE include dd/GLIBC_COPYING.LIB include dd/GLIBC_LICENSES include dd/PYTHON_LICENSE ================================================ FILE: Makefile ================================================ # build, install, test, release `dd` SHELL := bash wheel_file := $(wildcard dist/*.whl) .PHONY: cudd install test .PHONY: clean clean_all clean_cudd wheel_deps build_cudd: clean cudd install test build_sylvan: clean wheel_deps -pip uninstall -y dd export DD_SYLVAN=1; \ pip install . -vvv --use-pep517 --no-build-isolation pip install pytest make test sdist_test: clean wheel_deps pip install -U build cython export DD_CUDD=1 DD_BUDDY=1; \ python -m build --sdist --no-isolation pushd dist; \ pip install dd*.tar.gz; \ tar -zxf dd*.tar.gz && \ popd pip install pytest make -C dist/dd*/ -f ../../Makefile test sdist_test_cudd: clean wheel_deps pip install build cython ply export DD_CUDD=1 DD_BUDDY=1; \ python -m build --sdist --no-isolation yes | pip uninstall cython ply pushd dist; \ tar -zxf dd*.tar.gz; \ pushd dd*/; \ export DD_FETCH=1 DD_CUDD=1; \ pip install . -vvv --use-pep517 --no-build-isolation && \ popd && popd pip install pytest make -C dist/dd*/ -f ../../Makefile test # use to create source distributions for PyPI sdist: clean wheel_deps -rm dist/*.tar.gz pip install -U build cython export DD_CUDD=1 DD_CUDD_ZDD=1 DD_BUDDY=1 DD_SYLVAN=1; \ python -m build --sdist --no-isolation wheel_deps: pip install --upgrade \ cython \ pip \ setuptools \ wheel # use to create binary distributions for PyPI wheel: clean wheel_deps -rm dist/*.whl -rm wheelhouse/*.whl export DD_CUDD=1 DD_CUDD_ZDD=1; \ pip wheel . \ -vvv \ --wheel-dir dist \ --no-deps @echo "-------------" auditwheel show dist/*.whl @echo "-------------" auditwheel repair --plat manylinux_2_17_x86_64 dist/*.whl @echo "-------------" auditwheel show wheelhouse/*.whl install: wheel_deps export DD_CUDD=1; \ pip install . -vvv --use-pep517 --no-build-isolation reinstall: uninstall wheel_deps export DD_CUDD=1 DD_CUDD_ZDD=1 DD_SYLVAN=1; \ pip install . -vvv --use-pep517 --no-build-isolation reinstall_buddy: uninstall wheel_deps export DD_BUDDY=1; \ pip install . -vvv --use-pep517 --no-build-isolation reinstall_cudd: uninstall wheel_deps export DD_CUDD=1 DD_CUDD_ZDD=1; \ pip install . -vvv --use-pep517 --no-build-isolation reinstall_sylvan: uninstall wheel_deps export DD_SYLVAN=1; \ pip install . -vvv --use-pep517 --no-build-isolation uninstall: pip uninstall -y dd test: set -x; \ pushd tests; \ python -X dev -m pytest -v --continue-on-collection-errors . && \ popd # `pytest -Werror` turns all warnings into errors # # including pytest warnings about unraisable exceptions: # # test_abc: python -X dev tests/inspect_cython_signatures.py test_examples: pushd examples/; \ for script in `ls *.py`; \ do \ echo "Running: $$script"; \ python -X dev $$script; \ done && \ popd show_deprecated: python -X dev -Wall -c "from dd import bdd" typecheck: pytype \ -k \ -v 1 \ -j 'auto' \ dd/*.py \ setup.py \ examples/*.py # tests/*.py # download.py clean_type_cache: -rm -rf .pytype/ cudd: pushd cudd-*/; \ ./configure "CFLAGS=-fPIC -std=c99" \ make build XCFLAGS="\ -fPIC \ -mtune=native \ -DHAVE_IEEE_754 \ -DBSD \ -DSIZEOF_VOID_P=8 \ -DSIZEOF_LONG=8" && \ popd doc: grip --export doc.md index.html download_licenses: python -c 'import download; \ download.download_licenses()' clean_all: clean_cudd clean clean_cudd: pushd cudd-*/; make clean && popd clean: -rm -rf build/ dist/ dd.egg-info/ -rm dd/*.so -rm dd/buddy.c -rm dd/cudd.c -rm dd/cudd_zdd.c -rm dd/sylvan.c -rm *.pyc */*.pyc -rm -rf __pycache__ */__pycache__ -rm -rf wheelhouse rm_cudd: -rm -rf cudd*/ cudd*.tar.gz ================================================ FILE: README.md ================================================ [![Build Status][build_img]][ci] About ===== A pure-Python (Python >= 3.11) package for manipulating: - [Binary decision diagrams]( https://en.wikipedia.org/wiki/Binary_decision_diagram) (BDDs). - [Multi-valued decision diagrams]( https://dx.doi.org/10.1109/ICCAD.1990.129849) (MDDs). as well as [Cython](https://cython.org) bindings to the C libraries: - [CUDD]( https://web.archive.org/web/20180127051756/http://vlsi.colorado.edu/~fabio/CUDD/html/index.html) (also read [the introduction]( https://web.archive.org/web/20150317121927/http://vlsi.colorado.edu/~fabio/CUDD/node1.html), and note that the original link for CUDD is ) - [Sylvan](https://github.com/utwente-fmt/sylvan) (multi-core parallelization) - [BuDDy](https://sourceforge.net/projects/buddy/) These bindings expose almost identical interfaces as the Python implementation. The intended workflow is: - develop your algorithm in pure Python (easy to debug and introspect), - use the bindings to benchmark and deploy Your code remains the same. Contains: - All the standard functions defined, e.g., by [Bryant](https://www.cs.cmu.edu/~bryant/pubdir/ieeetc86.pdf). - Dynamic variable reordering using [Rudell's sifting algorithm]( http://www.eecg.toronto.edu/~ece1767/project/rud.pdf). - Reordering to obtain a given order. - Parser of quantified Boolean expressions in either [TLA+](https://en.wikipedia.org/wiki/TLA%2B) or [Promela](https://en.wikipedia.org/wiki/Promela) syntax. - Pre/Image computation (relational product). - Renaming variables. - Zero-omitted binary decision diagrams (ZDDs) in CUDD - Conversion from BDDs to MDDs. - Conversion functions to [`networkx`](https://networkx.org) and [DOT](https://www.graphviz.org/doc/info/lang.html) graphs. - BDDs have methods to `dump` and `load` them using [JSON]( https://wikipedia.org/wiki/JSON), or [`pickle`]( https://docs.python.org/3/library/pickle.html). - BDDs dumped by CUDD's DDDMP can be loaded using fast iterative parser. - [Garbage collection]( https://en.wikipedia.org/wiki/Garbage_collection_(computer_science)) that combines reference counting and tracing If you prefer to work with integer variables instead of Booleans, and have BDD computations occur underneath, then use the module [`omega.symbolic.fol`]( https://github.com/tulip-control/omega/blob/main/omega/symbolic/fol.py) from the [`omega` package]( https://github.com/tulip-control/omega/blob/main/doc/doc.md). If you are interested in computing minimal covers (two-level logic minimization) then use the module `omega.symbolic.cover` of the `omega` package. The method `omega.symbolic.fol.Context.to_expr` converts BDDs to minimal formulas in disjunctive normal form (DNF). Documentation ============= In the [Markdown](https://en.wikipedia.org/wiki/Markdown) file [`doc.md`](https://github.com/tulip-control/dd/blob/main/doc.md). The [changelog](https://en.wiktionary.org/wiki/changelog) is in the file [`CHANGES.md`]( https://github.com/tulip-control/dd/blob/main/CHANGES.md). Examples ======== The module `dd.autoref` wraps the pure-Python BDD implementation `dd.bdd`. The API of `dd.cudd` is almost identical to `dd.autoref`. You can skip details about `dd.bdd`, unless you want to implement recursive BDD operations at a low level. ```python from dd.autoref import BDD bdd = BDD() bdd.declare('x', 'y', 'z', 'w') # conjunction (in TLA+ syntax) u = bdd.add_expr(r'x /\ y') # symbols `&`, `|` are supported too # note the "r" before the quote, # which signifies a raw string and is # needed to allow for the backslash print(u.support) # substitute variables for variables (rename) rename = dict(x='z', y='w') v = bdd.let(rename, u) # substitute constants for variables (cofactor) values = dict(x=True, y=False) v = bdd.let(values, u) # substitute BDDs for variables (compose) d = dict(x=bdd.add_expr(r'z \/ w')) v = bdd.let(d, u) # as Python operators v = bdd.var('z') & bdd.var('w') v = ~ v # quantify universally ("forall") u = bdd.add_expr(r'\A x, y: (x /\ y) => y') # quantify existentially ("exist") u = bdd.add_expr(r'\E x, y: x \/ y') # less readable but faster alternative, # (faster because of not calling the parser; # this may matter only inside innermost loops) u = bdd.var('x') | bdd.var('y') u = bdd.exist(['x', 'y'], u) assert u == bdd.true, u # inline BDD references u = bdd.add_expr(rf'x /\ {v}') # satisfying assignments (models): # an assignment d = bdd.pick(u, care_vars=['x', 'y']) # iterate over all assignments for d in bdd.pick_iter(u): print(d) # how many assignments n = bdd.count(u) # write to and load from JSON file filename = 'bdd.json' bdd.dump(filename, roots=dict(res=u)) other_bdd = BDD() roots = other_bdd.load(filename) print(other_bdd.vars) ``` To run the same code with CUDD installed, change the first line to: ```python from dd.cudd import BDD ``` Most useful functionality is available via methods of the class `BDD`. A few of the functions can prove useful too, among them `to_nx()`. Use the method `BDD.dump` to write a `BDD` to a `pickle` file, and `BDD.load` to load it back. A CUDD dddmp file can be loaded using the function `dd.dddmp.load`. A `Function` object wraps each BDD node and decrements its reference count when disposed by Python's garbage collector. Lower-level details are discussed in the documentation. For using ZDDs, change the first line to ```python from dd.cudd_zdd import ZDD as BDD ``` Installation ============ ## pure-Python From the [Python Package Index (PyPI)](https://pypi.org) using the package installer [`pip`](https://pip.pypa.io): ```shell pip install dd ``` or from the directory of source files: ```shell pip install . ``` For graph layout, install also [graphviz](https://graphviz.org). The `dd` package requires Python 3.11 or later. For Python 2.7, use `dd == 0.5.7`. ## Cython bindings To compile also the module `dd.cudd` (which interfaces to CUDD) when installing from PyPI, run: ```shell pip install --upgrade wheel cython export DD_FETCH=1 DD_CUDD=1 pip install dd -vvv --use-pep517 --no-build-isolation ``` (`DD_FETCH=1 DD_CUDD=1 pip install dd` also works, when the source tarball includes cythonized code.) To confirm that the installation succeeded: ```shell python -c 'import dd.cudd' ``` The [environment variables]( https://en.wikipedia.org/wiki/Environment_variable) above mean: - `DD_FETCH=1`: download CUDD v3.0.0 sources from the internet, unpack the tarball (after checking its hash), and `make` CUDD. - `DD_CUDD=1`: build the Cython module `dd.cudd` More about environment variables that configure the C extensions of `dd` is described in the file [`doc.md`]( https://github.com/tulip-control/dd/blob/main/doc.md) ## Wheel files with compiled CUDD [Wheel files]( https://www.python.org/dev/peps/pep-0427/) are [available from PyPI]( https://pypi.org/project/dd/#files), which contain the module `dd.cudd`, with the CUDD library compiled and linked. If you have a Linux system and Python version compatible with one of the PyPI wheels, then `pip install dd` will install also `dd.cudd`. ### Licensing of the compiled modules `dd.cudd` and `dd.cudd_zdd` in the wheel These notes apply to the compiled modules `dd.cudd` and `dd.cudd_zdd` that are contained in the [wheel file](https://www.python.org/dev/peps/pep-0427/) on PyPI (namely the files `dd/cudd.cpython-39-x86_64-linux-gnu.so` and `dd/cudd_zdd.cpython-39-x86_64-linux-gnu.so` in the [`*.whl` file]( https://pypi.org/project/dd/#files), which can be obtained using [`unzip`](http://infozip.sourceforge.net/UnZip.html)). These notes do not apply to the source code of the modules `dd.cudd` and `dd.cudd_zdd`. The source distribution of `dd` on PyPI is distributed under a 3-clause BSD license. The following libraries and their headers were used when building the modules `dd.cudd` and `dd.cudd_zdd` that are included in the wheel: - Python: (where `A` and `B` the numerals of the corresponding Python version used; for example `10` and `2` to signify Python 3.10.2). CPython releases are described at: - [CUDD](https://sourceforge.net/projects/cudd-mirror/files/cudd-3.0.0.tar.gz/download). The licenses of Python and CUDD are included in the wheel archive. Cython [does not](https://github.com/cython/cython/blob/master/COPYING.txt) add its license to C code that it generates. GCC was used to compile the modules `dd.cudd` and `dd.cudd_zdd` in the wheel, and the GCC [runtime library exception]( https://github.com/gcc-mirror/gcc/blob/master/COPYING.RUNTIME#L61-L66) applies. The modules `dd.cudd` and `dd.cudd_zdd` in the wheel dynamically link to the: - Linux kernel (in particular [`linux-vdso.so.1`]( https://man7.org/linux/man-pages/man7/vdso.7.html)), which allows system calls (read the kernel's file [`COPYING`]( https://github.com/torvalds/linux/blob/master/COPYING) and the explicit syscall exception in the file [`LICENSES/exceptions/Linux-syscall-note`]( https://github.com/torvalds/linux/blob/master/LICENSES/exceptions/Linux-syscall-note)) - [GNU C Library](https://www.gnu.org/software/libc/) (glibc) (in particular `libpthread.so.0`, `libc.so.6`, `/lib64/ld-linux-x86-64.so.2`), which uses the [LGPLv2.1](https://sourceware.org/git/?p=glibc.git;a=blob_plain;f=COPYING.LIB;hb=HEAD) that allows dynamic linking, and other [licenses]( https://sourceware.org/git/?p=glibc.git;a=blob_plain;f=LICENSES;hb=HEAD). These licenses are included in the wheel file and apply to the GNU C Library that is dynamically linked. Tests ===== Use [`pytest`](https://pypi.org/project/pytest). Run with: ```shell pushd tests/ pytest -v --continue-on-collection-errors . popd ``` Tests of Cython modules that were not installed will fail. The code is covered well by tests. License ======= [BSD-3](https://opensource.org/licenses/BSD-3-Clause), read file `LICENSE`. [build_img]: https://github.com/tulip-control/dd/actions/workflows/main.yml/badge.svg?branch=main [ci]: https://github.com/tulip-control/dd/actions ================================================ FILE: dd/__init__.py ================================================ """Package of algorithms based on decision diagrams.""" try: import dd._version as _version __version__ = _version.version except ImportError: __version__ = None try: import dd.cudd as _bdd except ImportError: import dd.autoref as _bdd BDD = _bdd.BDD ================================================ FILE: dd/_abc.py ================================================ """Interface specification. This specification is implemented by the modules: - `dd.autoref` - `dd.cudd` - `dd.cudd_zdd` - `dd.sylvan` (partially) - `dd.buddy` (partially) """ # Copyright 2017 by California Institute of Technology # All rights reserved. Licensed under BSD-3. # import collections.abc as _abc import typing as _ty def _literals_of( type_alias: type ) -> set[str]: """Return arguments of `type_alias`. Recursive computation. Assumes `str` literals. """ return set(_literals_of_recurse(type_alias)) def _literals_of_recurse( type_alias: type ) -> _abc.Iterable[str]: """Yield literals of `type_alias`.""" args = _ty.get_args(type_alias) literals = set() for arg in args: match arg: case str(): yield arg case _: yield from _literals_of_recurse(arg) return literals Yes: _ty.TypeAlias = bool Nat: _ty.TypeAlias = int # ```tla # Nat # ``` Cardinality: _ty.TypeAlias = Nat NumberOfBytes: _ty.TypeAlias = Cardinality VariableName: _ty.TypeAlias = str Level: _ty.TypeAlias = Nat VariableLevels: _ty.TypeAlias = dict[ VariableName, Level] Ref = _ty.TypeVar('Ref') Assignment: _ty.TypeAlias = dict[ VariableName, bool] Renaming: _ty.TypeAlias = dict[ VariableName, VariableName] Fork: _ty.TypeAlias = tuple[ Level, Ref | None, Ref | None] Formula: _ty.TypeAlias = str _UnaryOperatorSymbol: _ty.TypeAlias = _ty.Literal[ # negation 'not', '~', '!'] UNARY_OPERATOR_SYMBOLS: _ty.Final = _literals_of( _UnaryOperatorSymbol) # These assertions guard against typos in # the enumerations. if len(UNARY_OPERATOR_SYMBOLS) != 3: raise AssertionError(UNARY_OPERATOR_SYMBOLS) _BinaryOperatorSymbol: _ty.TypeAlias = _ty.Literal[ # conjunction 'and', '/\\', '&', '&&', # disjunction 'or', r'\/', '|', '||', # different '#', 'xor', '^', # implication '=>', '->', 'implies', # equivalence '<=>', '<->', 'equiv', # subtraction (i.e., `a /\ ~ b`) 'diff', '-', # quantification r'\A', 'forall', r'\E', 'exists'] BINARY_OPERATOR_SYMBOLS: _ty.Final = _literals_of( _BinaryOperatorSymbol) if len(BINARY_OPERATOR_SYMBOLS) != 23: raise AssertionError(BINARY_OPERATOR_SYMBOLS) _TernaryOperatorSymbol: _ty.TypeAlias = _ty.Literal[ # ternary conditional # (if-then-else) 'ite'] TERNARY_OPERATOR_SYMBOLS: _ty.Final = _literals_of( _TernaryOperatorSymbol) if len(TERNARY_OPERATOR_SYMBOLS) != 1: raise AssertionError(TERNARY_OPERATOR_SYMBOLS) BDD_OPERATOR_SYMBOLS: _ty.Final = { *UNARY_OPERATOR_SYMBOLS, *BINARY_OPERATOR_SYMBOLS, *TERNARY_OPERATOR_SYMBOLS} if len(BDD_OPERATOR_SYMBOLS) != 3 + 23 + 1: raise AssertionError(BDD_OPERATOR_SYMBOLS) OperatorSymbol: _ty.TypeAlias = ( _UnaryOperatorSymbol | _BinaryOperatorSymbol | _TernaryOperatorSymbol) ImageFileType: _ty.TypeAlias = _ty.Literal[ 'pdf', 'png', 'svg'] JSONFileType: _ty.TypeAlias = _ty.Literal[ 'json'] PickleFileType: _ty.TypeAlias = _ty.Literal[ 'pickle'] BDDFileType: _ty.TypeAlias = ( ImageFileType | JSONFileType) class BDD(_ty.Protocol[Ref]): """Shared reduced ordered binary decision diagram.""" vars: VariableLevels def __init__( self, levels: dict | None=None ) -> None: ... def __eq__( self, other ) -> Yes: """Return `True` if `other` has same manager""" def __len__( self ) -> Cardinality: """Return number of nodes.""" def __contains__( self, u: Ref ) -> Yes: """Return `True` """ def __str__( self ) -> str: return 'Specification of BDD class.' def configure( self, **kw ) -> dict[ str, _ty.Any]: """Read and apply parameter values.""" def statistics( self ) -> dict[ str, _ty.Any]: """Return BDD manager statistics.""" # default implementation that offers no info return dict() def succ( self, u: Ref ) -> Fork: """Return `(level, low, high)` for node `u`. The manager uses complemented edges, so `low` and `high` correspond to the rectified `u`. """ def declare( self, *variables: VariableName ) -> None: """Add names in `variables` to `self.vars`. ```python bdd.declare('x', 'y', 'z') ``` """ def var( self, var: VariableName ) -> Ref: """Return node for variable named `var`.""" def var_at_level( self, level: Level ) -> VariableName: """Return variable with `level`.""" def level_of_var( self, var: VariableName ) -> ( Level | None): """Return level of `var`, or `None`.""" @property def var_levels( self ) -> VariableLevels: """Return mapping from variables to levels.""" def copy( self, u: Ref, other: 'BDD' ) -> Ref: """Copy operator `u` from `self` to `other` manager.""" def support( self, u: Ref, as_levels: Yes=False ) -> ( set[VariableName] | set[Level]): """Return variables that node `u` depends on. @param as_levels: if `True`, then return variables as integers, insted of strings """ def let( self, definitions: dict[VariableName, VariableName] | Assignment | dict[VariableName, Ref], u: Ref ) -> Ref: """Substitute variables in `u`. The mapping `definitions` need not have all declared variables as keys. """ def forall( self, variables: _abc.Iterable[ VariableName], u: Ref ) -> Ref: """Quantify `variables` in `u` universally.""" def exist( self, variables: _abc.Iterable[ VariableName], u: Ref ) -> Ref: """Quantify `variables` in `u` existentially.""" def count( self, u: Ref, nvars: Cardinality | None=None ) -> Cardinality: """Return number of models of node `u`. @param nvars: number of variables to assume. If omitted, then assume those in `support(u)`. If `nvars >= len(support(u))` then the count is multiplied by `2**(nvars-len(support(u)))`, compared to the case `nvars is None`. """ def pick( self, u: Ref, care_vars: set[VariableName] | None=None ) -> ( Assignment | None): r"""Return a single assignment. An assignment is a `dict` that maps each variable to a `bool`. Examples: ```python >>> u = bdd.add_expr('x') >>> bdd.pick(u) {'x': True} >>> u = bdd.add_expr('y') >>> bdd.pick(u) {'y': True} >>> u = bdd.add_expr('y') >>> bdd.pick(u, care_vars=['x', 'y']) {'x': False, 'y': True} >>> u = bdd.add_expr(r'x \/ y') >>> bdd.pick(u) {'x': False, 'y': True} >>> u = bdd.false >>> bdd.pick(u) is None True ``` By default, `care_vars = support(u)`. Log a warning if `care_vars < support(u)`. Thin wrapper around `pick_iter`. """ picks = self.pick_iter(u, care_vars) return next(iter(picks), None) def pick_iter( self, u: Ref, care_vars: set[VariableName] | None=None ) -> _abc.Iterable[ Assignment]: """Return iterator over assignments. By default, `care_vars = support(u)`. Log a warning if `care_vars < support(u)`. CASES: 1. `None`: return (uniform) assignments that include exactly those variables in `support(u)` 2. `set`: return (possibly partial) assignments that include at least all bits in `care_vars` """ # def to_bdd( # self, # expr): # raise NotImplementedError('use `add_expr`') def add_expr( self, expr: Formula ) -> Ref: """Return node for expression `expr`. Nodes are created for the BDD that represents the expression `expr`. """ def to_expr( self, u: Ref ) -> Formula: """Return a Boolean expression for node `u`.""" def ite( self, g: Ref, u: Ref, v: Ref ) -> Ref: """Ternary conditional `IF g THEN u ELSE v`. @param g: condition @param u: high @param v: low """ def apply( self, op: OperatorSymbol, u: Ref, v: Ref | None=None, w: Ref | None=None ) -> Ref: r"""Apply operator `op` to nodes `u`, `v`, `w`.""" def _add_int( self, i: int ) -> Ref: """Return node from `i`.""" def cube( self, dvars: Assignment ) -> Ref: """Return node for conjunction of literals in `dvars`.""" # TODO: homogeneize i/o API with `dd.cudd` def dump( self, filename: str, roots: dict[str, Ref] | list[Ref] | None=None, filetype: ImageFileType | None=None, **kw ) -> None: """Write BDDs to `filename`. The file type is inferred from the extension (case insensitive), unless a `filetype` is explicitly given. `filetype` can have the values: - `'pickle'` for Pickle - `'pdf'` for PDF - `'png'` for PNG - `'svg'` for SVG If `filetype is None`, then `filename` must have an extension that matches one of the file types listed above. Dump nodes reachable from `roots`. If `roots is None`, then all nodes in the manager are dumped. @type roots: - `list` of nodes, or - for Pickle: `dict` that maps names to nodes """ def load( self, filename: str, levels: Yes=True ) -> ( dict[str, Ref] | list[Ref]): """Load nodes from Pickle file `filename`. If `levels is True`, then load variables at the same levels. Otherwise, add missing variables. @return: roots of the loaded BDDs @rtype: depends on the contents of the file, either: - `dict` that maps names to nodes, or - `list` of nodes """ @property def false( self ) -> Ref: """Return Boolean constant false.""" @property def true( self ) -> Ref: """Return Boolean constant true.""" def reorder( bdd: BDD, order: VariableLevels | None=None ) -> None: """Apply Rudell's sifting algorithm to `bdd`. @param order: reorder to this specific order, if `None` then invoke group sifting """ class Operator(_ty.Protocol): """Convenience wrapper for edges returned by `BDD`.""" def __init__( self, node, bdd ) -> None: self.bdd: BDD self.manager: object self.node: object def __hash__( self ) -> int: return self.node def to_expr( self ) -> Formula: """Return Boolean expression of function.""" def __int__( self ) -> int: """Return integer ID of node. To invert this method call `BDD._add_int`. """ def __str__( self ) -> str: """Return string form of node as `@INT`. "INT" is an integer that depends on the implementation. For example "@54". The integer value is `int(self)`. The integer value is recognized by the method `BDD._add_int` of the same manager that the node belongs to. """ return f'@{int(self)}' def __len__( self ) -> Cardinality: """Number of nodes reachable from this node.""" def __del__( self ) -> None: r"""Dereference node in manager.""" def __eq__( self, other ) -> Yes: r"""`|= self \equiv other`. Return `False` if `other is None`. """ def __ne__( self, other ) -> Yes: r"""`~ |= self \equiv other`. Return `True` if `other is None`. """ def __lt__( self, other ) -> Yes: r"""`(|= self => other) /\ ~ |= self \equiv other`.""" def __le__( self, other ) -> Yes: """`|= self => other`.""" def __invert__( self ) -> 'Operator': """Negation `~ self`.""" def __and__( self, other ) -> 'Operator': r"""Conjunction `self /\ other`.""" def __or__( self, other ) -> 'Operator': r"""Disjunction `self \/ other`.""" def __xor__( self, other ) -> 'Operator': """Exclusive-or `self ^ other`.""" def implies( self, other ) -> 'Operator': """Logical implication `self => other`.""" def equiv( self, other ) -> 'Operator': r"""Logical equivalence `self <=> other`. The result is *different* from `__eq__`: - Logical equivalence is the Boolean function that is `TRUE` for models for which both `self` and `other` are `TRUE`, and `FALSE` otherwise. - BDD equality (`__eq__`) is the Boolean function that results from universal quantification of the logical equivalence, over all declared variables. In other words: "A <=> B" versus "\A x, y, ..., z: A <=> B" or, from a metatheoretic viewpoint: "A <=> B" versus "|= A <=> B" In the metatheory, [[A <=> B]] (`equiv`) is different from [[A]] = [[B]] (`__eq__`). Also, `equiv` differs from `__eq__` in that it returns a BDD as `Function`, instead of `bool`. """ @property def level( self ) -> Level: """Level where this node currently is.""" @property def var( self ) -> ( VariableName | None): """Variable at level where this node is.""" @property def low( self ) -> '''( Operator | None )''': """Return "else" node.""" @property def high( self ) -> '''( Operator | None )''': """Return "then" node.""" @property def ref( self ) -> Cardinality: """Sum of reference counts of node and its negation.""" @property def negated( self ) -> Yes: """Return `True` if `self` is a complemented edge.""" @property def support( self ) -> set[ VariableName]: """Return variables in support.""" def let( self, **definitions: '''( VariableName | Operator | bool )''' ) -> 'Operator': return self.bdd.let(definitions, self) def exist( self, *variables: VariableName ) -> 'Operator': return self.bdd.exist(variables, self) def forall( self, *variables: VariableName ) -> 'Operator': return self.bdd.forall(variables, self) def pick( self, care_vars: set[VariableName] | None=None ) -> ( Assignment | None): return self.bdd.pick(self, care_vars) def count( self, nvars: Cardinality | None=None ) -> Cardinality: return self.bdd.count(self, nvars) ================================================ FILE: dd/_copy.py ================================================ """Utilities for transferring BDDs.""" # Copyright 2016-2018 by California Institute of Technology # All rights reserved. Licensed under 3-clause BSD. # import collections.abc as _abc import contextlib as _ctx import json import os import shelve import shutil import typing as _ty import dd._abc import dd._utils as _utils SHELVE_DIR: _ty.Final = '__shelve__' _Yes: _ty.TypeAlias = dd._abc.Yes class _BDD( dd._abc.BDD[dd._abc.Ref], _ty.Protocol): """BDD context.""" def add_var( self, var: str, level: dd._abc.Level | None=None ) -> dd._abc.Level: ... def _top_cofactor( self, u: dd._abc.Ref, level: dd._abc.Level ) -> tuple[ dd._abc.Ref, dd._abc.Ref]: ... def reorder( self, var_order: dict[ dd._abc.VariableName, dd._abc.Level] | None=None ) -> None: ... def find_or_add( self, level: dd._abc.Level, u: dd._abc.Ref, v: dd._abc.Ref ) -> dd._abc.Ref: ... def incref( self, node: dd._abc.Ref ) -> None: ... def decref( self, node: dd._abc.Ref, **kw ) -> None: ... def assert_consistent( self ) -> None: ... Ref = _ty.TypeVar('Ref') class _Ref(_ty.Protocol): var: str | None level: int low: '_Ref | None' high: '_Ref | None' bdd: _BDD negated: _Yes ref: int def __int__( self ) -> int: ... def __invert__( self ) -> '_Ref': ... class _Shelf( _ctx.AbstractContextManager, _ty.Protocol): """Used for type checking.""" # `_abc.MutableMapping` cannot be # in the bases, because not # itself a `_ty.Protocol`. def __setitem__( self, key, value ) -> None: ... def __getitem__( self, key): ... def __iter__( self ) -> _abc.Iterable: ... def __contains__( self, item ) -> _Yes: ... def _open_shelf( name: str ) -> _Shelf: """Wrapper for type-checking.""" return shelve.open(name) def copy_vars( source: dd._abc.BDD, target ) -> None: """Copy variables, preserving levels.""" for var in source.vars: level = source.level_of_var(var) target.add_var(var, level=level) def copy_bdds_from( roots: _abc.Iterable[_Ref], target: _BDD ) -> list[_Ref]: """Copy BDDs in `roots` to manager `target`.""" cache = dict() return [ copy_bdd(u, target, cache) for u in roots] def copy_bdd( root: _Ref, target: _BDD, cache: dict | None=None ) -> _Ref: """Copy BDD with `root` to manager `target`. @param target: BDD or ZDD context @param cache: for memoizing results """ if cache is None: cache = dict() return _copy_bdd(root, target, cache) def _copy_bdd( u: _Ref, bdd: _BDD, cache: dict ) -> _Ref: """Recurse to copy node `u` to `bdd`.""" # terminal ? if u == u.bdd.true: return bdd.true # could be handled via cache, # but frequent case if u == u.bdd.false: return bdd.false # rectify z = _flip(u, u) # non-terminal # memoized ? k = int(z) if k in cache: r = cache[k] return _flip(r, u) # recurse low = _copy_bdd(u.low, bdd, cache) high = _copy_bdd(u.high, bdd, cache) # canonicity # if low.negated != u.low.negated: # raise AssertionError((low, u.low)) # if high.negated: # raise AssertionError(high) # add node g = bdd.var(u.var) r = bdd.ite(g, high, low) # if r.negated: # raise AssertionError(r) # memoize cache[k] = r return _flip(r, u) def _flip( r: _Ref, u: _Ref ) -> _Ref: """Negate `r` if `u` is negated. Else return `r`. """ return ~ r if u.negated else r def copy_zdd( root: _Ref, target: _BDD, cache: dict | None=None ) -> _Ref: """Copy ZDD with `root` to manager `target`. @param target: BDD or ZDD context @param cache: for memoizing results """ if cache is None: cache = dict() level = 0 return _copy_zdd(level, root, target, cache) def _copy_zdd( level: int, u: _Ref, target: _BDD, cache: dict[int, _Ref] ) -> _Ref: """Recurse to copy node `u` to `target`.""" src: _BDD = u.bdd # terminal ? if u == src.false: return target.false if level == len(src.vars): return target.true # memoized ? k = int(u) if k in cache: return cache[k] # recurse v, w = src._top_cofactor(u, level) low = _copy_zdd( level + 1, v, target, cache) high = _copy_zdd( level + 1, w, target, cache) # add node var = src.var_at_level(level) g = target.var(var) r = target.ite(g, high, low) # memoize cache[k] = r return r def dump_json( nodes: dict[str, Ref] | list[Ref], file_name: str ) -> None: """Write reachable nodes to JSON file. Writes the nodes that are reachable from the roots in `nodes` to the JSON file named `file_name`. Also dumps the variable names and the variable order, to the same JSON file. @param nodes: maps names to roots of the BDDs that will be written to the JSON file """ if not nodes: raise ValueError( 'Need nonempty `nodes` as roots.') tmp_fname = os.path.join( SHELVE_DIR, 'temporary_shelf') os.makedirs(SHELVE_DIR) try: with _open_shelf(tmp_fname) as cache,\ open(file_name, 'w') as fd: _dump_json(nodes, fd, cache) finally: # `shelve` file naming # depends on context shutil.rmtree(SHELVE_DIR) def _dump_json( nodes: dict[str, _Ref] | list[_Ref], fd: _ty.TextIO, cache: _abc.MutableMapping[str, bool] ) -> None: """Dump BDD as JSON to file `fd`. Use `cache` to keep track of visited nodes. """ fd.write('{') _dump_bdd_info(nodes, fd) for u in _utils.values_of(nodes): _dump_bdd(u, fd, cache) fd.write('\n}\n') def _dump_bdd_info( nodes: dict[str, _Ref] | list[_Ref], fd): """Dump variable levels and roots. @param nodes: maps names to roots of BDDs """ roots = _utils.map_container(_node_to_int, nodes) u = next(iter(_utils.values_of(nodes))) bdd = u.bdd var_level = { var: bdd.level_of_var(var) for var in bdd.vars} info = ( '\n"level_of_var": {level}' ',\n"roots": {roots}').format( level=json.dumps(var_level), roots=json.dumps(roots)) fd.write(info) def _dump_bdd( u: _Ref, fd: _ty.TextIO, cache: _abc.MutableMapping[str, bool] ) -> ( int | str): """Recursive step of dumping nodes.""" # terminal ? if u == u.bdd.true: return '"T"' if u == u.bdd.false: return '"F"' # rectify z = _flip(u, u) # non-terminal # dumped ? k = int(z) if str(k) in cache: return -k if u.negated else k # recurse low = _dump_bdd(u.low, fd, cache) high = _dump_bdd(u.high, fd, cache) # dump node s = f',\n"{k}": [{u.level}, {low}, {high}]' fd.write(s) # record as dumped cache[str(k)] = True return -k if u.negated else k def load_json( file_name: str, bdd, load_order: _Yes=False ) -> ( dict[str, _Ref] | list[_Ref]): """Add BDDs from JSON `file_name` to `bdd`. @param load_order: if `True`, then load variable order from `file_name`. @return: - keys (or indices) are names - values are BDD roots """ tmp_fname = os.path.join( SHELVE_DIR, 'temporary_shelf') os.makedirs(SHELVE_DIR) try: with _open_shelf(tmp_fname) as cache,\ open(file_name, 'r') as fd: nodes = _load_json( fd, bdd, load_order, cache) finally: shutil.rmtree(SHELVE_DIR) return nodes def _load_json( fd: _abc.Iterable[str], bdd, load_order: _Yes, cache: _abc.MutableMapping[str, int] ) -> ( dict[str, _Ref] | list[_Ref]): """Load BDDs from JSON file `fd` to `bdd`.""" context = dict(load_order=load_order) # if the variable order is going to be loaded, # then turn off dynamic reordering, # because it can change the order midway, # which would not return the loaded order, # and can also cause failure of # the assertion below if load_order: old_reordering = bdd.configure( reordering=False) for line in fd: d = _parse_line(line) _store_line(d, bdd, context, cache) roots = context['roots'] if hasattr(roots, 'items'): roots = { name: _node_from_int(k, bdd, cache) for name, k in roots.items()} else: roots = [ _node_from_int(k, bdd, cache) for k in roots] # rm refs to cached nodes for uid in cache: u = _node_from_int(int(uid), bdd, cache) if u.ref < 2: raise AssertionError(u.ref) # +1 ref due to `incref` in `_make_node` # +1 ref due to the `_node_from_int` # call for `u` if load_order and u.ref < 3: raise AssertionError(u.ref) # +1 ref due to `incref` in `_make_node` # +1 ref due to either: # - being a successor node # - being a root node # (thus referenced in `roots` above) # +1 ref due to the `_node_from_int` # call for `u` bdd.decref(u, _direct=True) # this module is unusual, # in that `incref` and `decref` need # to be called on different `Function` # instances for the same node bdd.assert_consistent() if load_order: bdd.configure( reordering=old_reordering) return roots def _parse_line( line: str ) -> ( dict | None): """Parse JSON from `line`.""" line = line.rstrip() if line == '{' or line == '}': return None if line.endswith(','): line = line.rstrip(',') return json.loads('{' + line + '}') def _store_line( d: dict | None, bdd: _BDD, context: dict, cache: _abc.MutableMapping[str, int] ) -> None: """Interpret data in `d`.""" if d is None: return order = d.get('level_of_var') if order is not None: order = { str(k): v for k, v in order.items()} bdd.declare(*order) context['level_of_var'] = order context['var_at_level'] = { v: k for k, v in order.items()} if context['load_order']: bdd.reorder(order) return roots = d.get('roots') if roots is not None: context['roots'] = roots return _make_node(d, bdd, context, cache) def _make_node( d: dict, bdd: _BDD, context: dict, cache: _abc.MutableMapping[str, int] ) -> None: """Create a new node in `bdd` from `d`.""" (uid, (level, low_id, high_id)), = d.items() k, level = map(int, (uid, level)) if k <= 0: raise AssertionError(k) if level < 0: raise AssertionError(level) low_id = _decode_node(low_id) high_id = _decode_node(high_id) if str(k) in cache: return low = _node_from_int(low_id, bdd, cache) high = _node_from_int(high_id, bdd, cache) var = context['var_at_level'][level] if context['load_order']: u = bdd.find_or_add(var, low, high) else: g = bdd.var(var) u = bdd.ite(g, high, low) if u.negated: raise AssertionError(u) # memoize cache[str(k)] = int(u) bdd.incref(u) def _decode_node( s: str ) -> int: """Map `s` to node-like number.""" match s: case 'F': return -1 case 'T': return 1 return int(s) def _node_from_int( uid: int, bdd: _BDD, cache: _abc.Mapping[str, int] ) -> _Ref: """Return `bdd` node represented by `uid`.""" if uid == -1: return bdd.false elif uid == 1: return bdd.true # not constant k = cache[str(abs(uid))] u = bdd._add_int(k) return ~ u if uid < 0 else u def _node_to_int( u: _Ref ) -> int: """Return numeric representation of `u`.""" z = _flip(u, u) k = int(z) return -k if u.negated else k ================================================ FILE: dd/_parser.py ================================================ """Construct BDD nodes from quantified Boolean formulae.""" # Copyright 2015 by California Institute of Technology # All rights reserved. Licensed under BSD-3. # import collections.abc as _abc import logging import typing as _ty import astutils _TABMODULE: _ty.Final[str] =\ 'dd._expr_parser_state_machine' class _Token(_ty.Protocol): type: str value: str class Lexer(astutils.Lexer): """Lexer for Boolean formulae.""" def __init__( self ) -> None: self.reserved = { 'ite': 'ITE', 'False': 'FALSE', 'True': 'TRUE', 'FALSE': 'FALSE', 'TRUE': 'TRUE'} self.delimiters = [ 'LPAREN', 'RPAREN', 'COMMA'] self.operators = [ 'NOT', 'AND', 'OR', 'XOR', 'IMPLIES', 'EQUIV', 'EQUALS', 'MINUS', 'DIV', 'AT', 'COLON', 'FORALL', 'EXISTS', 'RENAME'] self.misc = [ 'NAME', 'NUMBER'] super().__init__() def t_NAME( self, token: _Token ) -> _Token: r""" [A-Za-z_] [A-Za-z0-9_']* """ token.type = self.reserved.get( token.value, 'NAME') return token def t_AND( self, token: _Token ) -> _Token: r""" \&\& | \& | /\\ """ token.value = '&' return token def t_OR( self, token: _Token ) -> _Token: r""" \|\| | \| | \\/ """ token.value = '|' return token def t_NOT( self, token: _Token ) -> _Token: r""" \~ | ! """ token.value = '!' return token def t_IMPLIES( self, token: _Token ) -> _Token: r""" => | \-> """ token.value = '=>' return token def t_EQUIV( self, token: _Token ) -> _Token: r""" <=> | <\-> """ token.value = '<->' return token t_XOR = r''' \# | \^ ''' t_EQUALS = r' = ' t_LPAREN = r' \( ' t_RPAREN = r' \) ' t_MINUS = r' \- ' t_NUMBER = r' \d+ ' t_COMMA = r' , ' t_COLON = r' : ' t_FORALL = r' \\ A ' t_EXISTS = r' \\ E ' t_RENAME = r' \\ S ' t_DIV = r' / ' t_AT = r' @ ' t_ignore = ''.join(['\x20', '\t']) def t_trailing_comment( self, token: _Token ) -> None: r' \\ \* .* ' return None def t_doubly_delimited_comment( self, token: _Token ) -> None: r""" \( \* [\s\S]*? \* \) """ return None def t_newline( self, token ) -> None: r' \n+ ' _ParserResult = _ty.TypeVar('_ParserResult') class _ParserProtocol( _ty.Protocol, _ty.Generic[ _ParserResult]): """Parser internal interface.""" def _apply( self, operator: str, *operands: _ty.Any ) -> _ParserResult: ... def _add_var( self, name: str ) -> _ParserResult: ... def _add_int( self, numeric_literal: str ) -> _ParserResult: ... def _add_bool( self, bool_literal: str ) -> _ParserResult: ... class Parser( astutils.Parser, _ParserProtocol): """Parser for Boolean formulae.""" def __init__( self ) -> None: tabmodule_is_defined = ( hasattr(self, 'tabmodule') and self.tabmodule) if not tabmodule_is_defined: self.tabmodule = _TABMODULE self.start = 'expr' # low to high self.precedence = ( ('left', 'COLON'), ('left', 'EQUIV'), ('left', 'IMPLIES'), ('left', 'MINUS'), ('left', 'XOR'), ('left', 'OR'), ('left', 'AND'), ('left', 'EQUALS'), ('right', 'NOT'), ) kw = dict() kw.setdefault('lexer', Lexer()) super().__init__(**kw) def _apply( self, operator: str, *operands: _ty.Any): """Return syntax tree of application.""" if operator == r'\S': match operands: case subs, expr: pass case _: raise AssertionError(operands) return self.nodes.Operator( operator, expr, subs) return self.nodes.Operator( operator, *operands) def _add_var( self, name: str): """Return syntax tree for identifier.""" return self.nodes.Terminal( name, 'var') def _add_int( self, numeric_literal: str): """Return syntax tree of given index.""" return self.nodes.Terminal( numeric_literal, 'num') def _add_bool( self, bool_literal: str): """Return syntax tree for Boolean.""" return self.nodes.Terminal( bool_literal, 'bool') def p_bool( self, p: list ) -> None: """expr : TRUE | FALSE """ p[0] = self._add_bool(p[1]) def p_node( self, p: list ) -> None: """expr : AT number""" p[0] = p[2] def p_number( self, p: list ) -> None: """number : NUMBER""" p[0] = self._add_int(p[1]) def p_negative_number( self, p: list ) -> None: """number : MINUS NUMBER """ numeric_literal = f'{p[1]}{p[2]}' p[0] = self._add_int(numeric_literal) def p_var( self, p: list ) -> None: """expr : name""" p[0] = self._add_var(p[1].value) def p_unary( self, p: list ) -> None: """expr : NOT expr""" p[0] = self._apply( p[1], p[2]) def p_binary( self, p: list ) -> None: """expr : expr AND expr | expr OR expr | expr XOR expr | expr IMPLIES expr | expr EQUIV expr | expr EQUALS expr | expr MINUS expr """ p[0] = self._apply( p[2], p[1], p[3]) def p_ternary_conditional( self, p: list ) -> None: ("""expr : ITE LPAREN """ """ expr COMMA """ """ expr COMMA """ """ expr RPAREN""") p[0] = self._apply( p[1], p[3], p[5], p[7]) def p_quantifier( self, p: list ) -> None: """expr : EXISTS names COLON expr | FORALL names COLON expr """ p[0] = self._apply( p[1], p[2], p[4]) def p_rename( self, p: list ) -> None: """expr : RENAME subs COLON expr""" p[0] = self._apply( p[1], p[2], p[4]) def p_substitutions_iter( self, p: list ) -> None: """subs : subs COMMA sub""" u = p[1] u.append(p[3]) p[0] = u def p_substitutions_end( self, p: list ) -> None: """subs : sub""" p[0] = [p[1]] def p_substitution( self, p: list ) -> None: """sub : name DIV name""" new = p[1] old = p[3] p[0] = (old, new) def p_names_iter( self, p: list ) -> None: """names : names COMMA name""" u = p[1] u.append(p[3]) p[0] = u def p_names_end( self, p: list ) -> None: """names : name""" p[0] = [p[1]] def p_name( self, p: list ) -> None: """name : NAME""" p[0] = self.nodes.Terminal( p[1], 'var') def p_paren( self, p: list ) -> None: """expr : LPAREN expr RPAREN""" p[0] = p[2] _Ref = _ty.TypeVar('_Ref') class _BDD(_ty.Protocol[ _Ref]): """Interface of BDD context.""" @property def false( self ) -> _Ref: ... @property def true( self ) -> _Ref: ... def var( self, name: str ) -> _Ref: ... def apply( self, op: str, u: _Ref, v: _Ref | None=None, w: _Ref | None=None ) -> _Ref: ... def quantify( self, u: _Ref, qvars: set[str], forall: bool=False ) -> _Ref: ... def rename( self, u: _Ref, renaming: _abc.Mapping ) -> _Ref: ... def _add_int( self, number: int ) -> _Ref: ... class _Translator(Parser): """Parser for Boolean formulas.""" def __init__( self ) -> None: super().__init__() self._reset_state() def parse( self, expression: str, bdd: _BDD[_Ref] ) -> _Ref: """Return BDD of `expression`. The returned BDD is stored in the BDD manager `bdd`. """ self._bdd = bdd u = super().parse(expression) self._reset_state() return u def _reset_state( self ) -> None: """Set default attribute values.""" self._bdd = None has_lr_stack = ( self.parser is not None and hasattr(self.parser, 'statestack') and hasattr(self.parser, 'symstack')) if not has_lr_stack: return self.parser.restart() # Avoid references to BDD nodes # remaining in the LR stack, # because this side-effect would # change the reference-counts. def _add_bool( self, bool_literal: str): """Return BDD for Boolean values.""" value = bool_literal.lower() if value not in {'false', 'true'}: raise ValueError(value) return getattr(self._bdd, value) def _add_int( self, numeric_literal: str): """Return BDD with given index.""" number = int(numeric_literal) return self._bdd._add_int(number) def _add_var( self, name: str): """Return BDD for variable `name`.""" return self._bdd.var(name) def _apply( self, operator: str, *operands: _ty.Any): """Return BDD from applying `operator`.""" match operator: case r'\A' | r'\E': names, expr = operands names = { x.value for x in names} forall = (operator == r'\A') return self._bdd.quantify( expr, names, forall=forall) case r'\S': subs, expr = operands renaming = { k.value: v.value for k, v in subs} return self._bdd.rename( expr, renaming) return self._bdd.apply( operator, *operands) _parsers = dict() def add_expr( expression: str, bdd: _BDD[_Ref] ) -> _Ref: """Return `bdd` node for `expression`. Creates in `bdd` a node that represents `expression`, and returns this node. """ if 'boolean' not in _parsers: _parsers['boolean'] = _Translator() translator = _parsers['boolean'] return translator.parse(expression, bdd) def _rewrite_tables( outputdir: str='./' ) -> None: """Recache state machine of parser.""" astutils.rewrite_tables( Parser, _TABMODULE, outputdir) astutils.rewrite_tables( _Translator, _TABMODULE, outputdir) def _main( ) -> None: """Recompute parser state machine. Cache the state machine in a file. Configure logging. """ log = logging.getLogger('astutils') log.setLevel('DEBUG') log.addHandler(logging.StreamHandler()) _rewrite_tables() if __name__ == '__main__': _main() ================================================ FILE: dd/_utils.py ================================================ """Convenience functions.""" # Copyright 2017-2018 by California Institute of Technology # All rights reserved. Licensed under 3-clause BSD. # import collections as _cl import collections.abc as _abc import os import shlex as _sh import subprocess as _sbp import textwrap as _tw import types import typing as _ty import dd._abc try: import networkx as _nx except ImportError as error: _nx = None _nx_error = error if _nx is not None: MultiDiGraph: _ty.TypeAlias = _nx.MultiDiGraph # The mapping from values of argument `op` of # `__richcmp__()` of Cython objects, # to the corresponding operator symbols. # Constants are defined in `cpython.object`. _CY_SYMBOLS: _ty.Final = { 2: '==', 3: '!=', 0: '<', 1: '<=', 4: '>', 5: '>='} def import_module( module_name: str ) -> types.ModuleType: """Return module with `module_name`, if present. Raise `ImportError` otherwise. """ modules = dict( networkx=_nx) if module_name in modules: return modules[module_name] errors = dict( networkx=_nx_error) raise errors[module_name] def print_var_levels( bdd ) -> None: """Print `bdd` variables ordered by level.""" n = len(bdd.vars) levels = [ bdd.var_at_level(level) for level in range(n)] print( 'Variable order (starting at level 0):\n' f'{levels}') def var_counts( bdd ) -> str: """Return levels and numbers of variables, CUDD indices. @type bdd: `dd.cudd.BDD` or `dd.cudd_zdd.ZDD` """ n_declared_vars = len(bdd.vars) n_cudd_vars = bdd._number_of_cudd_vars() return _tw.dedent(f''' There are: {n_cudd_vars} variable indices in CUDD, {n_declared_vars} declared variables in {bdd!r}. So the set of levels of the declared variables is not a contiguous range of integers. This can occur when specific levels have been given to `{type(bdd)}.add_var()`. The declared variables and their levels are: {bdd.var_levels} ''') def contiguous_levels( callable: str, bdd ) -> str: """Return requirement about contiguous levels. @type bdd: `dd.cudd.BDD` or `dd.cudd_zdd.ZDD` """ return _tw.dedent(f''' The callable `{callable}()` requires that the number of variable indices in CUDD, and the number of declared variables in {bdd!r} be equal. ''') def raise_runtimerror_about_ref_count( ref_count_lb: int, name: str, class_name: str ) -> _ty.NoReturn: """Raise `RuntimeError` about reference count lower bound. Call this function when an unexpected nonpositive lower bound on a node's reference count is detected for a `Function` instance. @param ref_count_lb: lower bound on the reference count of the node that the `Function` instance points to. ```tla ASSUME ref_count_lb <= 0 ``` @param name: to mention as location where the error was detected. For example: ```python 'method `dd.cudd.BDD.decref`' ``` @param class_name: to mention as name of the class of the object where the value `ref_count_lb` was found. For example: ```python '`dd.cudd.Function`' ``` """ if ref_count_lb > 0: raise ValueError(ref_count_lb) raise RuntimeError( f'The {name} requires ' 'that `u._ref > 0` ' f'(where `u` is an instance of {class_name}). ' 'This ensures that deallocated memory ' 'in CUDD will not be accessed. The current ' f'value of attribute `_ref` is:\n{ref_count_lb}\n' 'For more information read the docstring of ' f'the class {class_name}.') @_ty.overload def map_container( mapper: _abc.Callable, container: _abc.Mapping ) -> dict: ... @_ty.overload def map_container( mapper: _abc.Callable, container: _abc.Iterable ) -> list: ... def map_container( mapper, container): """Map `container`, using `mapper()`. If `container` is a sequence, then map each item. If `container` is a mapping of keys to values, then map each value. """ if isinstance(container, _abc.Mapping): return _map_values(mapper, container) return list(map(mapper, container)) def _map_values( mapper: _abc.Callable, kv: _abc.Mapping ) -> dict: """Map each value of `kv` using `mapper()`. The keys of `kv` remain unchanged. """ return {k: mapper(v) for k, v in kv.items()} def values_of( container: _abc.Mapping | _abc.Collection ) -> _abc.Iterable: """Return container values. @return: - `container.values()` if `container` is a mapping - `container` otherwise """ if isinstance(container, _abc.Mapping): return container.values() return container def total_memory( ) -> ( int | None): """Return number of bytes of memory. Requires that: - `SC_PAGE_SIZE` and - `SC_PHYS_PAGES` be readable via `os.sysconf()`. """ names = os.sysconf_names has_both = ( 'SC_PAGE_SIZE' in names and 'SC_PHYS_PAGES' in names) if not has_both: print( 'skipping check that ' 'initial memory estimate fits ' 'in available memory of system, ' "because either `'SC_PAGE_SIZE'` or " "`'SC_PHYS_PAGES'` undefined in " '`os.sysconf_names`.') return None page_size = os.sysconf('SC_PAGE_SIZE') n_pages = os.sysconf('SC_PHYS_PAGES') both_defined = ( page_size >= 0 and n_pages >= 0) if not both_defined: return None return page_size * n_pages _OPERATOR_MAP: _ty.Final = dict( bdd=dict( unary=dd._abc.UNARY_OPERATOR_SYMBOLS, binary=dd._abc.BINARY_OPERATOR_SYMBOLS, ternary=dd._abc.TERNARY_OPERATOR_SYMBOLS, all=dd._abc.BDD_OPERATOR_SYMBOLS)) def assert_operator_arity( op: str, v: object | None, w: object | None, diagram_type: _ty.Literal[ 'bdd'] ) -> None: """Raise `ValueError` if unexpected values. Asserts: - `op` is an operator symbol - `v` is `None` if `op` is a unary operator - `w` is `None` if `op` has arity <= 2 """ operators = _OPERATOR_MAP[diagram_type] if op not in operators['all']: raise ValueError( f'Unknown operator: "{op}"') if op in operators['unary']: if v is not None: raise ValueError( f'`v is not None`, but: {v}') if w is not None: raise ValueError( f'`w is not None`, but: {w}') elif op in operators['binary']: if v is None: raise ValueError( '`v is None`') if w is not None: raise ValueError( f'`w is not None`, but: {w}') elif op in operators['ternary']: if v is None: raise ValueError( '`v is None`') if w is None: raise ValueError( '`w is None`') _GraphType: _ty.TypeAlias = _ty.Literal[ 'digraph', 'graph', 'subgraph'] DOT_FILE_TYPES: _ty.Final = { 'pdf', 'svg', 'png', 'dot'} class DotGraph: def __init__( self, graph_type: _GraphType='digraph', rank: str | None=None ) -> None: """A DOT graph.""" self.graph_type = graph_type self.rank = rank self.nodes = _cl.defaultdict(dict) self.edges = _cl.defaultdict(list) self.subgraphs = list() def add_node( self, node, **kw ) -> None: """Add node with attributes `kw`. If node exists, update its attributes. """ self.nodes[node].update(kw) def add_edge( self, start_node, end_node, **kw ) -> None: """Add edge with attributes `kw`. Multiple edges can exist between the same nodes. """ self.edges[start_node, end_node].append(kw) def to_dot( self, graph_type: _GraphType | None=None ) -> str: """Return DOT code.""" subgraphs = ''.join( g.to_dot( graph_type='subgraph') for g in self.subgraphs) def format_attributes( attr ) -> str: """Return formatted assignment.""" return ', '.join( f'{k}="{v}"' for k, v in attr.items()) def format_node( u, attr ) -> str: """Return DOT code for node.""" attributes = format_attributes(attr) return f'{u} [{attributes}];' def format_edge( u, v, attr ) -> str: """Return DOT code for edge.""" attributes = format_attributes(attr) return f'{u} -> {v} [{attributes}];' nodes = '\n'.join( format_node(u, attr) for u, attr in self.nodes.items()) edges = list() for (u, v), attrs in self.edges.items(): for attr in attrs: edge = format_edge(u, v, attr) edges.append(edge) edges = '\n'.join(edges) indent_level = 4 * '\x20' def fmt( text: str ) -> str: """Return indented text.""" newline = '\n' if text else '' return newline + _tw.indent( text, prefix=4 * indent_level) nodes = fmt(nodes) edges = fmt(edges) subgraphs = fmt(subgraphs) if graph_type is None: graph_type = self.graph_type if self.rank is None: rank = '' else: rank = f'rank = {self.rank}' return _tw.dedent(f''' {graph_type} {{ {rank}{nodes}{edges}{subgraphs} }} ''') def dump( self, filename: str, filetype: str, **kw ) -> None: """Write to file.""" if filetype not in DOT_FILE_TYPES: raise ValueError( f'Unknown file type "{filetype}" ' f'for "{filename}"') dot_code = self.to_dot() if filetype == 'dot': with open(filename, 'w') as fd: fd.write(dot_code) return dot = _sh.split(f''' dot -T{filetype} -o '{filename}' ''') _sbp.run( dot, encoding='utf8', input=dot_code, capture_output=True, check=True) ================================================ FILE: dd/autoref.py ================================================ """Wraps `dd.bdd` to automate reference counting. For function docstrings, refer to `dd.bdd`. """ # Copyright 2015 by California Institute of Technology # All rights reserved. Licensed under BSD-3. # import collections.abc as _abc import logging import typing as _ty import warnings import dd._abc import dd._copy as _copy import dd._utils as _utils import dd.bdd as _bdd log = logging.getLogger(__name__) _Yes: _ty.TypeAlias = dd._abc.Yes _Cardinality: _ty.TypeAlias = dd._abc.Cardinality _VariableName: _ty.TypeAlias = dd._abc.VariableName _Level: _ty.TypeAlias = dd._abc.Level _VariableLevels: _ty.TypeAlias = dd._abc.VariableLevels _Ref: _ty.TypeAlias = _ty.Union['Function'] _MaybeRef: _ty.TypeAlias = '''( _Ref | None )''' _Fork: _ty.TypeAlias = '''( tuple[ _Level, _MaybeRef, _MaybeRef] )''' _Assignment: _ty.TypeAlias = dd._abc.Assignment _Renaming: _ty.TypeAlias = dd._abc.Renaming _Formula: _ty.TypeAlias = dd._abc.Formula class BDD(dd._abc.BDD[_Ref]): """Shared ordered binary decision diagram. It takes and returns `Function` instances, which automate reference counting. Attributes: - `vars`: `dict` mapping `variables` to `int` levels Do not assign the `dict` itself. For docstrings, refer to methods of `dd.bdd.BDD`, with the difference that `Function`s replace nodes as arguments and returned types. """ # omitted docstrings are inheritted from `super()` def __init__( self, levels: _VariableLevels | None=None): manager = _bdd.BDD(levels) self._bdd = manager self.vars: _VariableLevels = manager.vars def __eq__( self, other: 'BDD' ) -> _Yes: if not isinstance(other, BDD): raise NotImplementedError return (self._bdd is other._bdd) def __len__( self ) -> _Cardinality: return len(self._bdd) def __contains__( self, u: _Ref ) -> _Yes: if self is not u.bdd: raise ValueError('`self is not u.bdd`') return u.node in self._bdd def __str__( self ) -> str: return ( 'Binary decision diagram (`dd.bdd.BDD` wrapper):\n' '------------------------\n' f'\t {len(self.vars)} BDD variables\n' f'\t {len(self)} nodes\n') def _wrap( self, u: int ) -> _Ref: """Return reference to node `u`. References can be thought of also as edges. @param u: node in `self._bdd` """ if u not in self._bdd: raise ValueError(u) return Function(u, self) def configure( self, **kw ) -> dict[ str, _ty.Any]: return self._bdd.configure(**kw) def succ( self, u ) -> _Fork: i, v, w = self._bdd.succ(u.node) def wrap( node: int | None ) -> _MaybeRef: match node: case None: return None case int(): return self._wrap(node) raise AssertionError(node) return i, wrap(v), wrap(w) def incref( self, u: _Ref ) -> None: self._bdd.incref(u.node) def decref( self, u: _Ref, **kw ) -> None: self._bdd.decref(u.node) def declare( self, *variables: _VariableName ) -> None: for var in variables: self.add_var(var) def add_var( self, var: _VariableName, level: _Level | None=None ) -> _Level: return self._bdd.add_var(var, level=level) def var( self, var: _VariableName ) -> _Ref: r = self._bdd.var(var) return self._wrap(r) def var_at_level( self, level: _Level ) -> _VariableName: return self._bdd.var_at_level(level) def level_of_var( self, var: _VariableName ) -> _Level: return self._bdd.level_of_var(var) @property def var_levels( self ) -> _VariableLevels: return self._bdd.var_levels def reorder( self, var_order: _VariableLevels | None=None ) -> None: reorder(self, var_order) def copy( self, u: _Ref, other: 'BDD' ) -> _Ref: if u not in self: raise ValueError(u) if self is other: log.warning('copying node to same manager') return u r = self._bdd.copy(u.node, other._bdd) return other._wrap(r) def support( self, u: _Ref, as_levels: _Yes=False ) -> set[_VariableName]: if u not in self: raise ValueError(u) return self._bdd.support(u.node, as_levels) def let( self, definitions: _Renaming | _Assignment | dict[_VariableName, _Ref], u: _Ref ) -> _Ref: if u not in self: raise ValueError(u) if not definitions: return u var = next(iter(definitions)) value = definitions[var] match value: case str() | bool(): d = definitions case Function(): def node_of( ref ) -> int: if isinstance(ref, Function): return ref.node raise ValueError( 'Expected homogeneous type ' 'for `dict` values.') d = { var: node_of(value) for var, value in definitions.items()} case _: raise TypeError(value) r = self._bdd.let(d, u.node) return self._wrap(r) def quantify( self, u: _Ref, qvars: _abc.Iterable[ _VariableName], forall: _Yes=False ) -> _Ref: if u not in self: raise ValueError(u) r = self._bdd.quantify(u.node, qvars, forall) return self._wrap(r) def forall( self, qvars: _abc.Iterable[_VariableName], u: _Ref ) -> _Ref: return self.quantify(u, qvars, forall=True) def exist( self, qvars: _abc.Iterable[ _VariableName], u: _Ref ) -> _Ref: return self.quantify(u, qvars, forall=False) def ite( self, g: _Ref, u: _Ref, v: _Ref ) -> _Ref: if g not in self: raise ValueError(g) if u not in self: raise ValueError(u) if v not in self: raise ValueError(v) r = self._bdd.ite(g.node, u.node, v.node) return self._wrap(r) def find_or_add( self, var: _VariableName, low: _Ref, high: _Ref ) -> _Ref: """Return node `IF var THEN high ELSE low`.""" level = self.level_of_var(var) r = self._bdd.find_or_add(level, low.node, high.node) return self._wrap(r) def count( self, u: _Ref, nvars: _Cardinality | None=None ) -> _Cardinality: if u not in self: raise ValueError(u) return self._bdd.count(u.node, nvars) def pick_iter( self, u: _Ref, care_vars: set[_VariableName] | None=None ) -> _abc.Iterable[ _Assignment]: if u not in self: raise ValueError(u) return self._bdd.pick_iter(u.node, care_vars) def add_expr( self, e: _Formula ) -> _Ref: r = self._bdd.add_expr(e) return self._wrap(r) def to_expr( self, u: _Ref ) -> _Formula: if u not in self: raise ValueError(u) return self._bdd.to_expr(u.node) def apply( self, op: dd._abc.OperatorSymbol, u: _Ref, v: _MaybeRef =None, w: _MaybeRef =None ) -> _Ref: if u not in self: raise ValueError(u) if v is None and w is not None: raise ValueError(w) if v is not None and v not in self: raise ValueError(v) if w is not None and w not in self: raise ValueError(w) if v is None: r = self._bdd.apply(op, u.node) elif w is None: r = self._bdd.apply(op, u.node, v.node) else: r = self._bdd.apply(op, u.node, v.node, w.node) return self._wrap(r) def _add_int( self, i: int ) -> _Ref: r = self._bdd._add_int(i) return self._wrap(r) def cube( self, dvars: _Assignment ) -> _Ref: r = self._bdd.cube(dvars) return self._wrap(r) def collect_garbage( self ) -> None: """Recursively remove nodes with zero reference count.""" self._bdd.collect_garbage() def dump( self, filename: str, roots: dict[str, _Ref] | list[_Ref] | None=None, filetype: dd._abc.BDDFileType | dd._abc.PickleFileType | None=None, **kw ) -> None: """Write BDDs to `filename`. The file type is inferred from the extension (case insensitive), unless a `filetype` is explicitly given. `filetype` can have the values: - `'pickle'` for Pickle - `'pdf'` for PDF - `'png'` for PNG - `'svg'` for SVG - `'json'` for JSON If `filetype is None`, then `filename` must have an extension that matches one of the file types listed above. Dump nodes reachable from `roots`. If `roots is None`, then all nodes in the manager are dumped. Dumping a JSON file requires that `roots` be nonempty. @type roots: - `list` of nodes, or - for JSON or Pickle: `dict` that maps names to nodes """ # The method's docstring is a slight modification # of the docstring of the method `dd._abc.BDD.dump`. if filetype is None: name = filename.lower() if name.endswith('.pdf'): filetype = 'pdf' elif name.endswith('.png'): filetype = 'png' elif name.endswith('.svg'): filetype = 'svg' elif name.endswith('.dot'): filetype = 'dot' elif name.endswith('.p'): filetype = 'pickle' elif name.endswith('.json'): filetype = 'json' else: raise ValueError( 'cannot infer file type ' 'from extension of file ' f'name "{filename}"') if filetype == 'json': if roots is None: raise ValueError(roots) _copy.dump_json(roots, filename) return elif (filetype != 'pickle' and filetype not in _utils.DOT_FILE_TYPES): raise ValueError(filetype) if roots is not None: def mapper(u): return u.node roots = _utils.map_container( mapper, roots) self._bdd.dump( filename, roots=roots, filetype=filetype) def load( self, filename: str, levels: _Yes=True ) -> ( dict[str, _Ref] | list[_Ref]): """Load nodes from Pickle or JSON file `filename`. If `levels is True`, then load variables at the same levels. Otherwise, add missing variables. @return: roots of the loaded BDDs @rtype: depends on the contents of the file, either: - `dict` that maps names to nodes, or - `list` of nodes """ # This method's docstring is a slight # modification of the docstring of # the method `dd._abc.BDD.dump`. name = filename.lower() if name.endswith('.p'): return self._load_pickle( filename, levels=levels) elif name.endswith('.json'): nodes = _copy.load_json(filename, self) def check( node ) -> Function: if isinstance(node, Function): return node raise AssertionError(node) match nodes: case dict(): return { k: check(v) for k, v in nodes.items()} case list(): return list(map(check, nodes)) case _: raise AssertionError(nodes) else: raise ValueError( f'Unknown file type of "{filename}"') def _load_pickle( self, filename: str, levels: _Yes=True ) -> ( dict[str, _Ref] | list[_Ref]): roots = self._bdd.load(filename, levels=levels) return _utils.map_container(self._wrap, roots) def assert_consistent( self ) -> None: self._bdd.assert_consistent() @property def false( self ) -> _Ref: u = self._bdd.false return self._wrap(u) @property def true( self ) ->_Ref: u = self._bdd.true return self._wrap(u) def image( trans: _Ref, source: _Ref, rename: _Renaming, qvars: set[_VariableName], forall: _Yes=False ) -> _Ref: if trans.bdd is not source.bdd: raise ValueError( (trans.bdd, source.bdd)) u = _bdd.image( trans.node, source.node, rename, qvars, trans.manager, forall) return trans.bdd._wrap(u) def preimage( trans: _Ref, target: _Ref, rename: _Renaming, qvars: set[_VariableName], forall: _Yes=False ) -> _Ref: if trans.bdd is not target.bdd: raise ValueError( (trans.bdd, target.bdd)) u = _bdd.preimage( trans.node, target.node, rename, qvars, trans.manager, forall) return trans.bdd._wrap(u) def reorder( bdd: BDD, order: _VariableLevels | None=None ) -> None: """Apply Rudell's sifting algorithm to `bdd`.""" _bdd.reorder(bdd._bdd, order=order) def copy_vars( source: BDD, target: BDD ) -> None: _copy.copy_vars(source._bdd, target._bdd) def copy_bdd( u: _Ref, target: BDD ) -> _Ref: r = _bdd.copy_bdd(u.node, u.manager, target._bdd) return target._wrap(r) class Function(dd._abc.Operator): r"""Convenience wrapper for edges returned by `BDD`. ```python import dd.autoref bdd = dd.autoref.BDD() bdd.declare('x', 'y') nd = bdd._bdd.add_expr(r'x /\ y') # `nd` is an integer # `bdd._bdd` is an instance of the # class `dd.bdd.BDD` u = _bdd.Function(nd, bdd) ``` Attributes: - `node`: `int` that describes edge (signed node) - `bdd`: `dd.autoref.BDD` instance that node belongs to - `manager`: `dd.bdd.BDD` instance that node belongs to Operations are valid only between functions with the same `BDD` in `Function.bdd`. After all references to a `Function` have been deleted, the reference count of its associated node is decremented. To explicitly release a `Function` instance, invoke `del f`. The design here is inspired by the PyEDA package. """ def __init__( self, node: int, bdd: BDD ) -> None: if node not in bdd._bdd: raise ValueError(node) self.bdd = bdd self.manager = bdd._bdd self.node = node self.manager.incref(node) def __hash__( self ) -> int: return self.node def to_expr( self ) -> _Formula: """Return Boolean expression of function.""" return self.manager.to_expr(self.node) def __int__( self ) -> int: return self.node def __len__( self ) -> _Cardinality: return len(self.manager.descendants([self.node])) @property def dag_size( self ) -> _Cardinality: return len(self) def __del__( self ) -> None: """Decrement reference count of `self.node` in `self.bdd`.""" if self.node is None: return node = self.node self.node = None self.manager.decref(node) def __eq__( self, other ) -> _Yes: if other is None: return False if not isinstance(other, Function): raise NotImplementedError if self.bdd is not other.bdd: raise ValueError((self.bdd, other.bdd)) return self.node == other.node def __ne__( self, other ) -> _Yes: if other is None: return True if not isinstance(other, Function): raise NotImplementedError if self.bdd is not other.bdd: raise ValueError((self.bdd, other.bdd)) return not (self == other) def __le__( self, other ) -> _Yes: if not isinstance(other, Function): raise NotImplementedError return (other | ~ self) == self.bdd.true def __lt__( self, other ) -> _Yes: if not isinstance(other, Function): raise NotImplementedError return self <= other and self != other def __invert__( self ) -> _Ref: return self._apply('not', other=None) def __and__( self, other: _Ref ) -> _Ref: return self._apply('and', other) def __or__( self, other: _Ref ) -> _Ref: return self._apply('or', other) def __xor__( self, other: _Ref ) -> _Ref: return self._apply('xor', other) def implies( self, other: _Ref ) -> _Ref: return self._apply('implies', other) def equiv( self, other: _Ref ) -> _Ref: return self._apply('equiv', other) def _apply( self, op: dd._abc.OperatorSymbol, other: _MaybeRef ) -> _Ref: """Return result of operation `op` with `other`.""" # unary op ? if other is None: u = self.manager.apply(op, self.node) else: if self.bdd is not other.bdd: raise ValueError((self.bdd, other.bdd)) u = self.manager.apply(op, self.node, other.node) return Function(u, self.bdd) @property def level( self ) -> _Level: i, _, _ = self.manager._succ[abs(self.node)] return i @property def var( self ) -> ( _VariableName | None): i, low, _ = self.manager._succ[abs(self.node)] if low is None: return None return self.manager.var_at_level(i) @property def low( self ) -> '''( _Ref | None )''': _, v, _ = self.manager._succ[abs(self.node)] if v is None: return None return Function(v, self.bdd) @property def high( self ) -> '''( _Ref | None )''': _, _, w = self.manager._succ[abs(self.node)] if w is None: return None return Function(w, self.bdd) @property def ref( self ) -> _Cardinality: return self.manager._ref[abs(self.node)] @property def negated( self ) -> _Yes: return self.node < 0 @property def support( self ) -> set[_VariableName]: return self.manager.support(self.node) ================================================ FILE: dd/bdd.py ================================================ """Ordered binary decision diagrams. References ========== Randal E. Bryant "Graph-based algorithms for Boolean function manipulation" IEEE Transactions on Computers Volume C-35, No. 8, August, 1986, pages 677--690 Karl S. Brace, Richard L. Rudell, Randal E. Bryant "Efficient implementation of a BDD package" 27th ACM/IEEE Design Automation Conference (DAC), 1990 pages 40--45 Richard Rudell "Dynamic variable ordering for ordered binary decision diagrams" IEEE/ACM International Conference on Computer-Aided Design (ICCAD), 1993 pages 42--47 Christel Baier and Joost-Pieter Katoen "Principles of model checking" MIT Press, 2008 Section 6.7, pages 381--421 Fabio Somenzi "Binary decision diagrams" Calculational system design, Vol.173 NATO Science Series F: Computer and systems sciences pages 303--366, IOS Press, 1999 Henrik R. Andersen "An introduction to binary decision diagrams" Lecture notes for "Efficient Algorithms and Programs", 1999 The IT University of Copenhagen """ # Copyright 2014 by California Institute of Technology # All rights reserved. Licensed under BSD-3. # import collections.abc as _abc import functools as _ft import inspect import logging import pickle import pprint as _pp import sys import typing as _ty import warnings import dd._abc import dd._parser as _parser import dd._utils as _utils logger = logging.getLogger(__name__) REORDER_STARTS = 100 REORDER_FACTOR = 2 GROWTH_FACTOR = 2 def _request_reordering( bdd: 'BDD' ) -> None: """Raise `NeedsReordering` if `len(bdd)` >= threshold.""" if bdd._last_len is None: return if len(bdd) >= REORDER_FACTOR * bdd._last_len: raise _NeedsReordering() _Ret = _ty.TypeVar('_Ret') _CallablePR: _ty.TypeAlias = _abc.Callable[..., _Ret] def _try_to_reorder( func: _CallablePR ) -> _CallablePR: """Decorator that serves reordering requests.""" @_ft.wraps(func) def _wrapper( bdd: 'BDD', *args, **kwargs ) -> _Ret: with _ReorderingContext(bdd): return func( bdd, *args, **kwargs) logger.info('Reordering needed...') # disable reordering requests while swapping bdd._last_len = None reorder(bdd) len_after = len(bdd) # try again, # reordering disabled to avoid livelock with _ReorderingContext(bdd): r = func( bdd, *args, **kwargs) # enable reordering requests bdd._last_len = GROWTH_FACTOR * len_after return r return _wrapper class _ReorderingContext: """Context manager that tracks decorator nesting.""" def __init__( self, bdd: 'BDD' ) -> None: self.bdd = bdd self.nested = None def __enter__( self): self.nested = self.bdd._reordering_context self.bdd._reordering_context = True def __exit__( self, ex_type, ex_value, tb): self.bdd._reordering_context = self.nested not_nested = ( ex_type is _NeedsReordering and not self.nested) if not_nested: return True class _NeedsReordering(Exception): """Raise this to request reordering.""" _Yes: _ty.TypeAlias = dd._abc.Yes _Nat: _ty.TypeAlias = dd._abc.Nat _Cardinality: _ty.TypeAlias = dd._abc.Cardinality _VariableName: _ty.TypeAlias = dd._abc.VariableName _Level: _ty.TypeAlias = dd._abc.Level _VariableLevels: _ty.TypeAlias = dd._abc.VariableLevels _Assignment: _ty.TypeAlias = dd._abc.Assignment _Renaming: _ty.TypeAlias = dd._abc.Renaming _Node: _ty.TypeAlias = _Nat _Ref: _ty.TypeAlias = int # ```tla # ASSUME # _Ref \neq 0 # ``` _Fork: _ty.TypeAlias = tuple[ _Level, _Ref | None, _Node | None] _Formula: _ty.TypeAlias = dd._abc.Formula class BDD(dd._abc.BDD[_Ref]): """Shared ordered binary decision diagram. The terminal node is 1. Nodes are positive integers, edges signed integers. Complemented edges are represented as negative integers. Values returned by methods are edges, possibly complemented. Attributes: - `vars`: `dict` mapping `variables` to `int` levels - `roots`: (optional) edges - `max_nodes`: raise `Exception` if this limit is reached. The default value is `sys.maxsize` in Python 3. Increase it if needed. To ensure that the target node of a returned edge is not garbage collected during reordering, increment its reference counter: `bdd.incref(edge)` To ensure that `ite` maintains reducedness add new nodes using `find_or_add` to keep the table updated, or call `update_predecessors` prior to calling `ite`. """ # omitted docstrings are inheritted from `super()` def __init__( self, levels: _VariableLevels | None=None ) -> None: if levels is None: levels = dict() _assert_valid_ordering(levels) self._pred: dict[ _Fork, _Node ] = dict() self._succ: dict[ _Node, _Fork ] = dict() self._ref: dict[ _Node, _Nat ] = dict() # all smaller positive integers # are used as node indices, and # no larger integers are used # as node indices self._min_free: _Nat = 2 # minimum number unused as BDD index self._ite_table: dict[ tuple[_Ref, _Ref, _Ref], _Ref ] = dict() # `(predicate, then, else) |-> edge` # cache for ternary conditional # ("ite" means "if-then-else") self.vars: _VariableLevels = dict() self._level_to_var: dict[ _Level, _VariableName ] = dict() # inverse of `self.vars` # handle no vars self._init_terminal(len(self.vars)) # for decorator nesting self._reordering_context: _Yes = False # after last reordering self._last_len: _Nat | None = None for var, level in levels.items(): self.add_var(var, level) # set of edges # optional self.roots: set = set() self.max_nodes: _Nat = sys.maxsize def __copy__( self ) -> 'BDD': bdd = BDD(self.vars) bdd._pred = dict(self._pred) bdd._succ = dict(self._succ) bdd._ref = dict(self._ref) bdd._min_free = self._min_free bdd.roots = set(self.roots) bdd.max_nodes = self.max_nodes return bdd def __del__( self ) -> None: """Assert that all remaining nodes are garbage.""" if self._ref[1] > 0: self.decref(1) # free ref from `self._init_terminal()` self.collect_garbage() refs_exist = any( v != 0 for v in self._ref.values()) if not refs_exist: return stack = inspect.stack() stack_str = _pp.pformat(stack) raise AssertionError( 'There are nodes still referenced ' 'upon shutdown. Details:\n' f'{self._ref}\n' f'{self._succ}\n' f'{self.vars}\n' f'{self._ite_table}\n' f'{type(self)}\n' f'{stack_str}') def __len__( self ) -> _Cardinality: return len(self._succ) def __contains__( self, u: _Ref ) -> _Yes: return abs(u) in self._succ def __iter__( self): return iter(self._succ) def __str__( self ) -> str: return ( 'Binary decision diagram:\n' '------------------------\n' f'var levels: {self.vars}\n' f'roots: {self.roots}\n') def configure( self, **kw ) -> dict[ str, _ty.Any]: """Read and apply parameter values. First read parameter values (returned as `dict`), then apply `kw`. Available keyword arguments: - `'reordering'`: if `True` then enable, else disable """ d = dict( reordering=(self._last_len is not None)) for k, v in kw.items(): if k == 'reordering': if v: self._last_len = max( REORDER_STARTS, len(self)) else: self._last_len = None else: raise ValueError( f'Unknown parameter "{k}"') return d @property def ordering( self): raise DeprecationWarning( 'use `dd.bdd.BDD.vars` ' 'instead of `.ordering`') def _init_terminal( self, level: _Level ) -> None: """Place constant node `1`. Used for initialization and to shift node `1` to lower levels, as fresh variables are being added. """ u = 1 t = (level, None, None) told = self._succ.setdefault(u, t) self._pred.pop(told, None) self._succ[u] = t self._pred[t] = u self._ref.setdefault(u, 1) def succ( self, u: _Ref ) -> _Fork: """Return `(level, low, high)` for `abs(u)`.""" return self._succ[abs(u)] def incref( self, u: _Ref ) -> None: """Increment reference count of node `u`.""" self._ref[abs(u)] += 1 def decref( self, u: _Ref ) -> None: """Decrement reference count of node `u`, with 0 as minimum value. """ if self._ref[abs(u)] <= 0: n = self._ref[abs(u)] warnings.warn( 'The method `dd.bdd.BDD.decref` was called ' f'for BDD node {u} with reference count {n}. ' 'This call has no effect. Calling `decref` ' 'for a node with nonpositive reference count ' 'may indicate a programming error.', UserWarning) return self._ref[abs(u)] -= 1 def ref( self, u: _Ref ) -> _Nat: """Return reference count of edge `u`.""" return self._ref[abs(u)] def declare( self, *variables: _VariableName ) -> None: for var in variables: self.add_var(var) def add_var( self, var: _VariableName, level: _Level | None=None ) -> _Level: """Declare a variable named `var` at `level`. The new variable is Boolean-valued. If `level` is absent, then add the new variable at the bottom level. Raise `ValueError` if: - `var` already exists at a level different than the given `level`, or - the given `level` is already used by another variable - `level` is not given and `var` does not exist, and the next level larger than the current bottom level is already used by another variable. If `var` already exists, and either `level` is not given, or `var` has `level`, then return without raising exceptions. @param var: name of new variable to declare @param level: level of new variable to declare @return: level of variable `var` """ # var already exists ? if var in self.vars: return self._check_var(var, level) # level already used ? level = self._next_free_level(var, level) # update the mappings between # vars and levels self.vars[var] = level self._level_to_var[level] = var # move the leaf node to # the new bottom level self._init_terminal(len(self.vars)) return level def _check_var( self, var: _VariableName, level: _Level | None ) -> _Level: """Assert that `var` has `level`. Return the level of `var`. Exceptions: - raise `ValueError` if: - `var` is not a declared variable, or - `level is not None` and `level` is not the level of variable `var` - raise `RuntimeError` if an unexpected value of level is found in `self.vars[var]` @param var: name of variable @param level: level of variable """ if var not in self.vars: raise ValueError( f'"{var}" is not the name of ' 'a declared variable') var_level = self.vars[var] if var_level is None or var_level < 0: raise RuntimeError( f'`{self.vars[var] = }` ' '(expected integer >= 0)') if level is None or level == var_level: return var_level raise ValueError( f'for variable "{var}": ' f'{level} = level != ' f'level of "{var}" = {var_level}') def _next_free_level( self, var, level: _Level | None ) -> _Nat: """Return a free level. Raise `ValueError`: - if the given `level` is already used by a variable, or - if `level is None` and the next level is used by a variable. If `level is None`, then return the next level after the current largest level. Otherwise, return the given `level`. @param var: name of intended new variable, used only to form the `ValueError` message @param level: level of intended new variable """ # assume next level is unoccupied if level is None: level = len(self.vars) if level < 0: raise AssertionError( f'`{level = } < 0') # level already used ? other = self._level_to_var.get(level) if other is None: return level raise ValueError( f'level {level} is already ' f'used by variable "{other}", ' 'choose another level for the ' f'new variable "{var}"') @_try_to_reorder def var( self, var: _VariableName ) -> _Ref: if var not in self.vars: raise ValueError( f'undeclared variable "{var}", ' 'the declared variables are:\n' f' {self.vars}') j = self.vars[var] u = self.find_or_add(j, -1, 1) return u def var_at_level( self, level: _Level ) -> _VariableName: if level not in self._level_to_var: raise ValueError( f'no variable has level: {level}, ' 'the current levels of all variables ' f'are: {self.vars}') return self._level_to_var[level] def level_of_var( self, var: _VariableName ) -> _Level: if var not in self.vars: raise ValueError( f'name "{var}" is not ' 'a declared variable, ' 'the declared variables are:' f' {self.vars}') return self.vars[var] @property def var_levels( self ) -> _VariableLevels: return dict(self.vars) @_ty.overload def _map_to_level( self, d: _abc.Mapping[ _VariableName, _ty.Any] | _abc.Mapping[ _Level, _ty.Any] ) -> dict[_Level, bool]: ... @_ty.overload def _map_to_level( self, d: _abc.Set[ _VariableName] | _abc.Set [_Level] ) -> set[_Level]: ... def _map_to_level( self, d: _abc.Mapping[ _VariableName, _ty.Any] | _abc.Mapping[ _Level, _ty.Any] | _abc.Set[ _VariableName] | _abc.Set[ _Level] ) -> ( dict[_Level, bool] | set[_Level]): """Map keys of `d` to variable levels. Uses `self.vars` to map the keys to levels. If `d` is an iterable but not a mapping, then an iterable is returned. """ match d: case _abc.Mapping(): d = dict(d) case _abc.Set(): d = set(d) case _: raise TypeError(d) if not d: match d: case dict(): return dict() case set(): return set() case _: raise TypeError(d) # are keys variable names ? u = next(iter(d)) if u not in self.vars: self._assert_keys_are_levels(d) match d: case dict(): return { int(k): v for k, v in d.items()} case set(): return set(map(int, d)) case _: raise ValueError(d) if isinstance(d, _abc.Mapping): return { self.vars[var]: bool(val) for var, val in d.items()} else: return { self.vars[k] for k in d} def _assert_keys_are_levels( self, kv: _abc.Iterable ) -> None: """Assert that `kv` values are levels. Raise `ValueError` if not. """ not_levels = set() def key_is_level( key ) -> _Yes: is_level = ( key in self._level_to_var) if not is_level: not_levels.add(key) return is_level keys_are_levels = all(map( key_is_level, kv)) if keys_are_levels: return def fmt(key): return ( f'key `{key}` ' 'is not a level') errors = ',\n'.join(map( fmt, not_levels)) raise ValueError( f'{errors},\n' 'currently the levels are:\n' f'{self._level_to_var = }') def _top_var( self, *nodes: _Ref ) -> _Level: def level_of(node): level, *_ = self._succ[abs(node)] return level return min(map(level_of, nodes)) def copy( self, u: _Ref, other: 'BDD' ) -> _Ref: """Transfer BDD with root `u` to `other`.""" return copy_bdd(u, self, other) def descendants( self, roots: _abc.Iterable[_Ref] ) -> set[_Ref]: """Return nodes reachable from `roots`. Nodes pointed to by references in `roots` are included. Nodes are represented as positive integers. """ abs_roots = set(map(abs, roots)) visited = set() for u in abs_roots: visited.add(1) self._descendants(u, visited) if not abs_roots.issubset(visited): raise AssertionError( (abs_roots, visited)) return visited def _descendants( self, u: _Ref, visited: set[_Node] ) -> None: r = abs(u) if r == 1 or r in visited: return _, v, w = self._succ[r] if not v: raise AssertionError(v) if not w: raise AssertionError(w) self._descendants(v, visited) self._descendants(w, visited) visited.add(r) def is_essential( self, u: _Ref, var: _VariableName ) -> _Yes: """Return `True` if `var` is essential for node `u`. If `var` is a name undeclared in `self.vars`, return `False`. """ i = self.vars.get(var) if i is None: return False iu, v, w = self._succ[abs(u)] # var above node u ? if i < iu: return False if i == iu: return True # u depends on node labeled with var ? if not v: raise AssertionError(v) if not w: raise AssertionError(w) if self.is_essential(v, var): return True if self.is_essential(w, var): return True return False def support( self, u: _Ref, as_levels: _Yes=False ) -> set[ _VariableName]: levels = set() nodes = set() self._support(u, levels, nodes) if as_levels: return levels return {self.var_at_level(i) for i in levels} def _support( self, u: _Ref, levels: set[_Level], nodes: set[_Ref]): """Recurse to collect variables in support.""" # exhausted all vars ? if len(levels) == len(self.vars): return # visited ? r = abs(u) if r in nodes: return nodes.add(r) # terminal ? if r == 1: return # add var i, v, w = self._succ[r] if not v: raise AssertionError(v) if not w: raise AssertionError(w) levels.add(i) # recurse self._support(v, levels, nodes) self._support(w, levels, nodes) def levels( self, skip_terminals: _Yes=False ) -> _abc.Iterable[ tuple[ _Ref, _Level, _Ref, _Node]]: """Return generator of tuples `(u, i, v, w)`. Where `i` ranges from terminals to root. @param skip_terminals: if `True`, then omit terminal nodes. """ if skip_terminals: n = len(self.vars) - 1 else: n = len(self.vars) for i in range(n, -1, -1): for u, (j, v, w) in self._succ.items(): if i != j: continue yield u, i, v, w def _levels( self ) -> dict[ _Level, set[_Node]]: """Return mapping from levels to nodes.""" n = len(self.vars) levels = { i: set() for var, i in self.vars.items()} levels[n] = set() for u, (i, v, w) in self._succ.items(): levels[i].add(u) levels.pop(n) return levels @_try_to_reorder def reduction( self): """Return copy reduced with respect to `self.vars`. This function has educational value. """ # terminals bdd = BDD(self.vars) umap = {1: 1} # non-terminals levels = self.levels( skip_terminals=True) for u, i, v, w in levels: if u <= 0: raise AssertionError(u) p, q = umap[abs(v)], umap[abs(w)] p = _flip(p, v) q = _flip(q, w) r = bdd.find_or_add(i, p, q) if r <= 0: raise AssertionError(r) umap[u] = r for v in self.roots: p = umap[abs(v)] p = _flip(p, v) bdd.roots.add(p) return bdd def undeclare_vars( self, *vrs ) -> set[str]: """Remove unused variables `vrs` from `self.vars`. Asserts that each variable in `vrs` corresponds to an empty level. If `vrs` is empty, then remove all unused variables. Garbage collection may need to be called before calling `undeclare_vars`, in order to collect unused nodes to obtain empty levels. """ for var in vrs: if var not in self.vars: raise ValueError( f'name "{var}" is not ' 'a declared variable. ' 'The declared variables are:\n' f'{self.vars}') full_levels = { i for i, _, _ in self._succ.values()} # remove only unused variables for var in vrs: level = self.level_of_var(var) if level in full_levels: raise ValueError( f'the given variable "{var}" is not ' 'at an empty level (i.e., there still ' f'exist BDD nodes at level {level}, ' f'where variable "{var}" is)') # keep unused variables not in `vrs` if vrs: full_levels |= { level for var, level in self.vars.items() if var not in vrs} # map old to new levels n = 1 + len(self.vars) # include terminal new_levels = [ i for i in range(n) if i in full_levels] new_levels = { i: new for new, i in enumerate(new_levels)} # update variables and level declarations rm_vars = { var for var, level in self.vars.items() if level not in full_levels} self.vars = { var: new_levels[old] for var, old in self.vars.items() if old in full_levels} self._level_to_var = { k: var for var, k in self.vars.items()} # update node levels self._succ = { u: (new_levels[i], v, w) for u, (i, v, w) in self._succ.items()} self._pred = { v: k for k, v in self._succ.items()} # clear cache self._ite_table = dict() return rm_vars def let( self, definitions: _Renaming | _Assignment | dict[ _VariableName, _Ref], u: _Ref ) -> _Ref: d = definitions if not d: logger.warning( 'Call to `BDD.let` with no effect: ' '`defs` is empty.') return u var = next(iter(definitions)) value = d[var] if isinstance(value, bool): return self.cofactor(u, d) elif isinstance(value, int): return self.compose(u, d) try: value + 's' except TypeError: raise ValueError( 'Key must be var name as `str`, ' 'or Boolean value as `bool`, ' 'or BDD node as `int`.') return self.rename(u, d) @_try_to_reorder def compose( self, f: _Ref, var_sub: dict[ _VariableName, _Ref] ) -> _Ref: """Return substitutions `var_sub` in `f`. @param f: node @param var_sub: `dict` that maps variables to BDD nodes """ cache = dict() if len(var_sub) == 1: (var, g), = var_sub.items() j = self.level_of_var(var) return self._compose( f, j, g, cache) else: dvars = { self.level_of_var(var): g for var, g in var_sub.items()} return self._vector_compose( f, dvars, cache) def _compose( self, f: _Ref, j: _Level, g: _Ref, cache: dict[ tuple[_Ref, _Ref], _Ref] ) -> _Ref: # terminal ? if abs(f) == 1: return f # cached ? if (f, g) in cache: return cache[(f, g)] # independent of j ? i, v, w = self._succ[abs(f)] if not v: raise AssertionError(v) if not w: raise AssertionError(w) # below j ? if j < i: return f elif i == j: r = self.ite(g, w, v) # complemented edge ? if f < 0: r = -r else: if i >= j: raise AssertionError( (i, j)) k, _, _ = self._succ[abs(g)] z = min(i, k) f0, f1 = self._top_cofactor(f, z) g0, g1 = self._top_cofactor(g, z) p = self._compose( f0, j, g0, cache) q = self._compose( f1, j, g1, cache) r = self.find_or_add(z, p, q) cache[(f, g)] = r return r def _vector_compose( self, f: _Ref, level_sub: dict[_Level, _Ref], cache: dict[_Node, _Ref] ) -> _Ref: # terminal ? if abs(f) == 1: return f # cached ? r = cache.get(abs(f)) if r is not None: if r == 0: raise AssertionError(r) # complement ? if f < 0: r = -r return r # recurse i, v, w = self._succ[abs(f)] if not v: raise AssertionError(v) if not w: raise AssertionError(w) p = self._vector_compose( v, level_sub, cache) q = self._vector_compose( w, level_sub, cache) # map this level g = level_sub.get(i) if g is None: g = self.find_or_add(i, -1, 1) r = self.ite(g, q, p) # memoize cache[abs(f)] = r # complement ? if f < 0: r = -r return r @_try_to_reorder def rename( self, u: _Ref, dvars: _Renaming ) -> _Ref: """Efficient rename to non-essential neighbors. @param dvars: `dict` from variabe levels to variable levels or from variable names to variable names """ return rename(u, self, dvars) def _top_cofactor( self, u: _Ref, i: _Level ) -> tuple[ _Ref, _Ref]: """Return successor pair with respect to level `i`.""" # terminal node ? if abs(u) == 1: return (u, u) # non-terminal node iu, v, w = self._succ[abs(u)] if not v: raise AssertionError(v) if not w: raise AssertionError(w) # u independent of var ? if i < iu: return (u, u) if iu != i: raise AssertionError( 'for i > iu, call cofactor instead ' f'({i = }, {iu = })') # u labeled with var # complement ? if u < 0: v, w = -v, -w return (v, w) @_try_to_reorder def cofactor( self, u: _Ref, values: _Assignment ) -> _Ref: """Replace variables in `u` with Booleans.""" level_values = self._map_to_level(values) cache = dict() ordvar = sorted(level_values) j = 0 if abs(u) not in self: raise ValueError( f'node {u} not in `self`') return self._cofactor( u, j, ordvar, level_values, cache) def _cofactor( self, u: _Ref, j: _Level, ordvar: list[_Level], values: dict[_Level, bool], cache: dict[_Ref, _Ref] ) -> _Ref: """Recurse to compute cofactor.""" # terminal ? if abs(u) == 1: return u # memoized ? if u in cache: return cache[u] i, v, w = self._succ[abs(u)] if not v: raise AssertionError(v) if not w: raise AssertionError(w) n = len(ordvar) # skip nonessential variables while j < n: if ordvar[j] < i: j += 1 else: break if j == n: # exhausted valuation return u if j >= n: raise AssertionError((j, n)) # recurse if i in values: val = values[i] if bool(val): v = w r = self._cofactor( v, j, ordvar, values, cache) else: p = self._cofactor( v, j, ordvar, values, cache) q = self._cofactor( w, j, ordvar, values, cache) r = self.find_or_add(i, p, q) # complement ? if u < 0: r = -r cache[u] = r return r @_try_to_reorder def quantify( self, u: _Ref, qvars: _abc.Iterable[ _VariableName], forall: _Yes=False ) -> _Ref: """Return existential or universal abstraction. @param u: node @param qvars: quantified variables @param forall: if `True`, then quantify `qvars` universally, else existentially. """ qvars = self._map_to_level(set(qvars)) cache = dict() ordvar = sorted(qvars) j = 0 return self._quantify( u, j, ordvar, qvars, forall, cache) def _quantify( self, u: _Ref, j: _Level, ordvar: list[_Level], qvars: set[_Level], forall: _Yes, cache: dict[_Ref, _Ref] ) -> _Ref: """Recurse to quantify variables.""" # terminal ? if abs(u) == 1: return u if u in cache: return cache[u] i, v, w = self._succ[abs(u)] if not v: raise AssertionError(v) if not w: raise AssertionError(w) # complement ? if u < 0: v, w = -v, -w n = len(ordvar) # skip nonessential variables while j < n: if ordvar[j] < i: j += 1 else: break else: # exhausted valuation return u # recurse p = self._quantify( v, j, ordvar, qvars, forall, cache) q = self._quantify( w, j, ordvar, qvars, forall, cache) if i in qvars: if forall: r = self.ite(p, q, -1) # conjoin else: r = self.ite(p, 1, q) # disjoin else: r = self.find_or_add(i, p, q) cache[u] = r return r def forall( self, qvars: _abc.Iterable[ _VariableName], u: _Ref ) -> _Ref: return self.quantify( u, qvars, forall=True) def exist( self, qvars: _abc.Iterable[ _VariableName], u: _Ref ) -> _Ref: return self.quantify( u, qvars, forall=False) @_try_to_reorder def ite( self, g: _Ref, u: _Ref, v: _Ref ) -> _Ref: # wrap so reordering can # delete unused nodes return self._ite(g, u, v) def _ite( self, g: _Ref, u: _Ref, v: _Ref ) -> _Ref: """Recurse to compute ternary conditional.""" # is g terminal ? if g == 1: return u elif g == -1: return v # g is non-terminal # already computed ? r = (g, u, v) w = self._ite_table.get(r) if w is not None: return w z = min(self._succ[abs(g)][0], self._succ[abs(u)][0], self._succ[abs(v)][0]) g0, g1 = self._top_cofactor(g, z) u0, u1 = self._top_cofactor(u, z) v0, v1 = self._top_cofactor(v, z) p = self._ite(g0, u0, v0) q = self._ite(g1, u1, v1) w = self.find_or_add(z, p, q) # cache self._ite_table[r] = w return w def find_or_add( self, i: _Level, v: _Ref, w: _Ref ) -> _Ref: """Return reference to specified node. The returned node is at level `i` with successors `v, w`. If such a node exists already, then it is quickly found in the cached table, and the reference returned. @param i: level in `range(n_vars - 1)` @param v: low edge @param w: high edge """ _request_reordering(self) if i < 0: raise ValueError( f'The given level: {i = } < 0') if i >= len(self.vars): raise ValueError( f'The given level: {i = } is not < of ' 'the number of ' f'declared variables ({len(self.vars)}) ' '(the set of levels is expected to ' 'comprise of contiguous numbers)') if abs(v) not in self._succ: raise ValueError( f'argument: {v = } is not ' 'a reference to an existing BDD node') if abs(w) not in self._succ: raise ValueError( f'argument: {w = } is not ' 'a reference to an existing BDD node') # ensure canonicity of complemented edges if w < 0: v, w = -v, -w r = -1 else: r = 1 # eliminate if v == w: return r * v # already exists ? t = (i, v, w) u = self._pred.get(t) if u is not None: return r * u # find a free integer u = self._min_free if u <= 1: raise AssertionError( f'min free index is {u}, ' 'which is <= 1') if u in self._succ: raise AssertionError( f'node index {u} ' 'is already used. ' f'{self._succ = }') # add node self._pred[t] = u self._succ[u] = t self._ref[u] = 0 self._min_free = self._next_free_int(u) # increment reference counters self.incref(v) self.incref(w) return r * u def _next_free_int( self, start: _Nat ) -> _Nat: """Return smallest unused integer `> start`.""" if start < 1: raise ValueError( f'{start} = start < 1') for i in range(start, self.max_nodes): if i not in self._succ: return i raise RuntimeError( 'full: reached `self.max_nodes` nodes ' f'({self.max_nodes = }).') def collect_garbage( self, roots: _abc.Iterable[_Ref] | None=None ) -> None: """Recursively remove unused nodes A node is unused when its reference count is zero. Removal starts from the nodes in `roots` with zero reference count. If no `roots` are given, then all nodes are scanned for zero reference counts. """ n = len(self) if roots is None: roots = self._ref def is_unused( u ) -> _Yes: return not self._ref[abs(u)] unused = filter( is_unused, roots) unused = set(map( abs, unused)) # keep terminal # # Filtering above implies 1 is kept, # except within `__del__()`. # There `roots` happens to be `None`. if 1 in unused: unused.remove(1) while unused: u = unused.pop() if u == 1: raise AssertionError(u) # remove i, v, w = self._succ.pop(u) if not v: raise AssertionError(v) if not w: raise AssertionError(w) u_ = self._pred.pop((i, v, w)) uref = self._ref.pop(u) self._min_free = min(u, self._min_free) if u != u_: raise AssertionError((u, u_)) if uref: raise AssertionError(uref) if self._min_free <= 1: raise AssertionError(self._min_free) # decrement reference counters self.decref(v) self.decref(w) # unused ? if not self._ref[abs(v)] and abs(v) != 1: unused.add(abs(v)) if not self._ref[w] and w != 1: unused.add(w) self._ite_table = dict() m = len(self) k = n - m if k < 0: raise AssertionError((n, m)) def update_predecessors( self ) -> None: """Update table `self._pred`. `self._pred` maps triplets `(level, low, high)` to nodes. """ for u, t in self._succ.items(): if abs(u) == 1: continue self._pred[t] = u def swap( self, x: _VariableName | _Level, y: _VariableName | _Level, all_levels: dict[ _Level, set[_Ref]] | None=None ) -> tuple[ _Nat, _Nat]: """Permute adjacent variables `x` and `y`. Swapping invokes the garbage collector, so be sure to `incref` nodes that should remain. @param x, y: variable name or level """ if all_levels is None: self.collect_garbage() all_levels = self._levels() logger.debug( f'swap variables "{x}" and "{y}"') if x in self.vars: x = self.vars[x] if y in self.vars: y = self.vars[y] match x: case int(): pass case _: raise ValueError(x) match y: case int(): pass case _: raise ValueError(y) if not (0 <= x < len(self.vars)): raise ValueError(x) if not (0 <= y < len(self.vars)): raise ValueError(y) # ensure x < y if x > y: x, y = y, x if x >= y: raise ValueError( (x, y)) if abs(x - y) != 1: raise ValueError( (x, y)) # count nodes oldsize = len(self._succ) # collect levels x and y levels: dict[ _Ref, dict[_Ref, tuple] ] = { x: dict(), y: dict()} for j in (x, y): for u in all_levels[j]: i, v, w = self._succ[abs(u)] if i != j: raise AssertionError( (i, x, y)) u_ = self._pred.pop( (i, v, w)) if u != u_: raise AssertionError( (u, u_)) levels[j][u] = (v, w) # move level y up for u, (v, w) in levels[y].items(): i, _, _ = self._succ[u] if i != y: raise AssertionError((i, y)) r = (x, v, w) self._succ[u] = r if r in self._pred: raise AssertionError(r) self._pred[r] = u # move level x down # first x nodes independent of y done = set() for u, (v, w) in levels[x].items(): i, _, _ = self._succ[u] if i != x: raise AssertionError((i, x)) if not v: raise AssertionError(v) if not w: raise AssertionError(w) iv, v0, v1 = self._low_high(v) iw, w0, w1 = self._low_high(w) # dependeds on y ? if iv <= y or iw <= y: continue # independent of y r = (y, v, w) self._succ[u] = r if r in self._pred: raise AssertionError(r) self._pred[r] = u done.add(u) # x nodes dependent on y garbage = set() xfresh = set() for u, (v, w) in levels[x].items(): # for type checking match u: case int(): pass case _: raise AssertionError(u) if u in done: continue i, _, _ = self._succ[u] if i != x: raise AssertionError((i, x)) if not v: raise AssertionError(v) if not w: raise AssertionError(w) self.decref(v) self.decref(w) # possibly unused garbage.add(abs(v)) garbage.add(w) # calling cofactor can fail # because y moved iv, v0, v1 = self._swap_cofactor(v, y) iw, w0, w1 = self._swap_cofactor(w, y) # x node depends on y if not (y <= iv and y <= iw): raise AssertionError( (iv, iw, y)) if not (y == iv or y == iw): raise AssertionError( (iv, iw, y)) # complemented edge ? if v < 0 and y == iv: v0, v1 = -v0, -v1 p = self.find_or_add( y, v0, w0) q = self.find_or_add( y, v1, w1) if q < 0: raise AssertionError(q) if p == q: raise AssertionError( 'No elimination: ' 'node depends on both x and y') if self._succ[abs(p)][0] == y: xfresh.add(abs(p)) if self._succ[q][0] == y: xfresh.add(q) r = (x, p, q) self._succ[u] = r if r in self._pred: raise AssertionError( (u, r, levels, self._pred)) self._pred[r] = u self.incref(p) self.incref(q) # garbage collection could be interleaved # but only if there is # substantial loss of efficiency # swap x and y in `vars` vx = self.var_at_level(x) self.vars[vx] = y vy = self.var_at_level(y) self.vars[vy] = x # reset self._level_to_var[y] = vx self._level_to_var[x] = vy self._ite_table = dict() # count nodes self.collect_garbage(garbage) newsize = len(self._succ) # new levels newx = set() newy = set() for u in levels[x]: if u not in self._succ: continue i, _, _ = self._succ[u] if i == x: newy.add(u) elif i == y: newx.add(u) else: raise AssertionError( (u, i, x, y)) for u in xfresh: i, _, _ = self._succ[u] if i != y: raise AssertionError( (u, i, x, y)) newx.add(u) for u in levels[y]: if u not in self._succ: continue i, _, _ = self._succ[u] if i != x: raise AssertionError( (u, i, x, y)) newy.add(u) all_levels[x] = newy all_levels[y] = newx return ( oldsize, newsize) def _low_high( self, u: _Ref ) -> tuple[ _Level, _Ref, _Node]: """Return level, low, and high. If node `u` is a leaf, then `u` is returned as low and high. This method is similar to the method `succ`, but different. @return: (level, low, high) """ i, v, w = self._succ[abs(u)] if abs(u) == 1: return i, u, u if not v: raise AssertionError(v) if not w: raise AssertionError(w) return i, v, w def _swap_cofactor( self, u: _Ref, y: _Level ) -> tuple[ _Level, _Ref, _Ref]: """Return cofactor of node `u` wrt level `y`. If node `u` is above level `y`, that means it was at level `y` when the swap started. To account for this, `y` is returned as the node level. """ i, v, w = self._succ[abs(u)] if y < i: return (i, u, u) # restore index of y node that # moved up if not v: raise AssertionError(v) if not w: raise AssertionError(w) return (y, v, w) def count( self, u: _Ref, nvars: _Nat | None=None ) -> _Nat: n = nvars if abs(u) not in self: raise ValueError(u) # index those levels in # support separately levels = { self.level_of_var(var) for var in self.support(u)} k = len(levels) if n is None: n = k slack = n - k if slack < 0: raise ValueError(slack) map_level = dict() for new, old in enumerate(sorted(levels)): map_level[old] = new + slack old, _, _ = self._succ[1] map_level[old] = n map_level['all'] = n r = self._sat_len( u, map_level, d=dict()) i, _, _ = self._succ[abs(u)] i = map_level[i] n_models = r * 2**i return self._assert_int(n_models) @staticmethod def _assert_int( number: _ty.Any ) -> int: """Return `number` if an `int`. Raise `AssertionError` otherwise. """ match number: case int(): return number raise AssertionError( 'Expected `int` result, ' f'but: {number = }') def _sat_len( self, u: _Ref, map_level: dict[ _Level | _ty.Literal['all'], _Level], d: dict[ _Node, _Nat] ) -> _Nat: """Recurse to compute the number of models.""" # terminal ? if u == 1: return 1 if u == -1: return 0 i, v, w = self._succ[abs(u)] if not v: raise AssertionError(v) if not w: raise AssertionError(w) i = map_level[i] # memoized ? if abs(u) in d: n = d[abs(u)] # complement ? if u < 0: n = 2**(map_level['all'] - i) - n return self._assert_int(n) # non-terminal nv = self._sat_len(v, map_level, d) nw = self._sat_len(w, map_level, d) iv, _, _ = self._succ[abs(v)] iw, _, _ = self._succ[w] iv = map_level[iv] iw = map_level[iw] # sum n = self._assert_int( nv * 2**(iv - i - 1) + nw * 2**(iw - i - 1)) d[abs(u)] = n # complement ? if u < 0: n = 2**(map_level['all'] - i) - n return self._assert_int(n) def pick_iter( self, u: _Ref, care_vars: set[_VariableName] | None=None ) -> _abc.Iterable[ _Assignment]: # empty ? if not self._succ: return # non-empty if abs(u) not in self._succ: raise ValueError( f'{u} is not a reference to ' 'a BDD node in the BDD manager ' f'`self` ({self!r})') support = self.support(u) if care_vars is None: care_vars = support missing = { v for v in support if v not in care_vars} if missing: logger.warning( 'Missing bits: ' f'support - care_vars = {missing}') cube = dict() value = True cubes = self._sat_iter( u, cube, value) for cube in cubes: minterms = _enumerate_minterms( cube, care_vars) for m in minterms: yield m def _sat_iter( self, u: _Ref, cube: dict[ _Level, bool], value: bool ) -> _abc.Iterable[ _Assignment]: """Recurse to enumerate models.""" if u < 0: value = not value # terminal ? if abs(u) == 1: if value: cube = { self._level_to_var[i]: v for i, v in cube.items()} yield cube return # non-terminal i, v, w = self._succ[abs(u)] if not v: raise AssertionError(v) if not w: raise AssertionError(w) d0 = dict(cube) d0[i] = False d1 = dict(cube) d1[i] = True for x in self._sat_iter(v, d0, value): yield x for x in self._sat_iter(w, d1, value): yield x def assert_consistent( self ) -> None: """Raise `AssertionError` if not a valid BDD.""" for root in self.roots: if abs(root) not in self._succ: raise AssertionError(root) # inverses succ_keys = set(self._succ) succ_values = set(self._succ.values()) pred_keys = set(self._pred) pred_values = set(self._pred.values()) if succ_keys != pred_values: raise AssertionError( succ_keys.symmetric_difference( pred_values)) if pred_keys != succ_values: raise AssertionError( pred_keys.symmetric_difference( succ_values)) # uniqueness n = len(succ_keys) n_ = len(succ_values) if n != n_: raise AssertionError(n - n_) for u, (i, v, w) in self._succ.items(): if not isinstance(i, int): raise TypeError(i) # terminal ? if v is None: if w is not None: raise AssertionError(w) continue else: if abs(v) not in self._succ: raise AssertionError(v) if w is None: if v is not None: raise AssertionError(v) continue else: # "high" is regular edge if w < 0: raise AssertionError(w) if w not in self._succ: raise AssertionError(w) # var order should increase for x in (v, w): ix, _, _ = self._succ[abs(x)] if not (i < ix): raise AssertionError((u, i)) # `_pred` contains inverse of `_succ` if (i, v, w) not in self._pred: raise AssertionError((i, v, w)) if self._pred[(i, v, w)] != u: raise AssertionError(u) # reference count if u not in self._ref: raise AssertionError(u) if self._ref[u] < 0: raise AssertionError(self._ref[u]) @_try_to_reorder def add_expr( self, expr: _Formula ) -> _Ref: return _parser.add_expr(expr, self) def to_expr( self, u: _Ref ) -> _Formula: if u not in self: raise ValueError( f'{u} is not a reference to ' 'a BDD node in the BDD manager ' f'`self` ({self!r})') cache = dict() return self._to_expr(u, cache) def _to_expr( self, u: _Ref, cache: dict[int, str] ) -> _Formula: if u == 1: return 'TRUE' if u == -1: return 'FALSE' if u in cache: return cache[u] level, v, w = self._succ[abs(u)] if not v: raise AssertionError(v) if not w: raise AssertionError(w) var = self._level_to_var[level] p = self._to_expr(v, cache) q = self._to_expr(w, cache) # pure var ? if p == 'FALSE' and q == 'TRUE': expr = var else: expr = f'ite({var}, {q}, {p})' # complemented ? if u < 0: expr = f'(~ {expr})' cache[u] = expr return expr def apply( self, op: dd._abc.OperatorSymbol, u: _Ref, v: _Ref | None=None, w: _Ref | None=None ) -> _Ref: _utils.assert_operator_arity(op, v, w, 'bdd') if abs(u) not in self: raise ValueError(u) if v is not None and abs(v) not in self: raise ValueError(v) if w is not None and abs(w) not in self: raise ValueError(w) # unary if op in ('~', 'not', '!'): return -u # Implied by `assert_operator_arity()` above, # present here for type-checking. elif v is None: raise ValueError( '`v is None`') # binary elif op in ('or', r'\/', '|', '||'): return self.ite(u, 1, v) elif op in ('and', '/\\', '&', '&&'): return self.ite(u, v, -1) elif op in ('#', 'xor', '^'): return self.ite(u, -v, v) elif op in ('=>', '->', 'implies'): return self.ite(u, v, 1) elif op in ('<=>', '<->', 'equiv'): return self.ite(u, v, -v) elif op in ('diff', '-'): return self.ite(u, -v, -1) elif op in (r'\A', 'forall'): qvars = self.support(u) return self.quantify( v, qvars, forall=True) elif op in (r'\E', 'exists'): qvars = self.support(u) return self.quantify( v, qvars, forall=False) # Implied by `assert_operator_arity()` above, # present here for type-checking. elif w is None: raise ValueError( '`w is None`') # ternary elif op == 'ite': return self.ite(u, v, w) raise ValueError( f'unknown operator "{op}"') def _add_int( self, i: int ) -> _Ref: if i not in self: raise ValueError( f'{i = } is not a reference ' 'to a BDD node in the BDD manager ' f'`self` ({self!r})') return i @_try_to_reorder def cube( self, dvars: _Assignment | _abc.Iterable[ _VariableName] ) -> _Ref: if not isinstance(dvars, dict): dvars = { k: True for k in dvars} # `dvars` keys can be var names or levels r = self.true for var, val in dvars.items(): u = self.var(var) u = u if val else -u r = self.apply('and', u, r) return r def dump( self, filename: str, roots: dict[str, _Ref] | list[_Ref] | None=None, filetype: dd._abc.ImageFileType | dd._abc.PickleFileType | None=None, **kw ) -> None: if filetype is None: name = filename.lower() if name.endswith('.pdf'): filetype = 'pdf' elif name.endswith('.png'): filetype = 'png' elif name.endswith('.svg'): filetype = 'svg' elif name.endswith('.dot'): filetype = 'dot' elif name.endswith('.p'): filetype = 'pickle' else: raise ValueError( 'cannot infer file type ' 'from extension of file ' f'name "{filename}"') if filetype in _utils.DOT_FILE_TYPES: self._dump_figure( roots, filename, filetype, **kw) elif filetype == 'pickle': self._dump_bdd(roots, filename, **kw) else: raise ValueError( f'unknown file type "{filetype}"') def _dump_figure( self, roots: _abc.Iterable[_Ref] | None, filename: str, filetype: dd._abc.ImageFileType, **kw ) -> None: """Write BDDs to `filename` as figure.""" g = _to_dot(roots, self) g.dump(filename, filetype=filetype, **kw) def _dump_bdd( self, roots: dict[str, _Ref] | list[_Ref] | None, filename: str, **kw ) -> None: """Write BDDs to `filename` as pickle.""" if roots is None: nodes = self._succ roots = list() else: values = _utils.values_of(roots) nodes = self.descendants(values) succ = ( (k, self._succ[k]) for k in nodes) d = dict( vars=self.vars, succ=dict(succ), roots=roots) kw.setdefault('protocol', 2) with open(filename, 'wb') as f: pickle.dump(d, f, **kw) def load( self, filename: str, levels: _Yes=True ) -> ( dict[str, _Ref] | list[_Ref]): name = filename.lower() if not name.endswith('.p'): raise ValueError( f'Unknown file type of "{filename}"') umap, roots = self._load_pickle( filename, levels=levels) def map_node(u): v = umap[abs(u)] if u < 0: return - v else: return v return _utils.map_container( map_node, roots) def _load_pickle( self, filename: str, levels: _Yes=True ) -> tuple[ dict, dict[str, _Ref] | list[_Ref]]: with open(filename, 'rb') as f: d = pickle.load(f) var2level = d['vars'] succ = d['succ'] n = len(var2level) level_map = dict() # level_map[n] = len(self.vars) for var, i in var2level.items(): if not (0 <= i < n): raise AssertionError((i, n)) if var not in self.vars: logger.warning( f'variable "{var}" added') if levels: j = self.add_var(var, i) else: j = self.add_var(var) level_map[i] = j umap = dict() for u in succ: # already added ? if u in umap: continue # add self._load( u, succ, umap, level_map) return umap, d['roots'] def _load( self, u: _Ref, succ: dict, umap: dict, level_map: dict ) -> _Ref: """Recurse to load BDD `u` from `succ`.""" # terminal ? if abs(u) == 1: return u # memoized ? if u in umap: r = umap[abs(u)] if r <= 0: raise AssertionError(r) if u < 0: r = -r return r i, v, w = succ[abs(u)] j = level_map[i] p = self._load( v, succ, umap, level_map) q = self._load( w, succ, umap, level_map) r = self.find_or_add(j, p, q) if r <= 0: raise AssertionError(r) umap[abs(u)] = r if u < 0: r = -r return r def _dump_manager( self, filename: str, **kw ) -> None: """Write `BDD` to `filename` as pickle.""" d = dict( vars=self.vars, max_nodes=self.max_nodes, roots=self.roots, pred=self._pred, succ=self._succ, ref=self._ref, min_free=self._min_free) kw.setdefault('protocol', 2) with open(filename, 'wb') as f: pickle.dump(d, f, **kw) @classmethod def _load_manager( cls, filename: str ) -> 'BDD': """Load `BDD` from pickle file `filename`.""" with open(filename, 'rb') as f: d = pickle.load(f) bdd = cls(d['vars']) bdd.max_nodes = d['max_nodes'] bdd.roots = d['roots'] bdd._pred = d['pred'] bdd._succ = d['succ'] bdd._ref = d['ref'] bdd._min_free = d['min_free'] return bdd @property def false( self ) -> _Ref: return -1 @property def true( self ) -> _Ref: return 1 def _enumerate_minterms( cube: _Assignment, bits: _abc.Iterable[ _VariableName] ) -> _abc.Iterator[ _Assignment]: """Generator of complete assignments in `cube`. @param bits: enumerate over those absent from `cube` """ if cube is None: raise ValueError(cube) if bits is None: raise ValueError(bits) bits = set(bits).difference(cube) # fix order bits = list(bits) n = len(bits) for i in range(2**n): values = bin(i).lstrip('-0b').zfill(n) model = { k: bool(int(v)) for k, v in zip(bits, values)} model.update(cube) if len(model) < len(bits): raise AssertionError((model, bits)) if len(model) < len(cube): raise AssertionError((model, cube)) yield model def _assert_isomorphic_orders( old: _VariableLevels, new: _VariableLevels, support: set[_VariableName] ) -> None: """Raise `AssertionError` if not isomorphic. @param old, new: levels @param support: `old` and `new` compared after restriction to `support`. """ _assert_valid_ordering(old) _assert_valid_ordering(new) s = { k: v for k, v in old.items() if k in support} t = { k: v for k, v in new.items() if k in support} old = sorted(s, key=s.get) new = sorted(t, key=t.get) if old != new: raise AssertionError((old, new)) def _assert_valid_ordering( levels: _VariableLevels ) -> None: """Check that `levels` is well-formed. - bijection - contiguous levels """ # `levels` is a mapping from # each variable to a single level if not isinstance(levels, _abc.Mapping): raise TypeError(levels) # levels are contiguous integers ? n = len(levels) numbers = set(levels.values()) numbers_ = set(range(n)) if numbers != numbers_: raise AssertionError(n, numbers) def rename( u: _Ref, bdd: BDD, dvars: _Renaming ) -> _Ref: """Rename variables of node `u`. @param dvars: `dict` from variabe names to variable names """ if abs(u) not in bdd: raise ValueError( f'{u} (given as `u`) is not a reference to ' 'a BDD node in the given BDD manager ' f'`bdd` ({bdd!r})') # nothing to rename ? if not dvars: return u # map variable names to levels levels = bdd.vars dvars = { levels[var]: levels[dvars.get(var, var)] for var in bdd.vars} cache = dict() return _copy_bdd(u, dvars, bdd, bdd, cache) def _assert_valid_rename( u: _Ref, bdd: BDD, dvars: dict[ _Level, _Level] ) -> None: """Assert renaming to only adjacent variables. Raise `AssertionError` if renaming to non-adjacent variables. @param dvars: `dict` that maps var levels to var levels """ if not dvars: return # valid levels ? bdd.var_at_level(0) # pairwise disjoint ? _assert_no_overlap(dvars) def _all_adjacent( dvars: dict, bdd: BDD ) -> _Yes: """Return `True` if all levels are adjacent. The pairs of levels checked for being adjacent are the key-value pairs of the mapping `dvars`. """ for v, vp in dvars.items(): if not _adjacent(v, vp, bdd): return False return True def _adjacent( i: _Level, j: _Level, bdd: BDD ) -> _Yes: """Warn if levels `i` and `j` not adjacent.""" if abs(i - j) == 1: return True logger.warning(( 'level {i} ("{x}") not adjacent to ' 'level {j} ("{y}")').format( i=i, j=j, x=bdd.var_at_level(i), y=bdd.var_at_level(j))) return False def _assert_no_overlap( d: dict ) -> None: """Raise `AssertionError` if keys and values overlap.""" if any((k in d) for k in d.values()): raise AssertionError( f'keys and values overlap: {d}') def image( trans: _Ref, source: _Ref, rename: _Renaming | dict[_Level, _Level], qvars: _abc.Iterable[_VariableName] | _abc.Iterable[_Level], bdd: BDD, forall: _Yes=False ) -> _Ref: """Return set reachable from `source` under `trans`. @param trans: transition relation @param source: the transition must start in this set @param rename: maps primed variables in `trans` to unprimed variables in `trans`. Applied to the quantified conjunction of `trans` and `source`. @param qvars: variables to quantify @param forall: if `True`, then quantify `qvars` universally, else existentially. """ # map to levels qvars = bdd._map_to_level(set(qvars)) rename = { bdd.vars.get(k, k): bdd.vars.get(v, v) for k, v in rename.items()} # init cache = dict() rename_u = rename rename_v = None # no overlap and neighbors _assert_no_overlap(rename) if not _all_adjacent(rename, bdd): logger.warning( 'BDD.image: not all vars adjacent') # unpriming maps to qvars or # outside support of conjunction s = bdd.support(trans, as_levels=True) s.update(bdd.support(source, as_levels=True)) s.difference_update(qvars) s.intersection_update(rename.values()) if s: raise AssertionError(s) return _image( trans, source, rename_u, rename_v, qvars, bdd, forall, cache) def preimage( trans: _Ref, target: _Ref, rename: _Renaming | dict[_Level, _Level], qvars: _abc.Iterable[_VariableName] | _abc.Iterable[_Level], bdd: BDD, forall: _Yes=False ) -> _Ref: """Return set that can reach `target` under `trans`. Also known as the "relational product". Assumes that primed and unprimed variables are neighbors. Variables are identified by their levels. @param trans: transition relation @param target: the transition must end in this set @param rename: maps (unprimed) variables in `target` to (primed) variables in `trans` @param qvars: variables to quantify @param forall: if `True`, then quantify `qvars` universally, else existentially. """ # map to levels qvars = bdd._map_to_level(set(qvars)) rename = { bdd.vars.get(k, k): bdd.vars.get(v, v) for k, v in rename.items()} # init cache = dict() rename_u = None rename_v = rename # check _assert_valid_rename(target, bdd, rename) return _image( trans, target, rename_u, rename_v, qvars, bdd, forall, cache) def _image( u: _Ref, v: _Ref, umap: dict | None, vmap: dict | None, qvars: set[_Level], bdd: BDD, forall: _Yes, cache: dict[ tuple[_Ref, _Ref], _Ref] ) -> _Ref: """Recursive (pre)image computation. Renaming requires that in each pair the variables are adjacent. @param umap: renaming of variables in `u` that occurs after conjunction of `u` with `v` and quantification. @param vmap: renaming of variables in `v` that occurs before conjunction with `u`. """ # controlling values for conjunction ? if u == -1 or v == -1: return -1 if u == 1 and v == 1: return 1 # already computed ? t = (u, v) w = cache.get(t) if w is not None: return w # recurse (descend) iu, _, _ = bdd._succ[abs(u)] jv, _, _ = bdd._succ[abs(v)] if vmap is None: iv = jv else: iv = vmap.get(jv, jv) z = min(iu, iv) u0, u1 = bdd._top_cofactor(u, z) v0, v1 = bdd._top_cofactor(v, jv + z - iv) p = _image( u0, v0, umap, vmap, qvars, bdd, forall, cache) q = _image( u1, v1, umap, vmap, qvars, bdd, forall, cache) # quantified ? if z in qvars: if forall: r = bdd.ite(p, q, -1) # conjoin else: r = bdd.ite(p, 1, q) # disjoin else: if umap is None: m = z else: m = umap.get(z, z) g = bdd.find_or_add(m, -1, 1) r = bdd.ite(g, q, p) cache[t] = r return r def reorder( bdd: BDD, order: _VariableLevels | None=None ) -> None: """Apply Rudell's sifting algorithm to reduce `bdd` size. Reordering invokes the garbage collector, so be sure to `incref` nodes that should remain. @param order: if given, then swap vars to obtain this order. The dictionary `order` maps each variable name to a level. """ len_before = len(bdd) if order is None: _apply_sifting(bdd) else: _sort_to_order(bdd, order) len_after = len(bdd) logger.info( 'Reordering changed `BDD` manager size ' f'from {len_before} to {len_after} nodes.') def _apply_sifting( bdd: BDD ) -> None: """Apply Rudell's sifting algorithm.""" bdd.collect_garbage() n = len(bdd) # using `set` injects some randomness levels = bdd._levels() names = set(bdd.vars) for var in names: k = _reorder_var(bdd, var, levels) m = len(bdd) logger.info( f'{m} nodes for variable ' f'"{var}" at level {k}') if m > n: raise AssertionError( f'expected: m <= n, but {m = } > {n = }') logger.info(f'final variable order:\n{bdd.vars}') def _reorder_var( bdd: BDD, var: _VariableName, levels: dict[ _Level, set[_Ref]] ) -> _Nat: """Reorder by sifting a variable `var`.""" if var not in bdd.vars: raise ValueError((var, bdd.vars)) m = len(bdd) n = len(bdd.vars) - 1 if n < 0: raise AssertionError(n) start = 0 end = n level = bdd.level_of_var(var) # closer to bottom ? if (2 * level) >= n: start, end = end, start _shift(bdd, level, start, levels) sizes = _shift(bdd, start, end, levels) k = min(sizes, key=sizes.get) _shift(bdd, end, k, levels) m_ = len(bdd) if sizes[k] != m_: raise AssertionError((sizes[k], m_)) if m_ > m: raise AssertionError((m_, m)) return k def _shift( bdd: BDD, start: _Level, end: _Level, levels: dict[ _Level, set[_Ref]] ) -> dict[ _Level, _Level]: r"""Shift level `start` to become `end`, by swapping. ```tla ASSUMPTION LET n_vars == len(bdd.vars) level_range == 0..(n_vars - 1) IN /\ start \in level_range /\ end \in level_range ``` """ m = len(bdd.vars) if not (0 <= start < m): raise AssertionError((start, m)) if not (0 <= end < m): raise AssertionError((end, m)) sizes = dict() d = 1 if start < end else -1 for i in range(start, end, d): j = i + d oldn, n = bdd.swap(i, j, levels) sizes[i] = oldn sizes[j] = n return sizes def _sort_to_order( bdd: BDD, order: _VariableLevels ) -> None: """Swap variables to obtain `order`.""" # TODO: use min number of swaps if len(bdd.vars) != len(order): raise ValueError( 'The number of BDD variables: ' f'{len(bdd.vars) = } is not equal to: ' f'{len(order) = }') m = 0 levels = bdd._levels() n = len(order) for k in range(n): for i in range(n - 1): for root in bdd.roots: if root not in bdd: raise ValueError( f'{root} in `bdd.roots` is not ' 'a reference to a BDD node in ' 'the given BDD manager `bdd` ' f'({bdd!r})') x = bdd.var_at_level(i) y = bdd.var_at_level(i + 1) p = order[x] q = order[y] if p > q: bdd.swap(i, i + 1, levels) m += 1 logger.debug( f'swap: {p} with {q}, {i}') if logger.getEffectiveLevel() < logging.DEBUG: bdd.assert_consistent() logger.info(f'total swaps: {m}') def reorder_to_pairs( bdd: BDD, pairs: _Renaming ) -> None: """Reorder variables to make adjacent the given pairs. @param pairs: has variable names as keys and values """ m = 0 levels = bdd._levels() for x, y in pairs.items(): jx = bdd.level_of_var(x) jy = bdd.level_of_var(y) k = abs(jx - jy) if k <= 0: raise AssertionError((jx, jy)) # already adjacent ? if k == 1: continue # shift x next to y if jx > jy: jx, jy = jy, jx _shift(bdd, start=jx, end=jy - 1, levels=levels) m += k logger.debug(f'shift by {k}') logger.info(f'total swaps: {m}') def copy_bdd( u: _Ref, from_bdd: BDD, to_bdd: BDD ) -> _Ref: """Copy BDD of node `u` `from_bdd` `to_bdd`. @param u: node in `from_bdd` """ if from_bdd is to_bdd: logger.warning( 'copying node to same BDD manager') return u level_map = { from_bdd.level_of_var(var): to_bdd.level_of_var(var) for var in from_bdd.vars if var in to_bdd.vars} r = _copy_bdd( u, level_map, from_bdd, to_bdd, cache=dict()) return r def _copy_bdd( u: _Ref, level_map: dict[_Level, _Level], old_bdd: BDD, bdd: BDD, cache: dict[_Node, _Ref] ) -> _Ref: """Recurse to copy nodes from `old_bdd` to `bdd`. @param u: node in `old_bdd` @param level_map: maps old to new levels """ # terminal ? if abs(u) == 1: return u # non-terminal # memoized ? r = cache.get(abs(u)) if r is not None: if r <= 0: raise AssertionError(r) # complement ? if u < 0: r = -r return r # recurse jold, v, w = old_bdd._succ[abs(u)] if not v: raise AssertionError(v) if not w: raise AssertionError(w) p = _copy_bdd( v, level_map, old_bdd, bdd, cache) q = _copy_bdd( w, level_map, old_bdd, bdd, cache) if p * v <= 0: raise AssertionError((p, v)) if q <= 0: raise AssertionError(q) # map this level jnew = level_map[jold] g = bdd.find_or_add(jnew, -1, 1) r = bdd.ite(g, q, p) # memoize if r <= 0: raise AssertionError(r) cache[abs(u)] = r # complement ? if u < 0: r = -r return r def _flip( r: _Ref, u: _Ref ) -> _Ref: """Flip `r` if `u` is negated, else identity.""" return -r if u < 0 else r def to_nx( bdd: BDD, roots: set[_Ref] ) -> '_utils.MultiDiGraph': """Convert node references in `roots` to graph. The resulting graph has: - nodes labeled with: - `level`: `int` from 0 to `len(bdd)` - edges labeled with: - `value`: `False` for low/"else", `True` for high/"then" - `complement`: `True` if target node is negated @param roots: iterable of edges, each a signed `int` """ _nx = _utils.import_module('networkx') g = _nx.MultiDiGraph() for root in roots: if abs(root) not in bdd: raise ValueError(root) Q = {root} while Q: u = Q.pop() u = abs(u) i, v, w = bdd._succ[u] if u <= 0: raise AssertionError(u) g.add_node(u, level=i) # terminal ? if v is None or w is None: if v is not None: raise AssertionError(v) if w is not None: raise AssertionError(w) continue # non-terminal r = (v < 0) v = abs(v) w = abs(w) if v not in g: Q.add(v) if w not in g: Q.add(w) if v <= 0: raise AssertionError(v) if w <= 0: raise AssertionError(w) g.add_edge( u, v, value=False, complement=r) g.add_edge( u, w, value=True, complement=False) return g def _to_dot( roots: _abc.Iterable[_Ref] | None, bdd: BDD ) -> _utils.DotGraph: """Convert `BDD` to DOT graph. Nodes are ordered by variable levels in support. Edges to low successors are dashed. Complemented edges are labeled with "-1". Nodes not reachable from `roots` are ignored, unless `roots is None`. The roots are plotted as external references, with complemented edges where applicable. """ # all nodes ? if roots is None: nodes = bdd._succ roots = list() else: nodes = bdd.descendants(roots) # show only levels in aggregate support levels = { bdd._succ[abs(u)][0] for u in nodes} if bdd._succ[1][0] not in levels: raise AssertionError( 'level of node 1 is missing from computed ' 'set of BDD nodes reachable from `roots`') g = _utils.DotGraph( graph_type='digraph') skeleton = list() subgraphs = dict() # layer for external BDD references layers = [-1] + sorted(levels) # add nodes for BDD levels for i in layers: h = _utils.DotGraph( rank='same') g.subgraphs.append(h) subgraphs[i] = h # add phantom node u = f'"L{i}"' skeleton.append(u) if i == -1: # layer for external BDD references label = 'ref' else: # BDD level label = str(i) h.add_node( u, label=label, shape='none') # auxiliary edges for ranking for i, u in enumerate(skeleton[:-1]): v = skeleton[i + 1] g.add_edge( u, v, style='invis') # add nodes idx2var = { k: v for v, k in bdd.vars.items()} # BDD nodes def f(x): return str(abs(x)) for u in nodes: i, v, w = bdd._succ[abs(u)] # terminal ? if v is None: var = str(bool(abs(u))) else: var = idx2var[i] su = f(u) label = f'{var}-{su}' # add node to subgraph for level i h = subgraphs[i] h.add_node( su, label=label) # add edges if v is None: continue sv = f(v) sw = f(w) kw = dict(style='dashed') if v < 0: kw['taillabel'] = '-1' g.add_edge( su, sv, **kw) g.add_edge( su, sw, style='solid') # external references to BDD nodes for u in roots: i, _, _ = bdd._succ[abs(u)] su = f'"ref{u}"' label = f'@{u}' # add node to subgraph for level -1 h = subgraphs[-1] h.add_node( su, label=label) # add edge from external reference to BDD node if u is None: raise ValueError(f'{u} in `roots`') sv = str(abs(u)) kw = dict(style='dashed') if u < 0: kw.update(taillabel='-1') g.add_edge( su, sv, **kw) return g ================================================ FILE: dd/buddy.pyx ================================================ # cython: profile=True """Cython interface to BuDDy. Reference ========= Jorn Lind-Nielsen "BuDDy: Binary Decision Diagram package" IT-University of Copenhagen (ITU) v2.4, 2002 """ # Copyright 2015 by California Institute of Technology # All rights reserved. Licensed under BSD-3. # import collections.abc as _abc import logging import pprint import sys import typing as _ty from cpython cimport bool as _py_bool from cpython.mem cimport PyMem_Malloc, PyMem_Free import cython from libc.stdio cimport fdopen, fopen import dd._abc as _dd_abc cimport dd.buddy_ as buddy ctypedef cython.int _c_int _Yes: _ty.TypeAlias = _py_bool _Cardinality: _ty.TypeAlias = _dd_abc.Cardinality _VariableName: _ty.TypeAlias = _dd_abc.VariableName _Level: _ty.TypeAlias = _dd_abc.Level _Renaming: _ty.TypeAlias = _dd_abc.Renaming _OperatorSymbol: _ty.TypeAlias = _ty.Literal[ '!', 'not', '&', 'and', '|', 'or', '#', '^', 'xor'] _OPERATOR_SYMBOLS: _ty.Final = set(_ty.get_args( _OperatorSymbol)) APPLY_MAP = { 'and': 0, 'xor': 1, 'or': 2, 'nand': 3, 'nor': 4, 'imp': 5, 'biimp': 6, 'diff': 7, 'less': 8, 'invimp': 9} BDD_REORDER_NONE = 0 BDD_REORDER_WIN2 = 1 BDD_REORDER_WIN2ITE = 2 # "ite" = iteratively BDD_REORDER_SIFT = 3 BDD_REORDER_SIFTITE = 4 BDD_REORDER_WIN3 = 5 BDD_REORDER_WIN3ITE = 6 BDD_REORDER_RANDOM = 7 BDD_REORDER_FREE = 0 BDD_REORDER_FIXED = 1 logger = logging.getLogger(__name__) cdef class BDD: """Wrapper of BuDDy. Interface similar to `dd.bdd.BDD`. There is only a single global shared BDD, so use only one instance. """ cdef public object var_to_index def __cinit__( self ) -> None: self.var_to_index = dict() if buddy.bdd_isrunning(): return n_nodes = 10**2 cache = 10**4 n_vars = 150 buddy.bdd_init(n_nodes, cache) buddy.bdd_setvarnum(n_vars) buddy.bdd_setcacheratio(64) buddy.bdd_autoreorder(BDD_REORDER_SIFT) buddy.bdd_reorder_verbose(1) def __dealloc__( self ) -> None: buddy.bdd_done() def __str__( self ) -> str: n = buddy.bdd_getnodenum() n_alloc = buddy.bdd_getallocnum() n_vars = buddy.bdd_varnum() s = ( 'Binary decision diagram (BuDDy wrapper) with:\n' f'\t {n} live nodes now\n' f'\t {n_alloc} total nodes allocated\n' f'\t {n_vars} BDD variables\n') return s def __len__( self ) -> _Cardinality: return buddy.bdd_getnodenum() cdef incref( self, u: _c_int): buddy.bdd_addref(u) cdef decref( self, u: _c_int): buddy.bdd_delref(u) property false: def __get__( self ) -> Function: return self._bool(False) property true: def __get__( self ) -> Function: return self._bool(True) cdef Function _bool( self, b: _py_bool): if b: r = buddy.bdd_true() else: r = buddy.bdd_false() return Function(r) cpdef int add_var( self, var: _VariableName): """Return index for variable `var`.""" j = self.var_to_index.get(var) if j is not None: return j j = len(self.var_to_index) self.var_to_index[var] = j # new block for reordering buddy.bdd_intaddvarblock(j, j, 0) return j cpdef Function var( self, var: _VariableName): """Return BDD for variable `var`.""" if var not in self.var_to_index: raise ValueError( f'"{var}" is not a variable (key) in ' f'{self.var_to_index = }') j = self.var_to_index[var] r = buddy.bdd_ithvar(j) if r == self.false.node: raise RuntimeError('failed') buddy.bdd_intaddvarblock(j, j, 0) return Function(r) cpdef int level_of_var( self, var: _VariableName): """Return level of variable `var`.""" if var not in self.var_to_index: raise ValueError( f'undeclared variable "{var}", ' 'known variables are:\n' f'{self.var_to_index}') j = self.var_to_index[var] level = buddy.bdd_var2level(j) return level cpdef str var_at_level( self, level: _Level): """Return variable at `level`.""" index = buddy.bdd_level2var(level) # unknown variable error ? if index == buddy.BDD_VAR: levels = { var: self.level_of_var(var) for var in self.var_to_index} raise ValueError( f'no variable has level: {level}, ' 'the current levels of all variables ' f'are: {levels}') index_to_var = { v: k for k, v in self.var_to_index.items()} var = index_to_var[index] return var cpdef Function apply( self, op: _OperatorSymbol, u: Function, v: Function | None=None): """Return as `Function` the result of applying `op`.""" if op not in _OPERATOR_SYMBOLS: raise ValueError( f'unknown operator: "{op}"') # unary if op in ('!', 'not'): if v is not None: raise ValueError((op, u, v)) r = buddy.bdd_not(u.node) elif v is None: raise ValueError((op, u, v)) # binary if op in ('&', 'and'): r = buddy.bdd_and(u.node, v.node) elif op in ('|', 'or'): r = buddy.bdd_or(u.node, v.node) elif op in ('#', '^', 'xor'): r = buddy.bdd_xor(u.node, v.node) return Function(r) cpdef Function quantify( self, u: Function, qvars: _abc.Iterable[ _VariableName], forall: _Yes=False): cube = self.cube(qvars) if forall: r = buddy.bdd_forall(u, cube) else: r = buddy.bdd_exist(u, cube) return Function(r) cpdef Function cube( self, dvars: _abc.Iterable[ _VariableName]): """Return a positive unate cube for `dvars`.""" n = len(dvars) cdef int *x x = PyMem_Malloc(n * sizeof(int)) for i, var in enumerate(dvars): j = self.add_var(var) x[i] = j try: r = buddy.bdd_makeset(x, n) finally: PyMem_Free(x) return Function(r) cpdef assert_consistent( self): raise NotImplementedError('TODO') cpdef Function and_abstract( u: Function, v: Function, qvars: _abc.Iterable[ _VariableName], bdd: BDD): """Return `? qvars. u & v`.""" cube = bdd.cube(qvars) op = APPLY_MAP['and'] r = buddy.bdd_appex(u.node, v.node, op, cube.node) return Function(r) cpdef Function or_abstract( u: Function, v: Function, qvars: _abc.Iterable[ _VariableName], bdd: BDD): """Return `! qvars. u | v`.""" cube = bdd.cube(qvars) op = APPLY_MAP['or'] r = buddy.bdd_appall(u.node, v.node, op, cube.node) return Function(r) def rename( u: Function, bdd: BDD, dvars: _Renaming ) -> Function: n = len(dvars) cdef int *oldvars cdef int *newvars oldvars = PyMem_Malloc(n * sizeof(int)) newvars = PyMem_Malloc(n * sizeof(int)) for i, (a, b) in enumerate(dvars.items()): ja = bdd.add_var(a) jb = bdd.add_var(b) oldvars[i] = ja newvars[i] = jb cdef buddy.bddPair *pair = buddy.bdd_newpair() try: buddy.bdd_setpairs(pair, oldvars, newvars, n) r = buddy.bdd_replace(u.node, pair) finally: buddy.bdd_freepair(pair) PyMem_Free(oldvars) PyMem_Free(newvars) return Function(r) cdef class Function: """Wrapper for nodes of `BDD`. Takes care of reference counting, using the `weakref`s. Use as: ```cython bdd = BDD() u = bdd_true() f = Function(u) h = g | ~ f ``` """ __weakref__: object cdef public int node def __cinit__( self, node: _c_int ) -> None: self.node = node buddy.bdd_addref(node) def __dealloc__( self ) -> None: buddy.bdd_delref(self.node) self.node = -1 def __str__( self ) -> str: n = len(self) return f'Function({self.node}, {n})' def __len__( self ) -> _Cardinality: return buddy.bdd_nodecount(self.node) def __eq__( self, other: Function | None ) -> _Yes: if other is None: return False other_: Function = other return self.node == other_.node def __ne__( self, other: Function | None ) -> _Yes: if other is None: return True other_: Function = other return self.node != other_.node def __invert__( self ) -> Function: r = buddy.bdd_not(self.node) return Function(r) def __and__( self, other: Function ) -> Function: r = buddy.bdd_and(self.node, other.node) return Function(r) def __or__( self, other: Function ) -> Function: r = buddy.bdd_or(self.node, other.node) return Function(r) def __xor__( self, other: Function ) -> Function: r = buddy.bdd_xor(self.node, other.node) return Function(r) ================================================ FILE: dd/buddy_.pxd ================================================ # cython: profile=True """Cython extern declarations from BuDDy. Reference ========= Jorn Lind-Nielsen "BuDDy: Binary Decision Diagram package" IT-University of Copenhagen (ITU) v2.4, 2002 """ # Copyright 2015 by California Institute of Technology # All rights reserved. Licensed under BSD-3. # from libc.stdio cimport FILE cdef extern from 'bdd.h': int BDD_VAR # BDD ctypedef int BDD # renaming pair struct s_bddPair: pass ctypedef s_bddPair bddPair int bdd_init(int BDDsize, int cachesize) int bdd_isrunning() void bdd_done() int bdd_setacheratio(int r) # variable creation BDD bdd_ithvar(int var) BDD bdd_nithvar(int var) int bdd_var2level(int var) int bdd_level2var(int level) int bdd_setvarnum(int num) int bdd_extvarnum(int num) int bdd_varnum() # variable manipulation int bdd_var(BDD r) BDD bdd_makeset(int *varset, int varnum) int bdd_scanset(BDD a, int **varset, int *varnum) BDD bdd_ibuildcube(int value, int width, int *var) BDD bdd_buildcube(int value, int width, BDD *var) # BDD elements BDD bdd_true() BDD bdd_false() BDD bdd_low(BDD r) BDD bdd_high(BDD r) BDD bdd_support(BDD r) BDD bdd_satone(BDD r) # cube BDD bdd_fullsatone(BDD r) # minterm double bdd_satcount(BDD r) int bdd_nodecount(BDD r) # refs BDD bdd_addref(BDD r) BDD bdd_delref(BDD r) void bdd_gbc() # basic Boolean operators BDD bdd_ite(BDD u, BDD v, BDD w) BDD bdd_apply(BDD u, BDD w, int op) BDD bdd_not(BDD u) BDD bdd_and(BDD u, BDD v) BDD bdd_or(BDD u, BDD v) BDD bdd_xor(BDD u, BDD v) BDD bdd_imp(BDD u, BDD v) BDD bdd_biimp(BDD u, BDD v) # composition operators BDD bdd_restrict(BDD r, BDD var) BDD bdd_constrain(BDD f, BDD c) BDD bdd_compose(BDD f, BDD g, BDD v) BDD bdd_simplify(BDD f, BDD d) # quantification BDD bdd_exist(BDD r, BDD var) BDD bdd_forall(BDD r, BDD var) BDD bdd_appex(BDD u, BDD v, int op, BDD var) BDD bdd_appall(BDD u, BDD v, int op, BDD var) # renaming BDD bdd_replace(BDD r, bddPair *pair) bddPair * bdd_newpair() void bdd_freepair(bddPair *p) int bdd_setpair(bddPair *pair, int oldvar, int newvar) int bdd_setpairs( bddPair *pair, int *oldvar, int *newvar, int size) void bdd_resetpair(bddPair *pair) void bdd_freepair(bddPair *p) # manager config int bdd_setmaxBDDnum(int size) int bdd_setmaxincrease(int size) int bdd_setminfreeBDDs(int mf) int bdd_getnodenum() int bdd_getallocnum() # both unused and active # reordering int bdd_addvarblock(BDD b, int fixed) int bdd_intaddvarblock(int first, int last, int fixed) void bdd_varblockall() void bdd_reorder(int method) int bdd_autoreorder(int method) int bdd_autoreorder_times(int method, int num) void bdd_enable_reorder() void bdd_disable_reorder() int bdd_reorder_gain() void bdd_setcacheratio(int r) # I/O int bdd_save(FILE *ofile, BDD r) int bdd_load(FILE *ifile, BDD r) # info int bdd_reorder_verbose(int value) void bdd_printorder() void bdd_fprintorder(FILE *ofile) # void bdd_stats(bddStat *stat) # void bdd_cachestats(bddCacheStat *s) void bdd_fprintstat(FILE *f) void bdd_printstat() ================================================ FILE: dd/c_sylvan.pxd ================================================ """Cython extern declarations from Sylvan. Reference ========= Tom van Dijk, Alfons Laarman, Jaco van de Pol "Multi-Core BDD Operations for Symbolic Reachability" PDMC 2012 """ # Copyright 2016 by California Institute of Technology # All rights reserved. Licensed under BSD-3. # cimport libc.stdint as stdint from libcpp cimport bool cdef extern from 'lace.h': """ #define LACE_ME_WRAP 0); LACE_ME (0 """ void LACE_ME_WRAP() ctypedef struct WorkerP ctypedef struct Task cdef extern from 'sylvan.h': ctypedef stdint.uint64_t BDD ctypedef stdint.uint64_t BDDSET ctypedef stdint.uint32_t BDDVAR ctypedef stdint.uint64_t BDDMAP # stdint.uint64_t sylvan_complement stdint.uint64_t sylvan_false stdint.uint64_t sylvan_true BDD sylvan_invalid # int gc_enabled # should not be `static` # ctypedef void (*lace_startup_cb)( WorkerP*, Task*, void*) # node elements BDD sylvan_ithvar(BDDVAR var) BDD sylvan_nithvar(BDD var) BDDVAR sylvan_var(BDD bdd) BDD sylvan_low(BDD bdd) BDD sylvan_high(BDD bdd) bool sylvan_isconst(BDD bdd) bool sylvan_isnode(BDD bdd) size_t sylvan_nodecount(BDD a) size_t sylvan_count_refs() # main Boolean operators BDD sylvan_not(BDD a) BDD sylvan_and(BDD a, BDD b) BDD sylvan_xor(BDD a, BDD b) BDD sylvan_ite(BDD a, BDD b, BDD c) # derived operators BDD sylvan_equiv(BDD a, BDD b) BDD sylvan_or(BDD a, BDD b) BDD sylvan_imp(BDD a, BDD b) BDD sylvan_biimp(BDD a, BDD b) BDD sylvan_diff(BDD a, BDD b) # compose BDD sylvan_support(BDD bdd) BDD sylvan_constrain(BDD f, BDD c) BDD sylvan_restrict(BDD f, BDD c) BDD sylvan_compose(BDD f, BDDMAP m) BDDMAP sylvan_map_empty() BDDMAP sylvan_map_add( BDDMAP map, BDDVAR key, BDD value) # enumeration double sylvan_satcount( BDD bdd, BDDSET variables) BDD sylvan_pick_cube(BDD bdd) double sylvan_pathcount(BDD bdd) # refs BDD sylvan_ref(BDD a) void sylvan_deref(BDD a) # logistics void lace_exit() void lace_init(int n_workers, size_t dqsize) void lace_startup( size_t stacksize, lace_startup_cb cb, void* arg) void sylvan_init_package( size_t initial_tablesize, size_t max_tablesize, size_t initial_cachesize, size_t max_cachesize) void sylvan_init_bdd(int granularity) void sylvan_quit() # quantification BDD sylvan_exists(BDD a, BDD qvars) BDD sylvan_forall(BDD a, BDD qvars) BDD sylvan_and_exists( BDD a, BDD b, BDD qvars) BDD sylvan_relprev(BDD a, BDD b, BDD qvars) BDD sylvan_relnext(BDD a, BDD b, BDD qvars) BDD sylvan_closure(BDD a) # TODO: `stats.h` ================================================ FILE: dd/cudd.pyx ================================================ """Cython interface to CUDD. Variable `__version__` equals CUDD's version string. Reference ========= Fabio Somenzi "CUDD: CU Decision Diagram Package" University of Colorado at Boulder v2.5.1, 2015 """ # Copyright 2015 by California Institute of Technology # All rights reserved. Licensed under BSD-3. # import collections.abc as _abc import logging import pickle import pprint import sys import textwrap as _tw import time import typing as _ty import warnings from cpython cimport bool as python_bool from cpython.mem cimport PyMem_Malloc, PyMem_Free cimport libc.stdint as stdint from libc.stdio cimport FILE, fdopen, fopen, fclose from libcpp cimport bool import dd._abc as _dd_abc from dd import _copy from dd import _parser from dd import _utils from dd import autoref from dd import bdd as _bdd _Yes: _ty.TypeAlias = python_bool _Nat: _ty.TypeAlias = _dd_abc.Nat _Cardinality: _ty.TypeAlias = _dd_abc.Cardinality _NumberOfBytes: _ty.TypeAlias = _dd_abc.NumberOfBytes _VariableName: _ty.TypeAlias = _dd_abc.VariableName _Level: _ty.TypeAlias = _dd_abc.Level _VariableLevels: _ty.TypeAlias = _dd_abc.VariableLevels _Assignment: _ty.TypeAlias = _dd_abc.Assignment _Renaming: _ty.TypeAlias = _dd_abc.Renaming _Formula: _ty.TypeAlias = _dd_abc.Formula _BDDFileType: _ty.TypeAlias = ( _dd_abc.BDDFileType | _ty.Literal['dddmp']) cdef extern from 'mtr.h': struct MtrNode_: pass ctypedef MtrNode_ MtrNode cdef MTR_DEFAULT = 0 cdef MTR_FIXED = 4 cdef extern from 'cuddInt.h': char* CUDD_VERSION int CUDD_CONST_INDEX # subtable (for a level) struct DdSubtable: unsigned int slots unsigned int keys # manager struct DdManager: DdSubtable *subtables unsigned int keys unsigned int dead double cachecollisions double cacheinserts double cachedeletions DdNode *cuddUniqueInter( DdManager *unique, int index, DdNode *T, DdNode *E) cdef extern from 'cudd.h': # node ctypedef unsigned int DdHalfWord struct DdNode: DdHalfWord index DdHalfWord ref ctypedef DdNode DdNode ctypedef DdManager DdManager DdManager *Cudd_Init( unsigned int numVars, unsigned int numVarsZ, unsigned int numSlots, unsigned int cacheSize, size_t maxMemory) struct DdGen ctypedef enum Cudd_ReorderingType: pass # node elements DdNode *Cudd_bddNewVar( DdManager *dd) DdNode *Cudd_bddNewVarAtLevel( DdManager *dd, int level) DdNode *Cudd_bddIthVar( DdManager *dd, int index) DdNode *Cudd_ReadLogicZero( DdManager *dd) DdNode *Cudd_ReadOne( DdManager *dd) DdNode *Cudd_Regular( DdNode *u) bool Cudd_IsConstant( DdNode *u) unsigned int Cudd_NodeReadIndex( DdNode *u) DdNode *Cudd_T( DdNode *u) DdNode *Cudd_E( DdNode *u) bool Cudd_IsComplement( DdNode *u) int Cudd_DagSize( DdNode *node) int Cudd_SharingSize( DdNode **nodeArray, int n) # basic Boolean operators DdNode *Cudd_Not( DdNode *dd) DdNode *Cudd_bddIte( DdManager *dd, DdNode *f, DdNode *g, DdNode *h) DdNode *Cudd_bddAnd( DdManager *dd, DdNode *f, DdNode *g) DdNode *Cudd_bddOr( DdManager *dd, DdNode *f, DdNode *g) DdNode *Cudd_bddXor( DdManager *dd, DdNode *f, DdNode *g) DdNode *Cudd_bddXnor( DdManager *dd, DdNode *f, DdNode *g) int Cudd_bddLeq( DdManager *dd, DdNode *f, DdNode *g) DdNode *Cudd_Support( DdManager *dd, DdNode *f) DdNode *Cudd_bddComputeCube( DdManager *dd, DdNode **vars, int *phase, int n) DdNode *Cudd_CubeArrayToBdd( DdManager *dd, int *array) int Cudd_BddToCubeArray( DdManager *dd, DdNode *cube, int *array) int Cudd_PrintMinterm( DdManager *dd, DdNode *f) DdNode *Cudd_Cofactor( DdManager *dd, DdNode *f, DdNode *g) DdNode *Cudd_bddCompose( DdManager *dd, DdNode *f, DdNode *g, int v) DdNode *Cudd_bddVectorCompose( DdManager *dd, DdNode *f, DdNode **vector) DdNode *Cudd_bddRestrict( DdManager *dd, DdNode *f, DdNode *c) # cubes DdGen *Cudd_FirstCube( DdManager *dd, DdNode *f, int **cube, double *value) int Cudd_NextCube( DdGen *gen, int **cube, double *value) int Cudd_IsGenEmpty( DdGen *gen) int Cudd_GenFree( DdGen *gen) double Cudd_CountMinterm( DdManager *dd, DdNode *f, int nvars) # refs void Cudd_Ref( DdNode *n) void Cudd_RecursiveDeref( DdManager *table, DdNode *n) void Cudd_Deref( DdNode *n) # checks int Cudd_CheckZeroRef( DdManager *manager) int Cudd_DebugCheck( DdManager *table) void Cudd_Quit( DdManager *unique) DdNode *Cudd_bddTransfer( DdManager *ddSource, DdManager *ddDestination, DdNode *f) # info int Cudd_PrintInfo( DdManager *dd, FILE *fp) int Cudd_ReadSize( DdManager *dd) long Cudd_ReadNodeCount( DdManager *dd) long Cudd_ReadPeakNodeCount( DdManager *dd) int Cudd_ReadPeakLiveNodeCount( DdManager *dd) size_t Cudd_ReadMemoryInUse( DdManager *dd) unsigned int Cudd_ReadSlots( DdManager *dd) double Cudd_ReadUsedSlots( DdManager *dd) double Cudd_ExpectedUsedSlots( DdManager *dd) unsigned int Cudd_ReadCacheSlots( DdManager *dd) double Cudd_ReadCacheUsedSlots( DdManager *dd) double Cudd_ReadCacheLookUps( DdManager *dd) double Cudd_ReadCacheHits( DdManager *dd) # reordering int Cudd_ReduceHeap( DdManager *table, Cudd_ReorderingType heuristic, int minsize) int Cudd_ShuffleHeap( DdManager *table, int *permutation) void Cudd_AutodynEnable( DdManager *unique, Cudd_ReorderingType method) void Cudd_AutodynDisable( DdManager *unique) int Cudd_ReorderingStatus( DdManager *unique, Cudd_ReorderingType *method) unsigned int Cudd_ReadReorderings( DdManager *dd) long Cudd_ReadReorderingTime( DdManager *dd) int Cudd_ReadPerm( DdManager *dd, int index) int Cudd_ReadInvPerm( DdManager *dd, int level) void Cudd_SetSiftMaxSwap( DdManager *dd, int sms) int Cudd_ReadSiftMaxSwap( DdManager *dd) void Cudd_SetSiftMaxVar( DdManager *dd, int smv) int Cudd_ReadSiftMaxVar( DdManager *dd) # variable grouping extern MtrNode *Cudd_MakeTreeNode( DdManager *dd, unsigned int low, unsigned int size, unsigned int type) extern MtrNode *Cudd_ReadTree( DdManager *dd) extern void Cudd_SetTree( DdManager *dd, MtrNode *tree) extern void Cudd_FreeTree( DdManager *dd) # manager config size_t Cudd_ReadMaxMemory( DdManager *dd) size_t Cudd_SetMaxMemory( DdManager *dd, size_t maxMemory) unsigned int Cudd_ReadMaxCacheHard( DdManager *dd) unsigned int Cudd_ReadMaxCache( DdManager *dd) void Cudd_SetMaxCacheHard( DdManager *dd, unsigned int mc) double Cudd_ReadMaxGrowth( DdManager *dd) void Cudd_SetMaxGrowth( DdManager *dd, double mg) unsigned int Cudd_ReadMinHit( DdManager *dd) void Cudd_SetMinHit( DdManager *dd, unsigned int hr) void Cudd_EnableGarbageCollection( DdManager *dd) void Cudd_DisableGarbageCollection( DdManager *dd) int Cudd_GarbageCollectionEnabled( DdManager * dd) unsigned int Cudd_ReadLooseUpTo( DdManager *dd) void Cudd_SetLooseUpTo( DdManager *dd, unsigned int lut) # quantification DdNode *Cudd_bddExistAbstract( DdManager *manager, DdNode *f, DdNode *cube) DdNode *Cudd_bddUnivAbstract( DdManager *manager, DdNode *f, DdNode *cube) DdNode *Cudd_bddAndAbstract( DdManager *manager, DdNode *f, DdNode *g, DdNode *cube) DdNode *Cudd_bddSwapVariables( DdManager *dd, DdNode *f, DdNode **x, DdNode **y, int n) cdef extern from '_cudd_addendum.c': DdNode *Cudd_bddTransferRename( DdManager *ddSource, DdManager *ddDestination, DdNode *f, int *renaming) ctypedef DdNode *DdRef cdef CUDD_UNIQUE_SLOTS = 2**8 cdef CUDD_CACHE_SLOTS = 2**18 cdef CUDD_REORDER_GROUP_SIFT = 14 cdef CUDD_OUT_OF_MEM = -1 cdef MAX_CACHE = - 1 # entries __version__ = CUDD_VERSION.decode('utf-8') # TODO: replace DDDMP cdef extern from 'dddmp.h': ctypedef enum Dddmp_VarInfoType: pass ctypedef enum Dddmp_VarMatchType: pass int Dddmp_cuddBddStore( DdManager *ddMgr, char *ddname, DdNode *f, char **varnames, int *auxids, int mode, Dddmp_VarInfoType varinfo, char *fname, FILE *fp) DdNode *Dddmp_cuddBddLoad( DdManager *ddMgr, Dddmp_VarMatchType varMatchMode, char **varmatchnames, int *varmatchauxids, int *varcomposeids, int mode, char *fname, FILE *fp) cdef DDDMP_MODE_TEXT = 65 # 'A' cdef DDDMP_VARIDS = 0 cdef DDDMP_VARNAMES = 3 cdef DDDMP_VAR_MATCHNAMES = 3 cdef DDDMP_SUCCESS = 1 # 2**30 = 1 GiB (gibibyte, read ISO/IEC 80000) DEFAULT_MEMORY = 1 * 2**30 logger = logging.getLogger(__name__) cdef class BDD: """Wrapper of CUDD manager. Interface similar to `dd.bdd.BDD`. Variable names are strings. Attributes: - `vars`: `set` of bit names as `str`ings """ cdef DdManager *manager cdef public object vars cdef public object _index_of_var cdef public object _var_with_index def __cinit__( self, memory_estimate: _NumberOfBytes | None=None, initial_cache_size: _Cardinality | None=None, *arg, **kw ) -> None: """Initialize BDD manager. @param memory_estimate: maximum allowed memory, in bytes. """ self.manager = NULL # prepare for # `__dealloc__`, # in case an exception is raised below. # Including `*arg, **kw` in the # signature of the method `__cinit__` # aims to prevent an exception from # being raised upon instantiation # of the class `BDD` before the # body of the method `__cinit__` # is entered. # In that case, `self.manager` # could in principle have an # arbitrary value when `__dealloc__` # is executed. total_memory = _utils.total_memory() default_memory = DEFAULT_MEMORY if memory_estimate is None: memory_estimate = default_memory if total_memory is None: pass elif memory_estimate >= total_memory: msg = ( 'Error in `dd.cudd`: ' 'total physical memory ' f'is {total_memory} bytes, ' f'but requested {memory_estimate} bytes. ' 'Please pass an amount of memory to ' 'the `BDD` constructor to avoid this error. ' 'For example, by instantiating ' 'the `BDD` manager as ' f'`BDD({round(total_memory / 2)})`.') # The motivation of both printing and # raising an exception was that Cython # failed with a segmentation fault, # without showing the exception message. print(msg) raise ValueError(msg) if initial_cache_size is None: initial_cache_size = CUDD_CACHE_SLOTS initial_subtable_size = CUDD_UNIQUE_SLOTS initial_n_vars_bdd = 0 initial_n_vars_zdd = 0 mgr = Cudd_Init( initial_n_vars_bdd, initial_n_vars_zdd, initial_subtable_size, initial_cache_size, memory_estimate) if mgr is NULL: raise RuntimeError( 'failed to initialize CUDD DdManager') self.manager = mgr def __init__( self, memory_estimate: _NumberOfBytes | None=None, initial_cache_size: _Cardinality | None=None ) -> None: logger.info(f'Using CUDD v{__version__}') self.configure( reordering=True, max_cache_hard=MAX_CACHE) self.vars = set() # map: str -> unique fixed int self._index_of_var = dict() self._var_with_index = dict() def __dealloc__( self ) -> None: if self.manager is NULL: raise RuntimeError( '`self.manager` is `NULL`, ' 'which suggests that ' 'an exception was raised ' 'inside the method ' '`dd.cudd.BDD.__cinit__`.') n = Cudd_CheckZeroRef(self.manager) if n != 0: raise AssertionError( f'Still {n} nodes ' 'referenced upon shutdown.') # Exceptions raised inside `__dealloc__` will be # ignored. So if the `AssertionError` above is # raised, then Python will continue execution # without calling `Cudd_Quit`. # # Even though this can cause a memory leak, # incorrect reference counts imply that there # already is some issue, and that calling # `Cudd_Quit` might be unsafe. Cudd_Quit(self.manager) def __eq__( self: BDD, other: _ty.Optional[BDD] ) -> _Yes: """Return `True` if `other` has same manager.""" if other is None: return False return self.manager == other.manager def __ne__( self: BDD, other: _ty.Optional[BDD] ) -> _Yes: if other is None: return True return self.manager != other.manager def __len__( self ) -> _Cardinality: """Number of nodes with nonzero references.""" return Cudd_CheckZeroRef(self.manager) def __contains__( self, u: Function ) -> _Yes: if u.manager != self.manager: raise ValueError( 'undefined containment, because ' '`u.manager != self.manager`') try: Cudd_NodeReadIndex(u.node) return True except: return False def __str__( self ) -> str: d = self.statistics() s = ( 'Binary decision diagram ' '(CUDD wrapper) with:\n' '\t {n} live nodes now\n' '\t {peak} live nodes at peak\n' '\t {n_vars} BDD variables\n' '\t {mem:10.1f} bytes in use\n' '\t {reorder_time:10.1f} sec ' 'spent reordering\n' '\t {n_reorderings} reorderings\n' ).format( n=d['n_nodes'], peak=d['peak_live_nodes'], n_vars=d['n_vars'], reorder_time=d['reordering_time'], n_reorderings=d['n_reorderings'], mem=d['mem']) return s def statistics( self: BDD, exact_node_count: _Yes=False ) -> dict[ str, _ty.Any]: """Return `dict` with CUDD node counts and times. If `exact_node_count` is `True`, then the list of dead nodes is cleared. Keys with meaning: - `n_vars`: number of variables - `n_nodes`: number of live nodes - `peak_nodes`: max number of all nodes - `peak_live_nodes`: max number of live nodes - `reordering_time`: sec spent reordering - `n_reorderings`: number of reorderings - `mem`: bytes in use - `unique_size`: total number of buckets in unique table - `unique_used_fraction`: buckets that contain >= 1 node - `expected_unique_used_fraction`: if properly working - `cache_size`: number of slots in cache - `cache_used_fraction`: slots with data - `cache_lookups`: total number of lookups - `cache_hits`: total number of cache hits - `cache_insertions` - `cache_collisions` - `cache_deletions` """ warnings.warn( "Changed in `dd` version 0.5.7: " "In the `dict` returned by the method " "`dd.cudd.BDD.statistics`, " "the value of the key `'mem'` " "has changed to bytes (from 10**6 bytes).", UserWarning) cdef DdManager *mgr mgr = self.manager n_vars = Cudd_ReadSize(mgr) # nodes if exact_node_count: n_nodes = Cudd_ReadNodeCount(mgr) else: n_nodes = mgr.keys - mgr.dead peak_nodes = Cudd_ReadPeakNodeCount(mgr) peak_live_nodes = Cudd_ReadPeakLiveNodeCount(mgr) # reordering t = Cudd_ReadReorderingTime(mgr) reordering_time = t / 1000.0 n_reorderings = Cudd_ReadReorderings(mgr) # memory m = Cudd_ReadMemoryInUse(mgr) mem = float(m) # unique table unique_size = Cudd_ReadSlots(mgr) unique_used_fraction = Cudd_ReadUsedSlots(mgr) expected_unique_fraction = ( Cudd_ExpectedUsedSlots(mgr)) # cache cache_size = Cudd_ReadCacheSlots(mgr) cache_used_fraction = Cudd_ReadCacheUsedSlots(mgr) cache_lookups = Cudd_ReadCacheLookUps(mgr) cache_hits = Cudd_ReadCacheHits(mgr) cache_insertions = mgr.cacheinserts cache_collisions = mgr.cachecollisions cache_deletions = mgr.cachedeletions d = dict( n_vars=n_vars, n_nodes=n_nodes, peak_nodes=peak_nodes, peak_live_nodes=peak_live_nodes, reordering_time=reordering_time, n_reorderings=n_reorderings, mem=mem, unique_size=unique_size, unique_used_fraction=unique_used_fraction, expected_unique_used_fraction= expected_unique_fraction, cache_size=cache_size, cache_used_fraction=cache_used_fraction, cache_lookups=cache_lookups, cache_hits=cache_hits, cache_insertions=cache_insertions, cache_collisions=cache_collisions, cache_deletions=cache_deletions) return d def configure( self: BDD, **kw ) -> dict[ str, _ty.Any]: """Read and apply parameter values. First read (returned), then apply `kw`. Available keyword arguments: - `'reordering'`: if `True` then enable, else disable - `'garbage_collection'`: if `True` then enable, else disable - `'max_memory'`: in bytes - `'loose_up_to'`: unique table fast growth upper bound - `'max_cache_hard'`: cache entries upper bound - `'min_hit'`: hit ratio for resizing cache - `'max_growth'`: intermediate growth during sifting - `'max_swaps'`: no more level swaps in one sifting - `'max_vars'`: no more variables moved in one sifting For more details, read `cuddAPI.c`. Example usage: ```python import dd.cudd bdd = dd.cudd.BDD() # store old settings, and apply new settings cfg = bdd.configure( max_memory=12 * 1024**3, loose_up_to=5 * 10**6, max_cache_hard=MAX_CACHE, min_hit=20, max_growth=1.5) # something fancy # ... # restore old settings bdd.configure(**cfg) ``` """ cdef int method cdef DdManager *mgr mgr = self.manager # read reordering = Cudd_ReorderingStatus( mgr, &method) garbage_collection = ( Cudd_GarbageCollectionEnabled(mgr)) max_memory = Cudd_ReadMaxMemory(mgr) loose_up_to = Cudd_ReadLooseUpTo(mgr) max_cache_soft = Cudd_ReadMaxCache(mgr) max_cache_hard = Cudd_ReadMaxCacheHard(mgr) min_hit = Cudd_ReadMinHit(mgr) max_growth = Cudd_ReadMaxGrowth(mgr) max_swaps = Cudd_ReadSiftMaxSwap(mgr) max_vars = Cudd_ReadSiftMaxVar(mgr) d = dict( reordering=True if reordering == 1 else False, garbage_collection=True if garbage_collection == 1 else False, max_memory=max_memory, loose_up_to=loose_up_to, max_cache_soft=max_cache_soft, max_cache_hard=max_cache_hard, min_hit=min_hit, max_growth=max_growth, max_swaps=max_swaps, max_vars=max_vars) # set for k, v in kw.items(): if k == 'reordering': if v: Cudd_AutodynEnable( mgr, CUDD_REORDER_GROUP_SIFT) else: Cudd_AutodynDisable(mgr) elif k == 'garbage_collection': if v: Cudd_EnableGarbageCollection(mgr) else: Cudd_DisableGarbageCollection(mgr) elif k == 'max_memory': Cudd_SetMaxMemory(mgr, v) elif k == 'loose_up_to': Cudd_SetLooseUpTo(mgr, v) elif k == 'max_cache_hard': Cudd_SetMaxCacheHard(mgr, v) elif k == 'min_hit': Cudd_SetMinHit(mgr, v) elif k == 'max_growth': Cudd_SetMaxGrowth(mgr, v) elif k == 'max_swaps': Cudd_SetSiftMaxSwap(mgr, v) elif k == 'max_vars': Cudd_SetSiftMaxVar(mgr, v) elif k == 'max_cache_soft': logger.warning( '"max_cache_soft" not settable.') else: raise ValueError( f'Unknown parameter "{k}"') return d cpdef tuple succ( self, u: Function): """Return `(level, low, high)` for `u`.""" if u.manager != self.manager: raise ValueError( '`u.manager != self.manager`') i = u.level v = u.low w = u.high return i, v, w cpdef incref( self, u: Function): """Increment the reference count of `u`. Raise `RuntimeError` if `u._ref <= 0`. For more details about avoiding this read the docstring of the class `Function`. The reference count of the BDD node in CUDD that `u` points to is incremented. Also, the attribute `u._ref` is incremented. Calling this method is unnecessary, because reference counting is automated. """ if u.node is NULL: raise RuntimeError( '`u.node` is `NULL` pointer.') if u._ref <= 0: _utils.raise_runtimerror_about_ref_count( u._ref, 'method `dd.cudd.BDD.incref`', '`dd.cudd.Function`') assert u._ref > 0, u._ref u._ref += 1 self._incref(u.node) cpdef decref( self, u: Function, recursive: _Yes=False, _direct: _Yes=False): """Decrement the reference count of `u`. Raise `RuntimeError` if `u._ref <= 0` or `u.node is NULL`. For more details about avoiding this read the docstring of the class `Function`. The reference count of the BDD node in CUDD that `u` points to is decremented. Also, the attribute `u._ref` is decremented. If after this decrement, `u._ref == 0`, then the pointer `u.node` is set to `NULL`. Calling this method is unnecessary, because reference counting is automated. If early dereferencing of the node is desired in order to allow garbage collection, then write `del u`, instead of calling this method. @param recursive: if `True`, then call `Cudd_RecursiveDeref`, else call `Cudd_Deref` @param _direct: use this parameter only after reading the source code of the Cython file `dd/cudd.pyx`. When `_direct == True`, some of the above description does not apply. """ if u.node is NULL: raise RuntimeError( '`u.node` is `NULL` pointer.') # bypass checks and leave `u._ref` unchanged, # directly call `_decref` if _direct: self._decref(u.node, recursive) return if u._ref <= 0: _utils.raise_runtimerror_about_ref_count( u._ref, 'method `dd.cudd.BDD.decref`', '`dd.cudd.Function`') assert u._ref > 0, u._ref u._ref -= 1 self._decref(u.node, recursive) if u._ref == 0: u.node = NULL cdef _incref( self, u: DdRef): Cudd_Ref(u) cdef _decref( self, u: DdRef, recursive: _Yes=False): # There is little point in checking here # the reference count of `u`, because # doing that relies on the assumption # that `u` still corresponds to a node, # which implies that the reference count # is positive. # # This point should not be reachable # after `u` reaches zero reference count. # # Moreover, if the memory has been deallocated, # then in principle the attribute `ref` # can have any value, so an assertion here # would not be ensuring correctness. if recursive: Cudd_RecursiveDeref(self.manager, u) else: Cudd_Deref(u) def declare( self, *variables: _VariableName ) -> None: """Add names in `variables` to `self.vars`.""" for var in variables: self.add_var(var) cpdef int add_var( self, var: _VariableName, index: _Nat | None=None): """Return index of variable named `var`. If a variable named `var` exists, the assert that it has `index`. Otherwise, create a variable named `var` with `index` (if given). If no reordering has yet occurred, then the returned index equals the level, provided `add_var` has been used so far. """ # var already exists ? j = self._index_of_var.get(var) if j is not None: if index is not None and j != index: raise AssertionError(j, index) return j # new var if index is None: j = len(self._index_of_var) else: j = index u = Cudd_bddIthVar(self.manager, j) if u is NULL: raise RuntimeError( f'failed to add var "{var}"') self._add_var(var, j) return j cpdef int insert_var( self, var: _VariableName, level: _Level): """Create new variable at `level`. The name of the variable is the string `var`. @param var: name of variable that this function will declare @param level: where the new variable will be placed in the variable order of this BDD manager @return: number that CUDD uses to identify the newly created variable. This number is also called an index of the variable. @rtype: `int` >= 0 """ r: DdRef r = Cudd_bddNewVarAtLevel( self.manager, level) if r is NULL: raise RuntimeError( f'failed to create var "{var}"') j = r.index self._add_var(var, j) return j cdef _add_var( self, var: _VariableName, index: _Nat): """Declare new variable `var`. Adds to `self` a *new* variable named `var`, identified within CUDD by the number `index`. @param var: name of variable that this function will declare @param index: number that will identify within CUDD the newly created variable """ if var in self.vars: raise ValueError( f'existing variable: "{var}"') if var in self._index_of_var: raise ValueError( 'variable already has index: {i}'.format( i=self._index_of_var[var])) if index in self._var_with_index: raise ValueError(( 'index already corresponds ' 'to a variable: {v}').format( v=self._var_with_index[index])) self.vars.add(var) self._index_of_var[var] = index self._var_with_index[index] = var if (len(self._index_of_var) != len(self._var_with_index)): raise AssertionError( 'the attributes ' '`_index_of_var` and ' '`_var_with_index` ' 'have different length') cpdef Function var( self, var: _VariableName): """Return node for variable named `var`.""" if var not in self._index_of_var: raise ValueError( f'undeclared variable "{var}", ' 'the declared variables are:\n' f'{self._index_of_var}') j = self._index_of_var[var] r = Cudd_bddIthVar(self.manager, j) return wrap(self, r) def var_at_level( self, level: _Level ) -> _VariableName: """Return name of variable at `level`. Raise `ValueError` if `level` is not the level of any variable declared in `self.vars`. """ j = Cudd_ReadInvPerm(self.manager, level) if (j == -1 or j == CUDD_CONST_INDEX or j not in self._var_with_index): raise ValueError(_tw.dedent(f''' No declared variable has level: {level}. {_utils.var_counts(self)} ''')) var = self._var_with_index[j] return var def level_of_var( self, var: _VariableName ) -> _Level: """Return level of variable named `var`. Raise `ValueError` if `var` is not a variable in `self.vars`. """ if var not in self._index_of_var: raise ValueError( f'undeclared variable "{var}", ' 'the declared variables are:\n' f'{self._index_of_var}') j = self._index_of_var[var] level = Cudd_ReadPerm(self.manager, j) if level == -1: raise AssertionError( f'index {j} out of bounds') return level @property def var_levels( self ) -> _VariableLevels: return { var: self.level_of_var(var) for var in self.vars} def _number_of_cudd_vars( self ) -> _Cardinality: """Return number of CUDD indices. Can be `> len(self.vars)`. """ n_cudd_vars = Cudd_ReadSize(self.manager) if 0 <= n_cudd_vars <= CUDD_CONST_INDEX: return n_cudd_vars raise RuntimeError(_tw.dedent(f''' Unexpected value: {n_cudd_vars} returned from `Cudd_ReadSize()` (expected <= {CUDD_CONST_INDEX} = CUDD_CONST_INDEX) ''')) def reorder( self, var_order: _VariableLevels | None=None ) -> None: """Reorder variables to `var_order`. If `var_order` is `None`, then invoke sifting. """ reorder(self, var_order) cpdef set support( self, u: Function): """Return variables that `u` depends on. @return: set of variable names @rtype: `set[str]` """ if self.manager != u.manager: raise ValueError( '`u.manager != self.manager`') r: DdRef r = Cudd_Support(self.manager, u.node) cube = wrap(self, r) support = self._cube_to_dict(cube) # constant ? if not support: return set() # must be positive unate for value in support.values(): if value is True: continue raise AssertionError(support) return set(support) def group( self, vrs: _abc.Mapping[ _VariableName, _Nat] ) -> None: r"""Couple adjacent variables. The variables in `vrs` must be at levels that form a contiguous range. ```tla ASSUME \A value \in vrs.values(): value >= 2 ``` """ cdef unsigned int group_low cdef unsigned int group_size for var, group_size in vrs.items(): if group_size <= 1: raise ValueError( 'singleton as group ' 'has no effect') group_low = self._index_of_var[var] Cudd_MakeTreeNode( self.manager, group_low, group_size, MTR_DEFAULT) def copy( self, u: Function, other: 'BDD' | autoref.BDD ) -> ( Function | autoref.Function): """Transfer BDD with root `u` to `other`.""" if isinstance(other, BDD): return copy_bdd(u, other) else: return _copy.copy_bdd(u, other) cpdef Function let( self, definitions: _Renaming | _Assignment | dict[_VariableName, Function], u: Function): r"""Substitute variables. Variables can be substituted with: - other variables (by name) - Boolean constant values (given as Python `bool` values) - binary decision diagrams (given as `Function` instances) Variables that are to be substituted are identified by their names, as keys of the argument `definitions`, which is a `dict`. Multiple variables can be substituted at once. This means that variables can be swapped too. The name of this function originates from TLA+ and languages with "let" expressions. A "let" expression in TLA+ takes the following form: ```tla LET x == TRUE IN x /\ y ``` In a context where `y` can take only the values `FALSE` and `TRUE`, the above `LET` expression is equivalent to the expression `y`. In comparison, the expression: ```tla LET x == FALSE IN x /\ y ``` is equivalent to `FALSE`. """ if not definitions: logger.warning( 'Call to `BDD.let` with no effect: ' '`defs` is empty.') return u var = next(iter(definitions)) value = definitions[var] if isinstance(value, python_bool): return self._cofactor(u, definitions) elif isinstance(value, Function): return self._compose(u, definitions) try: value + 's' except TypeError: raise ValueError( 'Value must be variable ' 'name as `str`, ' 'or Boolean value as `bool`, ' 'or BDD node as `int`. ' f'Got: {value}') return self._rename(u, definitions) cpdef Function _compose( self, f: Function, var_sub: dict): """Return the composition f|_(var = g). @param var_sub: maps variable names to nodes. """ n = len(var_sub) if n == 0: logger.warning( 'call without any effect') return f if n > 1: return self._multi_compose(f, var_sub) if n != 1: raise ValueError(n) var, g = next(iter(var_sub.items())) return self._unary_compose(f, var, g) cdef Function _unary_compose( self, f: Function, var: _VariableName, g: Function): """Return single composition.""" if f.manager != self.manager: raise ValueError( '`f.manager != self.manager`') if g.manager != self.manager: raise ValueError( '`g.manager != self.manager`') r: DdRef index = self._index_of_var[var] r = Cudd_bddCompose( self.manager, f.node, g.node, index) if r is NULL: raise RuntimeError('compose failed') return wrap(self, r) cdef Function _multi_compose( self, f: Function, var_sub: dict[ _VariableName, Function]): """Return vector composition.""" if f.manager != self.manager: raise ValueError( '`f.manager != self.manager`') r: DdRef cdef DdRef *x g: Function n_cudd_vars = self._number_of_cudd_vars() if n_cudd_vars <= 0: raise AssertionError(n_cudd_vars) x = PyMem_Malloc( n_cudd_vars *sizeof(DdRef)) for var in self.vars: j = self._index_of_var[var] if var in var_sub: # substitute g = var_sub[var] if g.manager != self.manager: raise ValueError((var, g)) x[j] = g.node else: # leave var same x[j] = Cudd_bddIthVar( self.manager, j) try: r = Cudd_bddVectorCompose( self.manager, f.node, x) finally: PyMem_Free(x) return wrap(self, r) cpdef Function _cofactor( self, f: Function, values: _Assignment): """Substitute Booleans for variables. @param values: maps variable names to Boolean constants @return: result of substitution @rtype: `Function` """ if self.manager != f.manager: raise ValueError(f) r: DdRef cube: Function cube = self.cube(values) r = Cudd_Cofactor( self.manager, f.node, cube.node) if r is NULL: raise RuntimeError( 'cofactor failed') return wrap(self, r) cpdef Function _rename( self, u: Function, dvars: dict[ _VariableName, _VariableName]): """Return node `u` after renaming variables. How to rename the variable is defined in the argument `dvars`, which is a `dict`. The argument value `dvars = dict(x='y')` results in variable `'x'` substituted by variable `'y'`. The argument value `dvars = dict(x='y', y='x')` results in simultaneous substitution of variable `'x'` by variable `'y'` and of variable `'y'` by variable `'x'`. """ rename = { k: self.var(v) for k, v in dvars.items()} return self._compose(u, rename) cpdef Function _swap( self, u: Function, dvars: dict[ _VariableName, _VariableName]): """Return result from swapping variable pairs. The variable pairs are defined in the argument `dvars`, which is a `dict`. Asserts that each variable occurs in at most one key-value pair of the dictionary `dvars`. The argument value `dvars = dict(x='y')` results in swapping of variables `'x'` and `'y'`, which is equivalent to simultaneous substitution of `'x'` by `'y'` and `'y'` by `'x'`. So the argument value `dvars = dict(x='y')` has the same result as calling `_rename` with `dvars = dict(x='y', y='x')`. """ # assert that each variable # occurs in at most one # key-value pair of the # dictionary `dvars`: # 1) assert keys and values of # `dvars` are disjoint sets common = { var for var in dvars.values() if var in dvars} if common: raise ValueError(common) # 2) assert each value is unique values = set(dvars.values()) if len(dvars) != len(values): raise ValueError(dvars) # # call swapping n = len(dvars) cdef DdRef *x = PyMem_Malloc( n * sizeof(DdRef)) cdef DdRef *y = PyMem_Malloc( n * sizeof(DdRef)) r: DdRef cdef DdManager *mgr = u.manager f: Function for i, xvar in enumerate(dvars): yvar = dvars[xvar] f = self.var(xvar) x[i] = f.node f = self.var(yvar) y[i] = f.node try: r = Cudd_bddSwapVariables( mgr, u.node, x, y, n) if r is NULL: raise RuntimeError( 'variable swap failed') finally: PyMem_Free(x) PyMem_Free(y) return wrap(self, r) cpdef Function ite( self, g: Function, u: Function, v: Function): """Ternary conditional. In other words, the root of the BDD that represents the expression: ```tla IF g THEN u ELSE v ``` """ if g.manager != self.manager: raise ValueError( '`g.manager != self.manager`') if u.manager != self.manager: raise ValueError( '`u.manager != self.manager`') if v.manager != self.manager: raise ValueError( '`v.manager != self.manager`') r: DdRef r = Cudd_bddIte( self.manager, g.node, u.node, v.node) return wrap(self, r) cpdef Function find_or_add( self, var: _VariableName, low: Function, high: Function): """Return node `IF var THEN high ELSE low`.""" if low.manager != self.manager: raise ValueError( '`low.manager != self.manager`') if high.manager != self.manager: raise ValueError( '`high.manager != self.manager`') if var not in self.vars: raise ValueError( f'undeclared variable: {var}, ' 'the declared variables ' f'are: {self.vars}') level = self.level_of_var(var) if level >= low.level: raise ValueError( level, low.level, 'low.level') if level >= high.level: raise ValueError( level, high.level, 'high.level') r: DdRef index = self._index_of_var[var] r = cuddUniqueInter( self.manager, index, high.node, low.node) return wrap(self, r) def count( self, u: Function, nvars: _Cardinality | None=None ) -> _Cardinality: """Return number of models of node `u`. @param nvars: regard `u` as an operator that depends on `nvars`-many variables. If omitted, then assume those variables in `support(u)`. """ if u.manager != self.manager: raise ValueError( '`u.manager != self.manager`') n = len(self.support(u)) if nvars is None: nvars = n if nvars < n: raise ValueError(nvars, n) r = Cudd_CountMinterm( self.manager, u.node, nvars) if r == CUDD_OUT_OF_MEM: raise RuntimeError( 'CUDD out of memory') if r == float('inf'): raise RuntimeError( 'overflow of integer ' 'type double') return r def pick( self, u: Function, care_vars: _abc.Set[ _VariableName] | None=None ) -> _Assignment: """Return a single assignment. @return: assignment of values to variables """ return next( self.pick_iter(u, care_vars), None) def _pick_iter( self, u: Function, care_vars: _abc.Set[ _VariableName] | None=None ) -> _abc.Iterable[ _Assignment]: """Return iterator over assignments. The returned iterator is generator-based. """ if u.manager != self.manager: raise ValueError( '`u.manager != self.manager`') cdef DdGen *gen cdef int *cube cdef double value support = self.support(u) if care_vars is None: care_vars = support missing = { v for v in support if v not in care_vars} if missing: logger.warning( 'Missing bits: ' f'support - care_vars = {missing}') config = self.configure( reordering=False) gen = Cudd_FirstCube( self.manager, u.node, &cube, &value) if gen is NULL: raise RuntimeError( 'first cube failed') try: r = 1 while Cudd_IsGenEmpty(gen) == 0: if r != 1: raise RuntimeError( 'gen not empty but ' 'no next cube', r) d = _cube_array_to_dict( cube, self._index_of_var) if not set(d).issubset(support): raise AssertionError( set(d).difference(support)) for m in _bdd._enumerate_minterms( d, care_vars): yield m r = Cudd_NextCube( gen, &cube, &value) finally: Cudd_GenFree(gen) self.configure( reordering=config['reordering']) def pick_iter( self, u: Function, care_vars: _abc.Set[ _VariableName] | None=None ) -> _abc.Iterable[ _Assignment]: """Return iterator over assignments. The returned iterator is generator-based. """ if self.manager != u.manager: raise ValueError( '`u.manager != self.manager`') support = self.support(u) if care_vars is None: care_vars = support missing = { v for v in support if v not in care_vars} if missing: logger.warning( 'Missing bits: ' f'support - care_vars = {missing}') cube = dict() value = True config = self.configure( reordering=False) for cube in self._sat_iter( u, cube, value, support): for m in _bdd._enumerate_minterms( cube, care_vars): yield m self.configure( reordering=config['reordering']) def _sat_iter( self, u: Function, cube: _Assignment, value: python_bool, support ) -> _abc.Iterable[ _Assignment]: """Recurse to enumerate models.""" if u.negated: value = not value # terminal ? if u.var is None: if value: if not set(cube).issubset(support): raise ValueError(set( cube).difference(support)) yield cube return # non-terminal i, v, w = self.succ(u) var = self.var_at_level(i) d0 = dict(cube) d0[var] = False d1 = dict(cube) d1[var] = True for x in self._sat_iter( v, d0, value, support): yield x for x in self._sat_iter( w, d1, value, support): yield x cpdef Function apply( self, op: _dd_abc.OperatorSymbol, u: Function, v: _ty.Optional[Function] =None, w: _ty.Optional[Function] =None): """Return the result of applying `op`.""" _utils.assert_operator_arity(op, v, w, 'bdd') if self.manager != u.manager: raise ValueError( '`u.manager != self.manager`') if v is not None and self.manager != v.manager: raise ValueError( '`v.manager != self.manager`') if w is not None and self.manager != w.manager: raise ValueError( '`w.manager != self.manager`') r: DdRef cdef DdManager *mgr mgr = u.manager # unary r = NULL if op in ('~', 'not', '!'): r = Cudd_Not(u.node) # binary elif op in ('and', '/\\', '&', '&&'): r = Cudd_bddAnd(mgr, u.node, v.node) elif op in ('or', r'\/', '|', '||'): r = Cudd_bddOr(mgr, u.node, v.node) elif op in ('#', 'xor', '^'): r = Cudd_bddXor(mgr, u.node, v.node) elif op in ('=>', '->', 'implies'): r = Cudd_bddIte( mgr, u.node, v.node, Cudd_ReadOne(mgr)) elif op in ('<=>', '<->', 'equiv'): r = Cudd_bddXnor(mgr, u.node, v.node) elif op in ('diff', '-'): r = Cudd_bddIte( mgr, u.node, Cudd_Not(v.node), Cudd_ReadLogicZero(mgr)) elif op in (r'\A', 'forall'): r = Cudd_bddUnivAbstract( mgr, v.node, u.node) elif op in (r'\E', 'exists'): r = Cudd_bddExistAbstract( mgr, v.node, u.node) # ternary elif op == 'ite': r = Cudd_bddIte( mgr, u.node, v.node, w.node) else: raise ValueError( f'unknown operator: "{op}"') if r is NULL: config = self.configure() raise RuntimeError(( 'CUDD appears to have ' 'run out of memory.\n' 'Current settings for ' 'upper bounds are:\n' ' max memory = {max_memory} bytes\n' ' max cache = {max_cache} entries' ).format( max_memory=config['max_memory'], max_cache=config['max_cache_hard'])) return wrap(self, r) cpdef Function _add_int( self, i: int): """Return node from `i`. Inverse of `Function.__int__()`. """ u: DdRef = _int_to_ddref(i) return wrap(self, u) cpdef Function cube( self, dvars: _abc.Collection[ _VariableName]): """Return node for cube over `dvars`.""" n_cudd_vars = self._number_of_cudd_vars() # make cube cube: DdRef cdef int *x x = PyMem_Malloc( n_cudd_vars * sizeof(int)) _dict_to_cube_array( dvars, x, self._index_of_var) try: cube = Cudd_CubeArrayToBdd( self.manager, x) finally: PyMem_Free(x) return wrap(self, cube) cdef Function _cube_from_bdds( self, dvars: _abc.Iterable[ _VariableName]): """Return node for cube over `dvars`. Only positive unate cubes implemented for now. """ n = len(dvars) # make cube cube: DdRef cdef DdRef *x x = PyMem_Malloc( n * sizeof(DdRef)) for i, var in enumerate(dvars): f = self.var(var) x[i] = f.node try: cube = Cudd_bddComputeCube( self.manager, x, NULL, n) finally: PyMem_Free(x) return wrap(self, cube) cpdef dict _cube_to_dict( self, f: Function): """Collect indices of support variables.""" if f.manager != self.manager: raise ValueError( '`f.manager != self.manager`') n_cudd_vars = self._number_of_cudd_vars() cdef int *x x = PyMem_Malloc( n_cudd_vars * sizeof(DdRef)) try: Cudd_BddToCubeArray( self.manager, f.node, x) d = _cube_array_to_dict( x, self._index_of_var) finally: PyMem_Free(x) return d cpdef Function quantify( self, u: Function, qvars: _abc.Iterable[ _VariableName], forall: _Yes=False): """Abstract variables `qvars` from node `u`.""" if u.manager != self.manager: raise ValueError( '`u.manager != self.manager`') cdef DdManager *mgr = u.manager c = set(qvars) cube = self.cube(c) # quantify if forall: r = Cudd_bddUnivAbstract( mgr, u.node, cube.node) else: r = Cudd_bddExistAbstract( mgr, u.node, cube.node) return wrap(self, r) cpdef Function forall( self, variables: _abc.Iterable[ _VariableName], u: Function): """Quantify `variables` in `u` universally. Wraps method `quantify` to be more readable. """ return self.quantify( u, variables, forall=True) cpdef Function exist( self, variables: _abc.Iterable[ _VariableName], u: Function): """Quantify `variables` in `u` existentially. Wraps method `quantify` to be more readable. """ return self.quantify( u, variables, forall=False) cpdef assert_consistent( self): """Raise `AssertionError` if not consistent.""" if Cudd_DebugCheck(self.manager) != 0: raise AssertionError( '`Cudd_DebugCheck` errored') n = len(self.vars) m = len(self._var_with_index) k = len(self._index_of_var) if n != m: raise AssertionError(n, m) if m != k: raise AssertionError(m, k) def add_expr( self, expr: _Formula ) -> Function: """Return node for expression `e`.""" return _parser.add_expr(expr, self) cpdef str to_expr( self, u: Function): """Return a Boolean expression for node `u`.""" if u.manager != self.manager: raise ValueError( '`u.manager != self.manager`') cache = dict() return self._to_expr(u.node, cache) cdef str _to_expr( self, u: DdRef, cache: dict[int, str]): if u == Cudd_ReadLogicZero(self.manager): return 'FALSE' if u == Cudd_ReadOne(self.manager): return 'TRUE' u_index = _ddref_to_int(u) if u_index in cache: return cache[u_index] v = Cudd_E(u) w = Cudd_T(u) p = self._to_expr(v, cache) q = self._to_expr(w, cache) r = Cudd_Regular(u) var = self._var_with_index[r.index] # pure var ? if p == 'FALSE' and q == 'TRUE': expr = var else: expr = f'ite({var}, {q}, {p})' # complemented ? if Cudd_IsComplement(u): expr = f'(~ {expr})' cache[u_index] = expr return expr def dump( self, filename: str, roots: dict[str, Function] | list[Function], filetype: _BDDFileType | None=None ) -> None: """Write BDDs to `filename`. The file type is inferred from the extension (case insensitive), unless a `filetype` is explicitly given. `filetype` can have the values: - `'pdf'` for PDF - `'png'` for PNG - `'svg'` for SVG - `'json'` for JSON - `'dddmp'` for DDDMP (of CUDD) If `filetype is None`, then `filename` must have an extension that matches one of the file types listed above. Dump nodes reachable from `roots`. Dumping a JSON file requires that `roots` be nonempty. Dumping a DDDMP file requires that `roots` contain a single node. @param roots: For JSON: a mapping from names to nodes. """ if filetype is None: name = filename.lower() if name.endswith('.pdf'): filetype = 'pdf' elif name.endswith('.png'): filetype = 'png' elif name.endswith('.svg'): filetype = 'svg' elif name.endswith('.dot'): filetype = 'dot' elif name.endswith('.p'): raise ValueError( 'pickling unsupported ' 'by this class, use JSON') elif name.endswith('.json'): filetype = 'json' elif name.endswith('.dddmp'): filetype = 'dddmp' else: raise ValueError( 'cannot infer file type ' 'from extension of file ' f'name "{filename}"') if filetype == 'dddmp': # single root supported for now u, = roots self._dump_dddmp(u, filename) return elif filetype == 'json': if roots is None: raise ValueError(roots) _copy.dump_json(roots, filename) return elif (filetype != 'pickle' and filetype not in _utils.DOT_FILE_TYPES): raise ValueError(filetype) bdd = autoref.BDD() _copy.copy_vars(self, bdd) # preserve levels if roots is None: root_nodes = None else: cache = dict() def mapper(u): return _copy.copy_bdd( u, bdd, cache) root_nodes = _utils.map_container( mapper, roots) bdd.dump( filename, root_nodes, filetype=filetype) cpdef _dump_dddmp( self, u: Function, fname: str): """Dump BDD as DDDMP file named `fname`.""" if u.manager != self.manager: raise ValueError( '`u.manager != self.manager`') n_declared_vars = len(self._var_with_index) n_cudd_vars = self._number_of_cudd_vars() if n_declared_vars != n_cudd_vars: counts = _utils.var_counts(self) contiguous = _utils.contiguous_levels( '_dump_dddmp', self) raise AssertionError( f'{counts}\n{contiguous}') cdef FILE *f cdef char **names cdef bytes py_bytes names = PyMem_Malloc( n_cudd_vars * sizeof(char *)) str_mem = list() for index, var in self._var_with_index.items(): py_bytes = var.encode() str_mem.append(py_bytes) # prevent garbage collection names[index] = py_bytes try: f = fopen(fname.encode(), 'w') i = Dddmp_cuddBddStore( self.manager, NULL, u.node, names, NULL, DDDMP_MODE_TEXT, DDDMP_VARNAMES, NULL, f) finally: fclose(f) PyMem_Free(names) if i != DDDMP_SUCCESS: raise RuntimeError( 'failed to write to DDDMP file') cpdef load( self, filename: str): """Return `Function` loaded from `filename`. @param filename: name of file from where the BDD is loaded @return: roots of loaded BDDs @rtype: depends on the contents of the file: | `dict[str, Function]` | `list[Function]` """ if filename.lower().endswith('.dddmp'): r = self._load_dddmp(filename) return [r] elif filename.lower().endswith('.json'): return _copy.load_json(filename, self) else: raise ValueError( f'Unknown file type "{filename}"') cpdef Function _load_dddmp( self, filename: str): n_declared_vars = len(self._var_with_index) n_cudd_vars = self._number_of_cudd_vars() if n_declared_vars != n_cudd_vars: counts = _utils.var_counts(self) contiguous = _utils.contiguous_levels( '_load_dddmp', self) raise AssertionError(f'{counts}\n{contiguous}') r: DdRef cdef FILE *f cdef char **names cdef bytes py_bytes names = PyMem_Malloc( n_cudd_vars * sizeof(char *)) str_mem = list() for index, var in self._var_with_index.items(): py_bytes = var.encode() str_mem.append(py_bytes) names[index] = py_bytes try: f = fopen(filename.encode(), 'r') r = Dddmp_cuddBddLoad( self.manager, DDDMP_VAR_MATCHNAMES, names, NULL, NULL, DDDMP_MODE_TEXT, NULL, f) except: raise Exception( 'A malformed DDDMP file ' 'can cause segmentation ' 'faults to `cudd/dddmp`.') finally: fclose(f) PyMem_Free(names) if r is NULL: raise RuntimeError( 'failed to load DDDMP file.') h = wrap(self, r) # `Dddmp_cuddBddArrayLoad` references `r` Cudd_RecursiveDeref(self.manager, r) return h @property def false( self ) -> Function: """Boolean value false.""" return self._bool(False) @property def true( self ) -> Function: """Boolean value true.""" return self._bool(True) cdef Function _bool( self, v: python_bool): """Return leaf node for Boolean `v`.""" r: DdRef if v: r = Cudd_ReadOne(self.manager) else: r = Cudd_ReadLogicZero(self.manager) return wrap(self, r) cpdef Function restrict( u: Function, care_set: Function): """Restrict `u` to `care_set`. The operator "restrict" is defined in 1990 Coudert ICCAD. """ if u.manager != care_set.manager: raise ValueError( '`u.manager != care_set.manager`') r: DdRef r = Cudd_bddRestrict( u.manager, u.node, care_set.node) return wrap(u.bdd, r) cpdef Function and_exists( u: Function, v: Function, qvars: _abc.Iterable[ _VariableName]): r"""Return `\E qvars: u /\ v`.""" if u.manager != v.manager: raise ValueError( '`u.manager != v.manager`') qvars = set(qvars) cube = u.bdd.cube(qvars) r = Cudd_bddAndAbstract( u.manager, u.node, v.node, cube.node) return wrap(u.bdd, r) cpdef Function or_forall( u: Function, v: Function, qvars: _abc.Iterable[ _VariableName]): r"""Return `\A qvars: u \/ v`.""" if u.manager != v.manager: raise ValueError( '`u.manager != v.manager`') qvars = set(qvars) cube = u.bdd.cube(qvars) r = Cudd_bddAndAbstract( u.manager, Cudd_Not(u.node), Cudd_Not(v.node), cube.node) r = Cudd_Not(r) return wrap(u.bdd, r) cpdef reorder( bdd: BDD, dvars: _VariableLevels | None=None): """Reorder `bdd` to order in `dvars`. If `dvars` is `None`, then invoke group sifting. """ # invoke sifting ? if dvars is None: Cudd_ReduceHeap( bdd.manager, CUDD_REORDER_GROUP_SIFT, 1) return n_declared_vars = len(bdd.vars) n_cudd_vars = bdd._number_of_cudd_vars() if n_declared_vars != n_cudd_vars: counts = _utils.var_counts(bdd) contiguous = _utils.contiguous_levels( 'reorder', bdd) raise AssertionError( f'{counts}\n{contiguous}') # partial reorderings not supported for now if len(dvars) != n_cudd_vars: raise ValueError( 'Mismatch of variable numbers:\n' 'the number of declared variables ' f'is: {n_cudd_vars}\n' f'new variable order: {len(dvars)}') cdef int *p p = PyMem_Malloc( n_cudd_vars * sizeof(int *)) level_to_var = {v: k for k, v in dvars.items()} for level in range(n_cudd_vars): var = level_to_var[level] index = bdd._index_of_var[var] p[level] = index try: r = Cudd_ShuffleHeap(bdd.manager, p) finally: PyMem_Free(p) if r != 1: raise RuntimeError( 'Failed to reorder. ' 'Variable groups that are incompatible to ' 'the given order can cause this.') def copy_vars( source: BDD, target: BDD ) -> None: """Copy variables, preserving CUDD indices.""" for var, index in source._index_of_var.items(): target.add_var(var, index=index) cpdef Function copy_bdd( u: Function, target: BDD): """Copy BDD of node `u` to manager `target`. Turns off reordering in `source` when checking for missing vars in `target`. ```tla ASSUME u in source ``` """ logger.debug('++ transfer bdd') source = u.bdd if u.manager == target.manager: logger.warning( 'copying node to same manager') return u # target missing vars ? cfg = source.configure(reordering=False) supp = source.support(u) source.configure(reordering=cfg['reordering']) missing = { var for var in supp if var not in target.vars} if missing: raise ValueError( '`target` BDD is missing the variables:\n' f'{missing}\n' 'the declared variables in `target` are:\n' f'{target.vars}\n') # mapping of indices n_cudd_vars = source._number_of_cudd_vars() cdef int *renaming renaming = PyMem_Malloc( n_cudd_vars * sizeof(int)) # only support will show up during BDD traversal for var in supp: i = source._index_of_var[var] j = target._index_of_var[var] renaming[i] = j try: r = Cudd_bddTransferRename( source.manager, target.manager, u.node, renaming) finally: PyMem_Free(renaming) logger.debug( '-- done transferring bdd') return wrap(target, r) cpdef int count_nodes( functions: list[Function]): """Return total nodes used by `functions`. Sharing is taken into account. """ cdef DdRef *x f: Function n = len(functions) x = PyMem_Malloc( n * sizeof(DdRef)) for i, f in enumerate(functions): x[i] = f.node try: k = Cudd_SharingSize(x, n) finally: PyMem_Free(x) return k cpdef dict count_nodes_per_level( bdd: BDD): """Return mapping of each var to a node count.""" d = dict() for var in bdd.vars: level = bdd.level_of_var(var) n = bdd.manager.subtables[level].keys d[var] = n return d def dump( u: Function, file_name: str ) -> None: """Pickle variable order and dump dddmp file.""" bdd = u.bdd pickle_fname = f'{file_name}.pickle' dddmp_fname = f'{file_name}.dddmp' order = { var: bdd.level_of_var(var) for var in bdd.vars} d = dict(variable_order=order) with open(pickle_fname, 'wb') as f: pickle.dump(d, f, protocol=2) bdd.dump(u, dddmp_fname) def load( file_name: str, bdd: BDD, reordering: _Yes=False ) -> Function: """Unpickle variable order and load dddmp file. Loads the variable order, reorders `bdd` to match that order, turns off reordering, then loads the BDD, restores reordering. Assumes that: - `file_name` has no extension - pickle file name: `file_name.pickle` - dddmp file name: `file_name.dddmp` @param reordering: if `True`, then enable reordering during DDDMP load. """ t0 = time.time() pickle_fname = f'{file_name}.pickle' dddmp_fname = f'{file_name}.dddmp' with open(pickle_fname, 'rb') as f: d = pickle.load(f) order = d['variable_order'] for var in order: bdd.add_var(var) reorder(bdd, order) cfg = bdd.configure(reordering=False) u = bdd.load(dddmp_fname) bdd.configure(reordering=cfg['reordering']) t1 = time.time() dt = t1 - t0 logger.info( f'BDD load time from file: {dt}') return u cdef _dict_to_cube_array( d: _Assignment, int *x, index_of_var: _Assignment | set[_VariableName]): """Assign array of literals `x` from assignment `d`. @param x: array of literals 0: negated, 1: positive, 2: don't care read `Cudd_FirstCube` @param index_of_var: `dict` from variables to `bool` or `set` of variable names. """ for var in d: if var not in index_of_var: raise ValueError(var) for var, j in index_of_var.items(): if var not in d: x[j] = 2 continue # var in `d` if isinstance(d, dict): b = d[var] else: b = True if b is False: x[j] = 0 elif b is True: x[j] = 1 else: raise ValueError( f'unknown value: {b}') cdef dict _cube_array_to_dict( int *x, index_of_var: dict): """Return assignment from array of literals `x`. @param x: read `_dict_to_cube_array` """ d = dict() for var, j in index_of_var.items(): b = x[j] if b == 2: continue elif b == 1: d[var] = True elif b == 0: d[var] = False else: raise Exception( f'unknown polarity: {b}, ' f'for variable "{var}"') return d cdef Function wrap( bdd: BDD, node: DdRef): """Return a `Function` that wraps `node`.""" # because `@classmethod` unsupported f = Function() f.init(node, bdd) return f cdef class Function: r"""Wrapper of `DdNode` from CUDD. Attributes (those that are properties are described in their docstrings): - `_index` - `_ref`: safe lower bound on reference count of the CUDD BDD node pointed to by this `Function` instance. Do not modify this value. - `var` - `level` - `ref` - `low` - `high` - `negated` - `support` - `dag_size` In Python, use as: ```python from dd.cudd import BDD bdd = BDD() u = bdd.true v = bdd.false w = u | ~ v ``` In Cython, use as: ```cython bdd = BDD() cdef DdNode *u u = Cudd_ReadOne(bdd.manager) f = Function() f.init(bdd, u) ``` About reference counting ======================== Nothing needs to be done for reference counting by the user: reference counting is automated. "Early" dereferencing of a CUDD BDD node is possible by using the statement: ```python del u ``` where `u` is an instance of the class `Function`. "Early" here means that the CUDD BDD node will be dereferenced before it would have otherwise been dereferenced. That (possibly) later time would have been when Python exited the scope where `u` was defined, or even later, in case other references to the object with `id(u)` existed. The method `dd.cudd.BDD.decref` should not be called for "early" dereferencing. Instead, write `del u` as above. However, if the user decides to call any of the methods: - `dd.cudd.BDD.incref(u)` - `dd.cudd.BDD.decref(u)` then the user needs to ensure that `u._ref > 0` before each call to these methods, taking into account that: - `dd.cudd.BDD.incref(u)` increments `u._ref` - `dd.cudd.BDD.decref(u)` decrements `u._ref` and sets `u.node` to `NULL` when `u._ref` becomes `0`. The attribute `u._ref` is *not* the reference count of the BDD node in CUDD that the C attribute `u.node` points to. The value of `u._ref` is a lower bound on the reference count of the BDD node that `u.node` points to. This is a safe approach for accessing memory in CUDD. The following example demonstrates this approach. We start with: ```python from dd.cudd import BDD bdd = BDD() bdd.declare('x', 'y') u = bdd.add_expr(r'x /\ ~ y') w = u assert w is u ``` i.e., `u` and `w` are different Python variables that point to the *same* instance of `Function`. This `Function` instance points to a BDD node in CUDD. We will refer to this `Function` instance as "the object with `id(u)`". ```python v = bdd.add_expr(r'x /\ ~ y') assert v is not u ``` i.e., the Python variable `v` points to an instance of `Function` different from the `Function` instance that `u` points to. We will refer to the `Function` instance that `v` points to as "the object with `id(v)`". The object with `id(v)` and the object with `id(u)` point to the same BDD node in CUDD. The statement ```python bdd.decref(v, recursive=True) ``` decrements: - the reference count of the BDD node in CUDD that the object with `id(v)` points to - the lower bound `v._ref` - the reference counts of CUDD BDD nodes that are successors, recursively, when a node's reference count becomes 0. For more details read the docstring of the CUDD function `Cudd_RecursiveDeref`. Setting the parameter `recursive` to `True` here has no effect, because due to `u` the reference count of the CUDD BDD node corresponding to `v` remains positive after the decrement. But in general this is not the case, so `recursive=True` is then necessary, because afterwards it is impossible to dereference the successors of the CUDD BDD node that corresponds to `v`. The reason is described next. The object with `id(v)` *cannot* be used after this point, because the call to the method `decref` resulted in `v._ref == 0`, so it also set the pointer `v.node` to `NULL`. Setting `v.node` to `NULL` guards from further use of the object with `id(v)` to access CUDD BDD nodes. The object with `id(v)` should *not* be used after this point. In this specific example, if the method `decref` did not set `v.node` to `NULL`, then using `v` beyond this point would actually not have caused problems, because the CUDD BDD node's reference count is still positive (due to the increment when the object with `id(u)` was instantiated). But in general this is not the case. Also, after the attribute `v._ref` becomes `0`, there is no safe way for the object with `id(v)` to read the reference count of the CUDD BDD node that this object points to, even though that reference count is positive and the BDD node is still accessible via `u` and `w`. From the perspective of the object with `id(v)`, further access to that CUDD BDD node is unsafe. Had the method `decref` not set `u.node` to `NULL`, then if we had continued by doing: ```python bdd.decref(u, recursive=True) ``` then both variables `u` and `w` should *not* had been used any further. These variables refer to the same Python object, and `u._ref == 0` (thus `w._ref == 0`). So the same observations apply to `u` and `w` as for `v` above. """ __weakref__: object cdef public BDD bdd cdef DdManager *manager node: DdRef cdef public int _ref cdef init( self, node: DdRef, bdd: BDD): if node is NULL: raise ValueError( '`DdNode *node` is `NULL` pointer.') self.bdd = bdd self.manager = bdd.manager self.node = node self._ref = 1 # lower bound on # reference count # # Assumed invariant: # this instance participates in # computation only as long as # `self._ref > 0`. # The user is responsible for # implementing this invariant. Cudd_Ref(node) def __hash__( self ) -> int: return int(self) @property def _index( self ) -> _Nat: """Index of `self.node`.""" return Cudd_NodeReadIndex(self.node) @property def var( self ) -> ( _VariableName | None): """Variable at level where this node is. If node is constant, return `None`. """ if Cudd_IsConstant(self.node): return None return self.bdd._var_with_index[self._index] @property def level( self ) -> _Level: """Level where this node currently is.""" i = self._index return Cudd_ReadPerm(self.manager, i) @property def ref( self ) -> _Cardinality: """Reference count of node. Returns the sum of the reference count of this BDD root, and of the reference count of the root of the negated BDD. """ u: DdRef u = Cudd_Regular(self.node) return u.ref @property def low( self ) -> ''' Function | None ''': """Return "else" node as `Function`.""" u: DdRef if Cudd_IsConstant(self.node): return None u = Cudd_E(self.node) return wrap(self.bdd, u) @property def high( self ) -> ''' Function | None ''': """Return "then" node as `Function`.""" u: DdRef if Cudd_IsConstant(self.node): return None u = Cudd_T(self.node) return wrap(self.bdd, u) @property def negated( self ) -> _Yes: """`True` if this is a complemented edge. Returns `True` if `self` is a complemented edge. """ return Cudd_IsComplement(self.node) @property def support( self: BDD ) -> set[_VariableName]: """Return `set` of variables in support.""" return self.bdd.support(self) def __dealloc__( self ) -> None: # when changing this method, # update also the function # `_test_call_dealloc` below if self._ref < 0: raise AssertionError( "The lower bound `_ref` " "on the node's " 'reference count has ' f'value {self._ref}, ' 'which is unexpected and ' 'should never happen. ' 'Was the value of `_ref` ' 'changed from outside ' 'this instance?') assert self._ref >= 0, self._ref if self._ref == 0: return if self.node is NULL: raise AssertionError( 'The attribute `node` is ' 'a `NULL` pointer. ' 'This is unexpected and ' 'should never happen. ' 'Was the value of `_ref` ' 'changed from outside ' 'this instance?') # anticipate multiple calls to `__dealloc__` self._ref -= 1 # deref Cudd_RecursiveDeref( self.manager, self.node) # avoid future access # to deallocated memory self.node = NULL def __int__( self ) -> int: """Inverse of `BDD._add_int()`.""" return _ddref_to_int(self.node) def __repr__( self ) -> str: u: DdRef u = Cudd_Regular(self.node) return ( f'') def __str__( self ) -> str: return f'@{int(self)}' def __len__( self ) -> _Cardinality: return Cudd_DagSize(self.node) @property def dag_size( self ) -> _Cardinality: """Return number of BDD nodes. This is the number of BDD nodes that are reachable from this BDD reference, i.e., with `self` as root. """ return len(self) def __eq__( self: Function, other: _ty.Optional[Function] ) -> _Yes: if other is None: return False # guard against mixing managers if self.manager != other.manager: raise ValueError( '`self.manager != other.manager`') return self.node == other.node def __ne__( self: Function, other: _ty.Optional[Function] ) -> _Yes: if other is None: return True if self.manager != other.manager: raise ValueError( '`self.manager != other.manager`') return self.node != other.node def __le__( self: Function, other: Function ) -> _Yes: if self.manager != other.manager: raise ValueError( '`self.manager != other.manager`') leq = Cudd_bddLeq( self.manager, self.node, other.node) return (leq == 1) def __lt__( self: Function, other: Function ) -> _Yes: if self.manager != other.manager: raise ValueError( '`self.manager != other.manager`') leq = Cudd_bddLeq( self.manager, self.node, other.node) return ( self.node != other.node and leq == 1) def __ge__( self: Function, other: Function ) -> _Yes: if self.manager != other.manager: raise ValueError( '`self.manager != other.manager`') geq = Cudd_bddLeq( self.manager, other.node, self.node) return (geq == 1) def __gt__( self: Function, other: Function ) -> _Yes: if self.manager != other.manager: raise ValueError( '`self.manager != other.manager`') geq = Cudd_bddLeq( self.manager, other.node, self.node) return ( self.node != other.node and geq == 1) def __invert__( self ) -> Function: r: DdRef r = Cudd_Not(self.node) return wrap(self.bdd, r) def __and__( self: Function, other: Function ) -> Function: if self.manager != other.manager: raise ValueError( '`self.manager != other.manager`') r = Cudd_bddAnd( self.manager, self.node, other.node) return wrap(self.bdd, r) def __or__( self: Function, other: Function ) -> Function: if self.manager != other.manager: raise ValueError( '`self.manager != other.manager`') r = Cudd_bddOr( self.manager, self.node, other.node) return wrap(self.bdd, r) def __xor__( self: Function, other: Function ) -> Function: if self.manager != other.manager: raise ValueError( '`self.manager != other.manager`') r = Cudd_bddXor( self.manager, self.node, other.node) return wrap(self.bdd, r) def implies( self: Function, other: Function ) -> Function: if self.manager != other.manager: raise ValueError( '`self.manager != other.manager`') r = Cudd_bddIte( self.manager, self.node, other.node, Cudd_ReadOne(self.manager)) return wrap(self.bdd, r) def equiv( self: Function, other: Function ) -> Function: if self.manager != other.manager: raise ValueError( '`self.manager != other.manager`') r = Cudd_bddIte( self.manager, self.node, other.node, Cudd_Not(other.node)) return wrap(self.bdd, r) def let( self: Function, **definitions: _VariableName | python_bool | Function ) -> Function: return self.bdd.let(definitions, self) def exist( self: Function, *variables: _VariableName ) -> Function: return self.bdd.exist(variables, self) def forall( self: Function, *variables: _VariableName ) -> Function: return self.bdd.forall(variables, self) def pick( self: Function, care_vars: _abc.Set[ _VariableName] | None=None ) -> _Assignment: return self.bdd.pick(self, care_vars) def count( self: Function, nvars: _Cardinality | None=None ) -> _Cardinality: return self.bdd.count(self, nvars) cdef _ddref_to_int( node: DdRef): """Convert node pointer to numeric index. Inverse of `_int_to_ddref()`. """ if sizeof(stdint.uintptr_t) != sizeof(DdRef): raise AssertionError( 'mismatch of sizes') index = node # 0, 1 used to represent TRUE and FALSE # in syntax of expressions if 0 <= index: index += 2 if index in (0, 1): raise AssertionError(index) return index cdef DdRef _int_to_ddref( index: int): """Convert numeric index to node pointer. Inverse of `_ddref_to_int()`. """ if index in (0, 1): raise ValueError(index) if 2 <= index: index -= 2 u: DdRef = index return u """Tests and test wrappers for C functions.""" cpdef _test_incref(): bdd = BDD() f: Function f = bdd.true i = f.ref bdd._incref(f.node) j = f.ref if j != i + 1: raise AssertionError((j, i)) # avoid errors in `BDD.__dealloc__` bdd._decref(f.node, recursive=True) del f cpdef _test_decref(): bdd = BDD() f: Function f = bdd.true i = f.ref if i != 2: raise AssertionError(i) bdd._incref(f.node) i = f.ref if i != 3: raise AssertionError(i) bdd._decref(f.node, recursive=True) j = f.ref if j != i - 1: raise AssertionError((j, i)) del f cpdef _test_dict_to_cube_array(): cdef int *x n = 3 x = PyMem_Malloc( n * sizeof(int)) index_of_var = dict(x=0, y=1, z=2) d = dict(y=True, z=False) _dict_to_cube_array( d, x, index_of_var) r = [j for j in x[:n]] r_ = [2, 1, 0] if r != r_: raise AssertionError((r, r_)) PyMem_Free(x) cpdef _test_cube_array_to_dict(): cdef int *x n = 3 x = PyMem_Malloc( n * sizeof(int)) x[0] = 2 x[1] = 1 x[2] = 0 index_of_var = dict(x=0, y=1, z=2) d = _cube_array_to_dict( x, index_of_var) d_ = dict(y=True, z=False) if d != d_: raise AssertionError((d, d_)) PyMem_Free(x) cpdef _test_call_dealloc( u: Function): """Duplicates the code of `Function.__dealloc__`. The main purpose of this function is to test the exceptions raised in the method `Function.__dealloc__`. Exceptions raised in `__dealloc__` are ignored (they become messages), and it seems impossible to call `__dealloc__` directly (unlike `__del__`), so there is no way to assert what exceptions are raised in `__dealloc__`. This function is the closest thing to testing those exceptions. """ self = u # the code of `Function.__dealloc__` follows: if self._ref < 0: raise AssertionError( "The lower bound `_ref` on the node's " 'reference count has value {self._ref}, ' 'which is unexpected and should never happen. ' 'Was the value of `_ref` changed from outside ' 'this instance?') assert self._ref >= 0, self._ref if self._ref == 0: return if self.node is NULL: raise AssertionError( 'The attribute `node` is a `NULL` pointer. ' 'This is unexpected and should never happen. ' 'Was the value of `_ref` changed from outside ' 'this instance?') # anticipate multiple calls to `__dealloc__` self._ref -= 1 # deref Cudd_RecursiveDeref(self.manager, self.node) # avoid future access to deallocated memory self.node = NULL ================================================ FILE: dd/cudd_zdd.pyx ================================================ """Cython interface to ZDD implementation in CUDD. ZDDs are represented without complemented edges in CUDD (unlike BDDs). So rectifying a node to "regular" is unnecessary. Variable `__version__` equals CUDD's version string. Reference ========= Fabio Somenzi "CUDD: CU Decision Diagram Package" University of Colorado at Boulder v2.5.1, 2015 """ # Copyright 2015-2020 by California Institute of Technology # All rights reserved. Licensed under 3-clause BSD. # # # Copyright (c) 1995-2015, Regents of the University of Colorado # # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # Neither the name of the University of Colorado nor the names of its # contributors may be used to endorse or promote products derived from # this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # import collections.abc as _abc import itertools as _itr import logging import textwrap as _tw import typing as _ty import warnings from cpython cimport bool as python_bool from cpython.mem cimport PyMem_Malloc, PyMem_Free import cython cimport libc.stdint as stdint from libc.stdio cimport FILE, fdopen, fopen, fclose from libcpp cimport bool import dd._abc as _dd_abc from dd import _copy from dd import _parser from dd import _utils from dd import bdd as _bdd ctypedef cython.int _c_level ctypedef cython.int _c_int _Yes: _ty.TypeAlias = python_bool _Nat: _ty.TypeAlias = _dd_abc.Nat _Cardinality: _ty.TypeAlias = _dd_abc.Cardinality _NumberOfBytes: _ty.TypeAlias = _dd_abc.NumberOfBytes _VariableName: _ty.TypeAlias = _dd_abc.VariableName _Level: _ty.TypeAlias = _dd_abc.Level _VariableLevels: _ty.TypeAlias = _dd_abc.VariableLevels _Assignment: _ty.TypeAlias = _dd_abc.Assignment _Renaming: _ty.TypeAlias = _dd_abc.Renaming _Formula: _ty.TypeAlias = _dd_abc.Formula cdef extern from 'cuddInt.h': char* CUDD_VERSION int CUDD_CONST_INDEX # subtable (for a level) struct DdSubtable: unsigned int slots unsigned int keys # manager struct DdManager: DdSubtable *subtables unsigned int keys unsigned int dead double cachecollisions double cacheinserts double cachedeletions DdNode **univ int reordered # local hash tables ctypedef stdint.intptr_t ptrint struct DdHashItem: DdHashItem *next DdNode *value struct DdHashTable: DdHashItem **bucket DdHashItem **memoryList unsigned int numBuckets DdManager *manager DdHashTable * cuddHashTableInit( DdManager *manager, unsigned int keySize, unsigned int initSize) void cuddHashTableQuit( DdHashTable *hash) int cuddHashTableInsert1( DdHashTable *hash, DdNode *f, DdNode *value, ptrint count) DdNode * cuddHashTableLookup1( DdHashTable *hash, DdNode *f) # cache DdNode * cuddCacheLookup2Zdd( DdManager *table, DdNode * (*)( DdManager *, DdNode *, DdNode *), DdNode *f, DdNode *g) void cuddCacheInsert2( DdManager *table, DdNode * (*)( DdManager *, DdNode *, DdNode *), DdNode *f, DdNode *g, DdNode *data) # node elements DdNode *cuddUniqueInter( DdManager *unique, int index, DdNode *T, DdNode *E) DdNode * cuddUniqueInterZdd( DdManager *unique, int index, DdNode *T, DdNode *E) bool cuddIsConstant( DdNode *u) DdNode *DD_ZERO( DdManager *mgr) DdNode *DD_ONE( DdManager *mgr) DdNode *cuddT( DdNode *u) # top cofactors DdNode *cuddE( DdNode *u) # BDD node elements DdNode *Cudd_Not( DdNode *dd) DdNode *Cudd_Regular( DdNode *u) bool Cudd_IsComplement( DdNode *u) # reference counting void cuddRef( DdNode *u) void cuddDeref( DdNode *u) # recursive ITE DdNode * cuddZddIte( DdManager *dd, DdNode *f, DdNode *g, DdNode *h) # realignment int Cudd_zddRealignmentEnabled( DdManager *unique) void Cudd_zddRealignEnable( DdManager *unique) void Cudd_zddRealignDisable( DdManager *unique) int Cudd_bddRealignmentEnabled( DdManager *unique) void Cudd_bddRealignEnable( DdManager *unique) void Cudd_bddRealignDisable( DdManager *unique) cdef extern from 'cudd.h': # node ctypedef unsigned int DdHalfWord struct DdNode: DdHalfWord index DdHalfWord ref DdNode *next # manager DdManager *Cudd_Init( unsigned int numVars, unsigned int numVarsZ, unsigned int numSlots, unsigned int cacheSize, size_t maxMemory) # generator struct DdGen # variables DdNode *Cudd_zddIthVar( DdManager *dd, int index) DdNode *Cudd_bddIthVar( DdManager *dd, int index) DdNode *Cudd_zddSupport( DdManager *dd, DdNode *f) int Cudd_ReadPermZdd( DdManager *dd, int index) int Cudd_ReadInvPermZdd( DdManager *dd, int level) unsigned int Cudd_NodeReadIndex( DdNode *u) # cofactors (of any given `var`, not only top) DdNode *Cudd_zddSubset1( DdManager *dd, DdNode *P, int var) DdNode *Cudd_zddSubset0( DdManager *dd, DdNode *P, int var) # conversions between BDDs and ZDDs int Cudd_zddVarsFromBddVars( DdManager *dd, int multiplicity) DdNode *Cudd_zddPortFromBdd( DdManager *dd, DdNode *B) DdNode *Cudd_zddPortToBdd( DdManager *dd, DdNode *f) # propositional operators DdNode *Cudd_zddIte( DdManager *dd, DdNode *f, DdNode *g, DdNode *h) DdNode *Cudd_zddUnion( DdManager *dd, DdNode *P, DdNode *Q) DdNode *Cudd_zddIntersect( DdManager *dd, DdNode *P, DdNode *Q) DdNode *Cudd_zddDiff( DdManager *dd, DdNode *P, DdNode *Q) # constants DdNode *Cudd_ReadZddOne( DdManager *dd, int i) # `i` is the index of the topmost variable DdNode *Cudd_ReadZero(DdManager *dd) # counting int Cudd_zddDagSize( DdNode *p_node) double Cudd_zddCountMinterm( DdManager *zdd, DdNode *node, int path) int Cudd_zddCount( DdManager *zdd, DdNode *P) int Cudd_BddToCubeArray( DdManager *dd, DdNode *cube, int *array) # pick DdGen *Cudd_zddFirstPath( DdManager *zdd, DdNode *f, int **path) int Cudd_zddNextPath( DdGen *gen, int **path) int Cudd_IsGenEmpty( DdGen *gen) int Cudd_GenFree( DdGen *gen) # info int Cudd_PrintInfo( DdManager *dd, FILE *fp) int Cudd_ReadZddSize( DdManager *dd) long Cudd_zddReadNodeCount( DdManager *dd) long Cudd_ReadPeakNodeCount( DdManager *dd) int Cudd_ReadPeakLiveNodeCount( DdManager *dd) size_t Cudd_ReadMemoryInUse( DdManager *dd) unsigned int Cudd_ReadSlots( DdManager *dd) double Cudd_ReadUsedSlots( DdManager *dd) double Cudd_ExpectedUsedSlots( DdManager *dd) unsigned int Cudd_ReadCacheSlots( DdManager *dd) double Cudd_ReadCacheUsedSlots( DdManager *dd) double Cudd_ReadCacheLookUps( DdManager *dd) double Cudd_ReadCacheHits( DdManager *dd) # reordering ctypedef enum Cudd_ReorderingType: pass void Cudd_AutodynEnableZdd( DdManager *unique, Cudd_ReorderingType method) void Cudd_AutodynDisableZdd( DdManager *unique) int Cudd_ReorderingStatusZdd( DdManager *unique, Cudd_ReorderingType *method) int Cudd_zddReduceHeap( DdManager *table, Cudd_ReorderingType heuristic, int minsize) int Cudd_zddShuffleHeap( DdManager *table, int *permutation) void Cudd_SetSiftMaxSwap( DdManager *dd, int sms) int Cudd_ReadSiftMaxSwap( DdManager *dd) void Cudd_SetSiftMaxVar( DdManager *dd, int smv) int Cudd_ReadSiftMaxVar( DdManager *dd) # The function `Cudd_zddReduceHeap` increments the # counter `dd->reorderings`. The function `Cudd_ReadReorderings` # reads this counter. unsigned int Cudd_ReadReorderings( DdManager *dd) # The function `Cudd_zddReduceHeap` adds to the attribute # `dd->reordTime`. The function `Cudd_ReadReorderingTime` # reads this attribute. long Cudd_ReadReorderingTime( DdManager *dd) # manager config size_t Cudd_ReadMaxMemory( DdManager *dd) size_t Cudd_SetMaxMemory( DdManager *dd, size_t maxMemory) unsigned int Cudd_ReadMaxCacheHard( DdManager *dd) unsigned int Cudd_ReadMaxCache( DdManager *dd) void Cudd_SetMaxCacheHard( DdManager *dd, unsigned int mc) double Cudd_ReadMaxGrowth( DdManager *dd) void Cudd_SetMaxGrowth( DdManager *dd, double mg) unsigned int Cudd_ReadMinHit( DdManager *dd) void Cudd_SetMinHit( DdManager *dd, unsigned int hr) void Cudd_EnableGarbageCollection( DdManager *dd) void Cudd_DisableGarbageCollection( DdManager *dd) int Cudd_GarbageCollectionEnabled( DdManager * dd) unsigned int Cudd_ReadLooseUpTo( DdManager *dd) void Cudd_SetLooseUpTo( DdManager *dd, unsigned int lut) # reference counting void Cudd_Ref( DdNode *n) void Cudd_Deref( DdNode *n) void Cudd_RecursiveDerefZdd( DdManager *table, DdNode *n) int Cudd_CheckZeroRef( DdManager *manager) # checks int Cudd_DebugCheck( DdManager *table) void Cudd_Quit( DdManager *unique) # manager config void Cudd_EnableGarbageCollection( DdManager *dd) void Cudd_DisableGarbageCollection( DdManager *dd) int Cudd_GarbageCollectionEnabled( DdManager * dd) # BDD functions DdNode *Cudd_bddIte( DdManager *dd, DdNode *f, DdNode *g, DdNode *h) cdef extern from 'util.h': void FREE(void *ptr) # node elements ctypedef DdNode *DdRef cdef CUDD_UNIQUE_SLOTS = 2**8 cdef CUDD_CACHE_SLOTS = 2**18 cdef CUDD_REORDER_SIFT = 4 cdef CUDD_OUT_OF_MEM = -1 cdef MAX_CACHE = - 1 # entries __version__ = CUDD_VERSION.decode('utf-8') # 2**30 = 1 GiB (gibibyte, read ISO/IEC 80000) DEFAULT_MEMORY = 1 * 2**30 logger = logging.getLogger(__name__) class CouldNotCreateNode(Exception): pass cdef class ZDD: """Wrapper of CUDD manager. Interface similar to `dd._abc.BDD` and `dd.cudd.BDD`. Variable names are strings. Attributes: - `vars`: `set` of bit names as `str`ings """ cdef DdManager *manager cdef public object vars cdef public object _index_of_var cdef public object _var_with_index def __cinit__( self, memory_estimate: _NumberOfBytes | None=None, initial_cache_size: _Cardinality | None=None, *arg, **kw ) -> None: """Initialize ZDD manager. @param memory_estimate: maximum allowed memory, in bytes. """ self.manager = NULL # prepare for `__dealloc__` total_memory = _utils.total_memory() default_memory = DEFAULT_MEMORY if memory_estimate is None: memory_estimate = default_memory if total_memory is None: pass elif memory_estimate >= total_memory: memory_example = round(total_memory / 2) msg = ( 'Error in `dd.cudd_zdd.ZDD`: ' f'total physical memory is {total_memory} bytes, ' f'but requested {memory_estimate} bytes. ' 'To avoid this error, pass an amount of memory, ' f'for example: `ZDD({memory_example})`.') # Motivation is described in # comments inside `dd.cudd.BDD.__cinit__`. print(msg) raise ValueError(msg) if initial_cache_size is None: initial_cache_size = CUDD_CACHE_SLOTS initial_subtable_size = CUDD_UNIQUE_SLOTS initial_n_vars_bdd = 0 initial_n_vars_zdd = 0 mgr = Cudd_Init( initial_n_vars_bdd, initial_n_vars_zdd, initial_subtable_size, initial_cache_size, memory_estimate) if mgr is NULL: raise RuntimeError( 'failed to initialize CUDD DdManager') self.manager = mgr def __init__( self, memory_estimate: _NumberOfBytes | None=None, initial_cache_size: _Cardinality | None=None ) -> None: logger.info(f'Using CUDD v{__version__}') self.configure(reordering=True, max_cache_hard=MAX_CACHE) self.vars = set() self._index_of_var = dict() # map: str -> unique fixed int self._var_with_index = dict() def __dealloc__( self ) -> None: if self.manager is NULL: raise RuntimeError( '`self.manager` is `NULL`, which suggests that ' 'an exception was raised inside the method ' '`dd.cudd_zdd.ZDD.__cinit__`.') n = Cudd_CheckZeroRef(self.manager) if n != 0: raise AssertionError( f'Still {n} nodes ' 'referenced upon shutdown.') Cudd_Quit(self.manager) def __eq__( self: ZDD, other: _ty.Optional[ZDD] ) -> _Yes: """Return `True` if `other` has same manager. If `other is None`, then return `False`. """ if other is None: return False return self.manager == other.manager def __ne__( self: ZDD, other: _ty.Optional[ZDD] ) -> _Yes: """Return `True` if `other` has different manager. If `other is None`, then return `True`. """ if other is None: return True return self.manager != other.manager def __len__( self ) -> _Cardinality: """Return number of nodes with non-zero references.""" return Cudd_CheckZeroRef(self.manager) def __contains__( self, u: Function ) -> _Yes: """Return `True` if `u.node` in `self.manager`.""" if u.manager != self.manager: raise ValueError( 'undefined containment, because ' '`u.manager != self.manager`') try: Cudd_NodeReadIndex(u.node) return True except: return False # This method is similar to # the method `dd.cudd.BDD.__str__`. def __str__( self ) -> str: d = self.statistics() s = ( 'Zero-omitted binary decision diagram (CUDD wrapper).\n' '\t {n} live nodes now\n' '\t {peak} live nodes at peak\n' '\t {n_vars} ZDD variables\n' '\t {mem:10.1f} bytes in use\n' '\t {reorder_time:10.1f} sec spent reordering\n' '\t {n_reorderings} reorderings\n').format( n=d['n_nodes'], peak=d['peak_live_nodes'], n_vars=d['n_vars'], reorder_time=d['reordering_time'], n_reorderings=d['n_reorderings'], mem=d['mem']) return s # This method is similar to # the method `dd.cudd.BDD.statistics`. def statistics( self: ZDD, exact_node_count: _Yes=False ) -> dict[ str, _ty.Any]: """Return `dict` with CUDD node counts and times. For details read the docstring of the method `dd.cudd.BDD.statistics`. """ warnings.warn( "Changed in `dd` version 0.5.7: " "In the `dict` returned by the method " '`dd.cudd_zdd.ZDD.statistics`, ' "the value of the key `'mem'` " "has changed to bytes (from 10**6 bytes).", UserWarning) cdef DdManager *mgr mgr = self.manager n_vars = Cudd_ReadZddSize(mgr) # nodes if exact_node_count: n_nodes = Cudd_zddReadNodeCount(mgr) else: n_nodes = mgr.keys - mgr.dead peak_nodes = Cudd_ReadPeakNodeCount(mgr) peak_live_nodes = Cudd_ReadPeakLiveNodeCount(mgr) # reordering t = Cudd_ReadReorderingTime(mgr) reordering_time = t / 1000.0 n_reorderings = Cudd_ReadReorderings(mgr) # memory m = Cudd_ReadMemoryInUse(mgr) mem = float(m) # unique table unique_size = Cudd_ReadSlots(mgr) unique_used_fraction = Cudd_ReadUsedSlots(mgr) expected_unique_fraction = Cudd_ExpectedUsedSlots(mgr) # cache cache_size = Cudd_ReadCacheSlots(mgr) cache_used_fraction = Cudd_ReadCacheUsedSlots(mgr) cache_lookups = Cudd_ReadCacheLookUps(mgr) cache_hits = Cudd_ReadCacheHits(mgr) cache_insertions = mgr.cacheinserts cache_collisions = mgr.cachecollisions cache_deletions = mgr.cachedeletions d = dict( n_vars=n_vars, n_nodes=n_nodes, peak_nodes=peak_nodes, peak_live_nodes=peak_live_nodes, reordering_time=reordering_time, n_reorderings=n_reorderings, mem=mem, unique_size=unique_size, unique_used_fraction=unique_used_fraction, expected_unique_used_fraction=expected_unique_fraction, cache_size=cache_size, cache_used_fraction=cache_used_fraction, cache_lookups=cache_lookups, cache_hits=cache_hits, cache_insertions=cache_insertions, cache_collisions=cache_collisions, cache_deletions=cache_deletions) return d # This method is similar to the method # `dd.cudd.BDD.configure`. def configure( self, **kw ) -> dict[ str, _ty.Any]: """Read and apply parameter values. For details read the docstring of the method `dd.cudd.BDD.configure`. """ cdef int method cdef DdManager *mgr mgr = self.manager # read reordering = Cudd_ReorderingStatusZdd( self.manager, &method) garbage_collection = Cudd_GarbageCollectionEnabled(self.manager) max_memory = Cudd_ReadMaxMemory(mgr) loose_up_to = Cudd_ReadLooseUpTo(mgr) max_cache_soft = Cudd_ReadMaxCache(mgr) max_cache_hard = Cudd_ReadMaxCacheHard(mgr) min_hit = Cudd_ReadMinHit(mgr) max_growth = Cudd_ReadMaxGrowth(mgr) max_swaps = Cudd_ReadSiftMaxSwap(mgr) max_vars = Cudd_ReadSiftMaxVar(mgr) d = dict( reordering= True if reordering == 1 else False, garbage_collection= True if garbage_collection == 1 else False, max_memory=max_memory, loose_up_to=loose_up_to, max_cache_soft=max_cache_soft, max_cache_hard=max_cache_hard, min_hit=min_hit, max_growth=max_growth, max_swaps=max_swaps, max_vars=max_vars) # set for k, v in kw.items(): if k == 'reordering': if v: self._enable_reordering() else: self._disable_reordering() elif k == 'garbage_collection': if v: Cudd_EnableGarbageCollection(self.manager) else: Cudd_DisableGarbageCollection(self.manager) elif k == 'max_memory': Cudd_SetMaxMemory(mgr, v) elif k == 'loose_up_to': Cudd_SetLooseUpTo(mgr, v) elif k == 'max_cache_hard': Cudd_SetMaxCacheHard(mgr, v) elif k == 'min_hit': Cudd_SetMinHit(mgr, v) elif k == 'max_growth': Cudd_SetMaxGrowth(mgr, v) elif k == 'max_swaps': Cudd_SetSiftMaxSwap(mgr, v) elif k == 'max_vars': Cudd_SetSiftMaxVar(mgr, v) elif k == 'max_cache_soft': logger.warning('"max_cache_soft" not settable.') else: raise ValueError( f'Unknown parameter "{k}"') return d cpdef tuple succ( self, u: Function): """Return `(level, low, high)` for `u`.""" if u.manager != self.manager: raise ValueError('`u.manager != self.manager`') i = u.level v = u.low w = u.high if v is not None and i >= v.level: raise AssertionError('v.level') if w is not None and i >= w.level: raise AssertionError('w.level') return i, v, w cpdef incref( self, u: Function): """Increment the reference count of `u`. For details read the docstring of the method `dd.cudd.BDD.incref`. """ if u.node is NULL: raise RuntimeError('`u.node` is `NULL` pointer.') if u._ref <= 0: _utils.raise_runtimerror_about_ref_count( u._ref, 'method `dd.cudd_zdd.ZDD.incref`', '`dd.cudd_zdd.Function`') assert u._ref > 0, u._ref u._ref += 1 self._incref(u.node) cpdef decref( self, u: Function, recursive: _Yes=False, _direct: _Yes=False): """Decrement the reference count of `u`. For details read the docstring of the method `dd.cudd.BDD.decref`. @param recursive: if `True`, then call `Cudd_RecursiveDerefZdd`, else call `Cudd_Deref` @param _direct: use this parameter only after reading the source code of the Cython file `dd/cudd_zdd.pyx`. When `_direct == True`, some of the above description does not apply. """ if u.node is NULL: raise RuntimeError('`u.node` is `NULL` pointer.') # bypass checks and leave `u._ref` unchanged, # directly call `_decref` if _direct: self._decref(u.node, recursive) return if u._ref <= 0: _utils.raise_runtimerror_about_ref_count( u._ref, 'method `dd.cudd_zdd.ZDD.decref`', '`dd.cudd_zdd.Function`') assert u._ref > 0, u._ref u._ref -= 1 self._decref(u.node, recursive) if u._ref == 0: u.node = NULL cdef _incref( self, u: DdRef): Cudd_Ref(u) cdef _decref( self, u: DdRef, recursive: _Yes=False): if recursive: Cudd_RecursiveDerefZdd(self.manager, u) else: Cudd_Deref(u) def declare( self, *variables: _VariableName ) -> None: """Add names in `variables` to `self.vars`.""" for var in variables: self.add_var(var) cpdef int add_var( self, var: _VariableName, index: _Nat | None=None): """Return index of variable named `var`. If a variable named `var` exists, then assert that it has `index`. Otherwise, create a variable named `var` with `index` (if given). If no reordering has yet occurred, then the returned index equals the level, provided `add_var` has been used so far. """ # var already exists ? j = self._index_of_var.get(var) if j is not None: if index is not None and j != index: raise AssertionError(j, index) return j # new var if index is None: j = len(self._index_of_var) else: j = index u = Cudd_zddIthVar(self.manager, j) # ref and recursive deref, # in order to cancel refs out Cudd_Ref(u) Cudd_RecursiveDerefZdd(self.manager, u) if u is NULL: raise RuntimeError( f'failed to add var "{var}"') self._add_var(var, j) return j cdef _add_var( self, var: _VariableName, index: _Nat): """Add to `self` a *new* variable named `var`.""" if var in self.vars: raise ValueError( f'existing variable: {var}') if var in self._index_of_var: raise ValueError( 'variable already has ' f'index: {self._index_of_var[var]}') if index in self._var_with_index: raise ValueError( 'index already corresponds ' 'to a variable: ' f'{self._var_with_index[index]}') self.vars.add(var) self._index_of_var[var] = index self._var_with_index[index] = var if (len(self._index_of_var) != len(self._var_with_index)): raise AssertionError( 'the attributes `_index_of_var` and ' '`_var_with_index` have different length') cpdef Function var( self, var: _VariableName): """Return node for variable named `var`.""" if var not in self._index_of_var: raise ValueError( f'undeclared variable "{var}", ' 'the declared variables are:\n' f'{self._index_of_var}') j = self._index_of_var[var] r = _ith_var(var, self) return r # CUDD implementation of `var` cpdef Function _var_cudd( self, var: _VariableName): """Return node for variable named `var`.""" if var not in self._index_of_var: raise ValueError( f'undeclared variable "{var}", ' 'the declared variables are:\n' f'{self._index_of_var}') j = self._index_of_var[var] r = Cudd_zddIthVar(self.manager, j) return wrap(self, r) def _add_bdd_var( self, j: _Nat ) -> None: """Declare a BDD variable with index `j`.""" Cudd_bddIthVar(self.manager, j) def var_at_level( self, level: _Level ) -> _VariableName: """Return name of variable at `level`. Raise `ValueError` if `level` is not the level of any variable declared in `self.vars`. """ j = Cudd_ReadInvPermZdd(self.manager, level) if (j == -1 or j == CUDD_CONST_INDEX or j not in self._var_with_index): raise ValueError(_tw.dedent(f''' No declared variable has level: {level}. {_utils.var_counts(self)} ''')) var = self._var_with_index[j] return var def level_of_var( self, var: _VariableName ) -> _Level: """Return level of variable named `var`. Raise `ValueError` if `var` is not a variable in `self.vars`. """ if var not in self._index_of_var: raise ValueError( f'undeclared variable "{var}", ' 'the declared variables are:\n' f'{self._index_of_var}') j = self._index_of_var[var] level = Cudd_ReadPermZdd(self.manager, j) if level == -1: raise AssertionError( f'index {j} out of bounds') return level @property def var_levels( self ) -> _VariableLevels: """Return `dict` that maps variables to levels.""" return {var: self.level_of_var(var) for var in self.vars} def _index_at_level( self, level: _Level ) -> ( int | None): """Return index of CUDD variable at `level`. The presence of such an index does not mean that a variable has been declared for that index. Declaring variables with indices over a set of noncontiguous integers creates these intermediate indices. For example: ```python import dd.cudd_zdd as _zdd import pytest zdd = _zdd.ZDD() zdd.add_var('x', 1) assert zdd.level_of_var('x') == 1 # no `ZDD` declared variable at level 0 # CUDD does return an index for level 0 with pytest.raises(ValueError): zdd.level_of_var(0) # no CUDD variable at level 2 with pytest.raises(ValueError): zdd.level_of_var(2) ``` The usefulness of this method is during recursion over BDDs. If this method returns `None`, then this means that `level` is below the bottommost CUDD variable, therefore also below the bottommost `dd.cudd.ZDD` variable. So a return value of `None` means that we have reached the constant nodes (even though it can be `level > CUDD_CONST_INDEX`). Checking `len(zdd.vars) == level` is not equivalent, for example: ```python import dd.cudd_zdd as _zdd zdd = _zdd.ZDD() zdd.add_var('x', 2) n_declared_vars = len(zdd.vars) max_var_level = max( zdd.level_of_var(var) for var in zdd.vars) n_cudd_vars = zdd._number_of_cudd_vars() print(f''' {n_vars = } {max_var_level = } {n_cudd_vars = } ''') ``` So if the recursion stopped at level `n_vars`, it would not reach the nodes at `max_var_level`. Any computation with noncontiguous levels of declared variables is equivalent to a computation with contiguous levels. @param level: level >= 0 for which the corresponding CUDD variable index will be returned, if it exists """ # `Cudd_ReadInvPermZdd()`: # - returns `CUDD_CONST_INDEX` if # `level == CUDD_CONST_INDEX` # - returns `invperm[self.manager.level]` if # `0 <= level < self.manager.sizeZ` # - otherwise returns `-1` j = Cudd_ReadInvPermZdd(self.manager, level) if j == -1: return None if j < 0: raise RuntimeError( f'unexpected value: {j}, returned from ' 'CUDD function `Cudd_ReadInvPermZdd()`') return j cpdef python_bool _gt_var_levels( self, level: _Level): """Return `True` if `level` > any variable level. Raise `ValueError` if `level < 0`. Similar to how `Cudd_ReadInvPermZdd()` works. Note that the constant nodes are below all variable levels: ```python import dd.cudd_zdd zdd = dd.cudd_zdd.ZDD() assert zdd._gt_var_levels(zdd.false.level) ``` Read also `_index_at_level()` @param level: >= 0 @return: `True` if any CUDD variable has level < of given `level` """ if level < 0: raise ValueError( f'requires `level >= 0`, got: {level}') n_cudd_vars = Cudd_ReadZddSize(self.manager) if 0 <= n_cudd_vars <= CUDD_CONST_INDEX: return level >= n_cudd_vars raise RuntimeError(_tw.dedent(f''' Unexpected value: {n_cudd_vars} returned from CUDD function `Cudd_ReadZddSize()` (0 <= expected <= {CUDD_CONST_INDEX} = CUDD_CONST_INDEX) ''')) cpdef int _number_of_cudd_vars( self): """Return number of CUDD indices. Can be `> len(self.vars)`, because CUDD creates variable indices also for levels in between those of variables declared using `ZDD.declare()`. Read also `_index_at_level()`. @rtype: `int` >= 0 """ n_cudd_vars = Cudd_ReadZddSize(self.manager) if 0 <= n_cudd_vars <= CUDD_CONST_INDEX: return n_cudd_vars raise RuntimeError(_tw.dedent(f''' Unexpected value: {n_cudd_vars} returned from CUDD function `Cudd_ReadZddSize()` (0 <= expected <= {CUDD_CONST_INDEX} = CUDD_CONST_INDEX) ''')) def reorder( self, var_order: _VariableLevels | None=None ) -> None: """Reorder variables to `var_order`. If `var_order` is `None`, then invoke sifting. """ if var_order is None: Cudd_zddReduceHeap(self.manager, CUDD_REORDER_SIFT, 1) return n = len(var_order) if n != len(self.vars): raise ValueError( 'Mismatch of variable numbers:\n' 'the number of declared variables is: ' f'{len(self.vars)}\n' f'new variable order has length: {n}') cdef int *p p = PyMem_Malloc(n * sizeof(int)) for var, level in var_order.items(): index = self._index_of_var[var] p[level] = index try: r = Cudd_zddShuffleHeap(self.manager, p) finally: PyMem_Free(p) if r != 1: raise RuntimeError('Failed to reorder.') def _enable_reordering( self ) -> None: """Enable dynamic reordering of ZDDs.""" Cudd_AutodynEnableZdd(self.manager, CUDD_REORDER_SIFT) def _disable_reordering( self ) -> None: """Disable dynamic reordering of ZDDs.""" Cudd_AutodynDisableZdd(self.manager) cpdef set support( self, u: Function): """Return `set` of variables that `u` depends on. These are the variables that the Boolean function represented by the ZDD with root `u` depends on. """ if self.manager != u.manager: raise ValueError('`u.manager != self.manager`') return _c_support(u) cpdef set _support_py( self, u: Function): # Python implementation if self.manager != u.manager: raise ValueError('`u.manager != self.manager`') visited = set() support = set() level = 0 self._support(level, u, support, visited) return support cdef _support( self, level: _c_level, u: Function, support: set[_VariableName], visited: set[Function]): """Recurse to compute the support of `u`.""" # terminal ? if u == self.false or self._gt_var_levels(level): return if u in visited: return var = self.var_at_level(level) u_level, v, w = self.succ(u) if level > u_level: raise ValueError(level, u_level) if level < u_level: support.add(var) self._support(level + 1, u, support, visited) elif v == w: self._support(level + 1, v, support, visited) else: support.add(var) self._support(level + 1, v, support, visited) self._support(level + 1, w, support, visited) visited.add(u) cpdef set _support_cudd( self, f: Function): """Return `set` of variables that node `f` depends on.""" if self.manager != f.manager: raise ValueError('`f.manager != self.manager`') r: DdRef r = Cudd_zddSupport(self.manager, f.node) f = wrap(self, r) supp = self._cube_to_dict(f) # constant ? if not supp: return set() # must be positive unate if set(supp.values()) != {True}: raise AssertionError(supp) return set(supp) def _copy_bdd_vars( self, bdd ) -> None: """Copy BDD to ZDD variables.""" Cudd_zddVarsFromBddVars(self.manager, 1) def _bdd_to_zdd( self, u: Function ) -> Function: """Copy BDD `u` to a ZDD in `self`. @param u: node in a `dd.cudd.BDD` manager @type u: `dd.cudd.Function` """ r: DdRef bdd = u.bdd u_ = bdd.copy(u, self) r = u_.node r = Cudd_zddPortFromBdd(self.manager, r) return wrap(self, r) def copy( self, u: Function, other): """Transfer ZDD with root `u` to `other`. @param other: `ZDD` or `BDD` manager @type other: | `dd.cudd_zdd.ZDD` | `dd.cudd.BDD` | `dd.autoref.BDD` @rtype: | `dd.cudd_zdd.Function` | `dd.cudd.Function` | `dd.autoref.Function` """ return _copy.copy_zdd(u, other) cpdef Function let( self, definitions: _Renaming | _Assignment | dict[_VariableName, Function], u: Function): """Replace variables with `definitions` in `u`. @param definitions: maps variable names to either: - Boolean values, or - variable names, or - ZDD nodes @rtype: `Function` """ if self.manager != u.manager: raise ValueError('`u.manager != self.manager`') d = definitions if not d: logger.warning( 'Call to `ZDD.let` with no effect, ' 'because the dictionary `definitions` ' 'is empty.') return u var = next(iter(d)) value = d[var] if isinstance(value, python_bool): return self._cofactor_root(u, d) elif isinstance(value, Function): # return self._compose_root(u, d) return _c_compose(u, d) try: value + 's' except TypeError: raise ValueError( 'Value must be variable name as `str`, ' 'or Boolean value as `bool`, ' f'or ZDD node as `int`. Got: {value}') return self._rename(u, d) cpdef Function _cofactor_root( self, u: Function, d: _Assignment): """Return cofactor of `u` as defined in `d`. @param d: maps variable names to Boolean values """ if self.manager != u.manager: raise ValueError('`u.manager != self.manager`') level = 0 self.manager.reordered = 1 while self.manager.reordered == 1: cache = dict() self.manager.reordered = 0 try: r = self._cofactor(level, u, d, cache) except CouldNotCreateNode: r = None return r cpdef Function _cofactor( self, level: _Level, u: Function, d: _Assignment, cache: dict[ tuple[Function, _Level], Function]): """Recursively compute the cofactor of `u`.""" # terminal ? if u == self.false or self._gt_var_levels(level): return u t = (u, level) if t in cache: return cache[t] var = self.var_at_level(level) i = u.level if level > i: raise ValueError((level, i)) if level < i: if var in d: value = d[var] if value: r = self.false else: r = self._cofactor(level + 1, u, d, cache) r = self.find_or_add(var, r, r) else: r = self._cofactor(level + 1, u, d, cache) else: if level != i: raise AssertionError((level, i)) _, v, w = self.succ(u) if var in d: value = d[var] if value: r = self._cofactor(level + 1, w, d, cache) else: r = self._cofactor(level + 1, v, d, cache) r = self.find_or_add(var, r, r) else: p = self._cofactor(level + 1, v, d, cache) q = self._cofactor(level + 1, w, d, cache) r = self.find_or_add(var, p, q) cache[t] = r return r cpdef Function _cofactor_cudd( self, u: Function, var: _VariableName, value: python_bool): """CUDD implementation of cofactor.""" if self.manager != u.manager: raise ValueError('`u.manager != self.manager`') r: DdRef index = self._index_of_var[var] if value: r = Cudd_zddSubset1(self.manager, u.node, index) else: r = Cudd_zddSubset0(self.manager, u.node, index) return wrap(self, r) cpdef Function _compose_root( self, u: Function, d: dict[ _VariableName, Function]): """Return the composition defined in `d`. @param d: `dict` from variable names (`str`) to ZDD nodes (`Function`). """ if self.manager != u.manager: raise ValueError('`u.manager != self.manager`') self.manager.reordered = 1 while self.manager.reordered == 1: cache = dict() self.manager.reordered = 0 try: r = self._compose(0, u, d, cache) except CouldNotCreateNode: r = None return r cpdef Function _compose( self, level: _Level, u: Function, d: dict[ _VariableName, Function], cache: dict[ tuple[Function, _Level], Function]): """Recursively compute composition of `u`.""" if level > u.level: raise ValueError( 'requires `level <= u.level`, but: ' f'{level = } > {u.level = }') # terminal ? if u == self.false: return u if self._gt_var_levels(level): return self.true # full ZDDs at output t = (u, level) if t in cache: return cache[t] var = self.var_at_level(level) u_level = u.level if var in d: g = d[var] else: g = self.var(var) if level < u_level: if level + 1 > u.level: raise ValueError( 'expected `level + 1 <= u.level`, but ' f'{level + 1} = level + 1 > ' f'u.level = {u.level}') r = self._compose(level + 1, u, d, cache) r = self._ite_recursive(g, self.false, r) else: if level != u.level: raise AssertionError( 'expected `level == u.level`, but ' f'{level} = level != ' f'u.level = {u.level}') _, v, w = self.succ(u) if level + 1 > v.level: raise AssertionError( 'expected `level + 1 <= v.level`, but ' f'{level + 1} = level + 1 > ' f'v.level = {v.level}') if level + 1 > w.level: raise AssertionError( 'expected `level + 1 <= w.level`, but ' f'{level + 1} = level + 1 > ' f'w.level = {w.level}') p = self._compose(level + 1, v, d, cache) if level + 1 > w.level: raise AssertionError( 'expected `level + 1 <= w.level`, but ' f'{level + 1} = level + 1 > ' f'w.level = {w.level}') q = self._compose(level + 1, w, d, cache) r = self._ite_recursive(g, q, p) cache[t] = r return r cpdef Function _rename( self, u: Function, d: _Renaming): """Return node from renaming in `u` the variables in `d`.""" if self.manager != u.manager: raise ValueError('`u.manager != self.manager`') rename = {k: self.var(v) for k, v in d.items()} # return self._compose_root(u, rename) return _c_compose(u, rename) cpdef Function ite( self, g: Function, u: Function, v: Function): """Ternary conditional `IF g THEN u ELSE v` for ZDDs. Calls `Cudd_zddIte`. """ # for calling `cuddZddIte` # read the method `_ite_recursive` if g.manager != self.manager: raise ValueError('`g.manager != self.manager`') if u.manager != self.manager: raise ValueError('`u.manager != self.manager`') if v.manager != self.manager: raise ValueError('`v.manager != self.manager`') r: DdRef r = Cudd_zddIte(self.manager, g.node, u.node, v.node) if r is NULL: raise CouldNotCreateNode() return wrap(self, r) cpdef Function _ite_recursive( self, g: Function, u: Function, v: Function): """Recursive call to ternary conditional. Raises `CouldNotCreateNode` if reordering occurred. Calls `cuddZddIte`. """ if g.manager != self.manager: raise ValueError('`g.manager != self.manager`') if u.manager != self.manager: raise ValueError('`u.manager != self.manager`') if v.manager != self.manager: raise ValueError('`v.manager != self.manager`') r: DdRef r = cuddZddIte(self.manager, g.node, u.node, v.node) if r is NULL: raise CouldNotCreateNode() return wrap(self, r) cpdef Function find_or_add( self, var: _VariableName, low: Function, high: Function): """Return node `IF var THEN high ELSE low`.""" if low.manager != self.manager: raise ValueError('`low.manager != self.manager`') if high.manager != self.manager: raise ValueError('`high.manager != self.manager`') if var not in self.vars: raise ValueError( f'undeclared variable: {var}, ' f'not in: {self.vars}') level = self.level_of_var(var) if level >= low.level: raise ValueError(level, low.level, 'low.level') if level >= high.level: raise ValueError(level, high.level, 'high.level') r: DdRef index = self._index_of_var[var] if high == self.false: r = low.node else: r = cuddUniqueInterZdd( self.manager, index, high.node, low.node) if r is NULL: raise CouldNotCreateNode() f = wrap(self, r) if level > f.level: raise AssertionError(level, f.level, 'f.level') return f cdef DdRef _find_or_add( self, index: int, low: DdRef, high: DdRef ) except NULL: """Implementation of method `find_or_add` in C.""" r: DdRef if low is NULL: raise AssertionError('`low is NULL`') if high is NULL: raise AssertionError('`high is NULL`') if high == Cudd_ReadZero(self.manager): return low r = cuddUniqueInterZdd( self.manager, index, high, low) if r is NULL: raise AssertionError('r is NULL') return r cpdef tuple _top_cofactor( self, u: Function, level: _Level): """Return cofactor at `level`.""" u_level = u.level if level > u_level: raise ValueError((level, u_level)) if level < u_level: return (u, self.false) v, w = u.low, u.high if v is None: raise AssertionError('`v is None`') if w is None: raise AssertionError('`w is None`') return (v, w) def count( self, u: Function, nvars: _Cardinality | None=None ) -> _Cardinality: """Return nuber of models of node `u`. @param nvars: regard `u` as an operator that depends on `nvars` many variables. If omitted, then assume those in `support(u)`. """ if u.manager != self.manager: raise ValueError('nodes from different managers') support = self.support(u) r = self._count(0, u, support, cache=dict()) n_support = len(support) if nvars == None: nvars = n_support if nvars < n_support: raise ValueError((nvars, n_support)) return r * 2**(nvars - n_support) def _count( self, level: _Level, u: Function, support: set[_VariableName], cache: dict[Function, Function] ) -> _Cardinality: """Recurse to count satisfying assignments.""" if u == self.false: return 0 if self._gt_var_levels(level): return 1 if u in cache: return cache[u] v, w = self._top_cofactor(u, level) var = self.var_at_level(level) if var in support: nv = self._count(level + 1, v, support, cache) nw = self._count(level + 1, w, support, cache) r = nv + nw else: r = self._count(level + 1, v, support, cache) cache[u] = r return r def _count_cudd( self, u: Function, nvars: _Cardinality ) -> _Cardinality: """CUDD implementation of `self.count`.""" # returns different results if u.manager != self.manager: raise ValueError('nodes from different managers') n = len(self.support(u)) if nvars < n: raise ValueError((nvars, n)) r = Cudd_zddCountMinterm(self.manager, u.node, nvars) if r == CUDD_OUT_OF_MEM: raise RuntimeError('CUDD out of memory') if r == float('inf'): raise RuntimeError('overflow of integer type double') return r def pick( self, u: Function, care_vars: _abc.Set[ _VariableName] | None=None ) -> ( _Assignment | None): """Return a single satisfying assignment as `dict`.""" return next(self.pick_iter(u, care_vars), None) def pick_iter( self, u: Function, care_vars: _abc.Set[ _VariableName] | None=None ) -> _abc.Iterable[ _Assignment]: """Return iterator over satisfying assignments. The returned iterator is generator-based. """ if self.manager != u.manager: raise ValueError('nodes from different managers') support = self.support(u) if care_vars is None: care_vars = support missing = support.difference(care_vars) if missing: logger.warning( 'Missing bits: ' rf'support \ care_vars = {missing}') cube = dict() value = True config = self.configure(reordering=False) level = 0 for cube in self._sat_iter(level, u, cube, value, support): for m in _bdd._enumerate_minterms(cube, care_vars): yield m self.configure(reordering=config['reordering']) def _pick_iter_cudd( self, u: Function, care_vars: _abc.Set[ _VariableName] | None=None ) -> _abc.Iterable[ _Assignment]: """CUDD implementation of `self.pick_iter`.""" # assigns also to variables outside the support if self.manager != u.manager: raise ValueError('nodes from different ZDD managers') cdef DdGen *gen cdef int *path cdef double value support = self.support(u) if care_vars is None: care_vars = support missing = support.difference(care_vars) if missing: logger.warning( 'Missing bits: ' rf'support \ care_vars = {missing}') config = self.configure(reordering=False) gen = Cudd_zddFirstPath(self.manager, u.node, &path) if gen is NULL: raise RuntimeError('first path failed') try: r = 1 while Cudd_IsGenEmpty(gen) == 0: if r != 1: raise RuntimeError( f'gen not empty but no next path: {r}') d = _path_array_to_dict(path, self._index_of_var) if not set(d).issubset(support): raise AssertionError( set(d).difference(support)) for m in _bdd._enumerate_minterms(d, care_vars): yield m r = Cudd_zddNextPath(gen, &path) finally: Cudd_GenFree(gen) self.configure(reordering=config['reordering']) def _sat_iter( self, level: _Level, u: Function, cube: _Assignment, value: python_bool, support: set[_VariableName] ) -> _abc.Iterable[ _Assignment]: """Recurse to enumerate models.""" # terminal ? if u == self.false or self._gt_var_levels(level): if u != self.false: if not set(cube).issubset(support): raise ValueError( set(cube).difference(support)) yield cube return # non-terminal i, v, w = self.succ(u) var = self.var_at_level(level) if level > i: raise ValueError((level, i)) if level < i: cube[var] = False if var not in support: raise ValueError((var, support, '<')) for x in self._sat_iter( level + 1, u, cube, value, support): yield x elif v != w: if level != i: raise AssertionError((level, i)) if var not in support: raise ValueError((var, support, '==')) d0 = dict(cube) d0[var] = False d1 = dict(cube) d1[var] = True for x in self._sat_iter( level + 1, v, d0, value, support): yield x for x in self._sat_iter( level + 1, w, d1, value, support): yield x else: if level != i: raise AssertionError((level, i)) if not (v == w): raise AssertionError((v, w)) for x in self._sat_iter( level + 1, v, cube, value, support): yield x cpdef Function apply( self, op: _dd_abc.OperatorSymbol, u: Function, v: _ty.Optional[Function] =None, w: _ty.Optional[Function] =None): r"""Return as `Function` the result of applying `op`. @type op: `str` in - `'~'`, `'not'`, `'!'` (logical negation) - `'/\\'`, `'and'`, `'&'`, `'&&'` (conjunction) - `'or'`, `r'\/'`, `'|'`, `'||'` (disjunction) - `'#'`, `'xor'`, `'^'` (different values) - `'=>'`, `'implies'`, `'->'` (logical implication) - `'<=>'`, `'equiv'`, `'<->'` (logical equivalence) - `'ite'` (ternary conditional) - `r'\A'`, `'forall'` (universal quantification) - `r'\E'`, `'exists'` (existential quantification) - `'-'` (`a - b` means `a /\ ~ b`) """ _utils.assert_operator_arity(op, v, w, 'bdd') if self.manager != u.manager: raise ValueError( 'node `u` is from different ZDD manager') if v is not None and self.manager != v.manager: raise ValueError( 'node `v` is from different ZDD manager') if w is not None and self.manager != w.manager: raise ValueError( 'node `w` is from different ZDD manager') r: DdRef neg_node: DdRef t: Function cdef DdManager *mgr mgr = u.manager # unary r = NULL if op in ('~', 'not', '!'): r = Cudd_zddDiff( mgr, Cudd_ReadZddOne(mgr, 0), u.node) # binary elif op in ('and', '/\\', '&', '&&'): r = Cudd_zddIntersect(mgr, u.node, v.node) elif op in ('or', r'\/', '|', '||'): r = Cudd_zddUnion(mgr, u.node, v.node) elif op in ('#', 'xor', '^'): neg_node = Cudd_zddDiff( mgr, Cudd_ReadZddOne(mgr, 0), v.node) neg = wrap(self, neg_node) r = Cudd_zddIte(mgr, u.node, neg.node, v.node) elif op in ('=>', '->', 'implies'): r = Cudd_zddIte( mgr, u.node, v.node, Cudd_ReadZddOne(mgr, 0)) elif op in ('<=>', '<->', 'equiv'): neg_node = Cudd_zddDiff( mgr, Cudd_ReadZddOne(mgr, 0), v.node) neg = wrap(self, neg_node) r = Cudd_zddIte(mgr, u.node, v.node, neg.node) elif op in ('diff', '-'): r = Cudd_zddDiff(mgr, u.node, v.node) elif op in (r'\A', 'forall'): qvars = self.support(u) cube = _dict_to_zdd(qvars, v.zdd) r = _forall_root( mgr, v.node, cube.node) elif op in (r'\E', 'exists'): qvars = self.support(u) cube = _dict_to_zdd(qvars, v.zdd) r = _exist_root( mgr, v.node, cube.node) # ternary elif op == 'ite': r = Cudd_zddIte(mgr, u.node, v.node, w.node) else: raise ValueError( f'unknown operator: "{op}"') if r is NULL: config = self.configure() raise RuntimeError( 'CUDD appears to have run out of memory.\n' f'Computing the operator {op}\n.' 'Current settings for upper bounds:\n' f' max memory = {config["max_memory"]} bytes\n' f' max cache = {config["max_cache_hard"]} entries') res = wrap(self, r) return res cpdef Function _add_int( self, i: int): """Return node from integer `i`.""" u: DdRef if i in (0, 1): raise ValueError( rf'{i} \in {{0, 1}}') # invert `Function.__int__` if 2 <= i: i -= 2 u = i return wrap(self, u) cpdef Function cube( self, dvars: _abc.Collection[ _VariableName]): """Return conjunction of variables in `dvars`. If `dvars` is a `dict`, then a Boolean value `False` results in a negated variable. @param dvars: `dict` or container of variables as `str` @rtype: `Function` """ r = self.true for var in dvars: u = self.var(var) if isinstance(dvars, dict): value = dvars[var] else: value = True if value is True: r &= u elif value is False: r &= ~ u else: raise ValueError( f'value not Boolean: {value}') return r cpdef Function _disjoin_root( self, u: Function, v: Function): """Disjoin `u` and `v`.""" level = 0 self.manager.reordered = 1 while self.manager.reordered == 1: try: self.manager.reordered = 0 cache = dict() r = self._disjoin(level, u, v, cache) except CouldNotCreateNode: r = None return r cdef Function _disjoin( self, level: _c_level, u: Function, v: Function, cache: dict[ tuple[Function, Function], Function]): """Recursively disjoin `u` and `v`. The recursion starts at `level`. """ if u == self.false: return v if v == self.false: return u if self._gt_var_levels(level): if u.low is not None: raise ValueError( (u, u.low, level, self.var_levels)) if v.low is not None: raise ValueError( (v, v.low, level, self.var_levels)) if u != v: raise AssertionError( (level, u, v)) return u t = (u, v) if t in cache: return cache[t] pu, qu = self._top_cofactor(u, level) pv, qv = self._top_cofactor(v, level) p = self._disjoin(level + 1, pu, pv, cache) q = self._disjoin(level + 1, qu, qv, cache) var = self.var_at_level(level) r = self.find_or_add(var, p, q) cache[t] = r return r cpdef Function _conjoin_root( self, u: Function, v: Function): """Conjoin `u` and `v`.""" level = 0 self.manager.reordered = 1 while self.manager.reordered == 1: try: self.manager.reordered = 0 cache = dict() r = self._conjoin(level, u, v, cache) except CouldNotCreateNode: r = None return r cdef Function _conjoin( self, level: _c_level, u: Function, v: Function, cache): """Recursively conjoin `u` and `v`. The recursion starts at `level`. """ if u == self.false: return u if v == self.false: return v if self._gt_var_levels(level): if u.low is not None: raise ValueError( (u, u.low, level, self.var_levels)) if v.low is not None: raise ValueError( (v, v.low, level, self.var_levels)) if u != v: raise AssertionError( (level, u, v)) return u t = (u, v) if t in cache: return cache[t] pu, qu = self._top_cofactor(u, level) pv, qv = self._top_cofactor(v, level) p = self._conjoin(level + 1, pu, pv, cache) q = self._conjoin(level + 1, qu, qv, cache) var = self.var_at_level(level) r = self.find_or_add(var, p, q) cache[t] = r return r cpdef Function quantify( self, u: Function, qvars: _abc.Iterable[ _VariableName], forall: _Yes=False): """Abstract variables `qvars` from node `u`.""" if u.manager != self.manager: raise ValueError('`u.manager != self.manager`') # similar to the C implementation # return self._quantify_using_cube_root( # u, qvars, forall) # implementation that uses a `dict` # return self._quantify_root( # u, qvars, forall) # C implementation r: Function if forall: r = _c_forall(qvars, u) else: r = _c_exist(qvars, u) return r cpdef Function _quantify_root( self, u: Function, qvars: _abc.Container[ _VariableName], forall: _Yes=False): """Abstract variables `qvars` in `u`. @param forall: if `True`, then quantify `qvars` universally, else existentially. """ level = 0 self.manager.reordered = 1 while self.manager.reordered == 1: self.manager.reordered = 0 cache = dict() r = self._quantify( level, u, qvars, forall, cache) return r cpdef Function _quantify_using_cube_root( self, u: Function, qvars: _abc.Iterable[ _VariableName], forall: _Yes=False): """Abstract variables `qvars` in `u`. This implementation usses a ZDD to represent the set of variables to quantify. """ level = 0 cube = _dict_to_zdd(qvars, self) self.manager.reordered = 1 while self.manager.reordered == 1: self.manager.reordered = 0 cache = dict() r = self._quantify_using_cube( level, u, cube, forall, cache) return r def _quantify_using_cube( self, level: _Level, u: Function, cube: Function, forall: _Yes, cache: dict[ tuple[Function, _Level], Function] ) -> Function: """Recurse to quantify variables. This implementation uses a ZDD to represent the set of variables to quantify. """ if u == self.false or self._gt_var_levels(level): return u t = (u, level) if t in cache: return cache[t] v, w = self._top_cofactor(u, level) if cube.low != cube.high: raise ValueError((cube, cube.low, cube.high)) new_cube, _ = self._top_cofactor(cube, level) p = self._quantify_using_cube(level + 1, v, new_cube, forall, cache) q = self._quantify_using_cube(level + 1, w, new_cube, forall, cache) var = self.var_at_level(level) if level > cube.level: raise ValueError((level, cube, cube.level)) if level == cube.level: if forall: r = self._conjoin( level + 1, p, q, dict()) else: r = self._disjoin( level + 1, p, q, dict()) r = self.find_or_add(var, r, r) else: r = self.find_or_add(var, p, q) cache[t] = r return r def _quantify( self, level: _Level, u: Function, qvars: _abc.Container[ _VariableName], forall: _Yes, cache: dict[ tuple[Function, _Level], Function] ) -> Function: """Recurse to quantify variables.""" if (level > u.level): raise ValueError( (level, u.level, u)) # terminal ? if u == self.false or self._gt_var_levels(level): return u t = (u, level) if t in cache: r = cache[t] if level > r.level: raise AssertionError( (level, r.level, r, '= cache[(u, level)]')) return r v, w = self._top_cofactor(u, level) if level >= v.level: raise AssertionError((level, v.level, v)) if level >= w.level: raise AssertionError((level, w.level, w)) p = self._quantify(level + 1, v, qvars, forall, cache) q = self._quantify(level + 1, w, qvars, forall, cache) if level >= p.level: raise AssertionError((level, p.level, p)) if level >= q.level: raise AssertionError((level, q.level, q)) var = self.var_at_level(level) if var in qvars: if forall: r = self._conjoin( level + 1, p, q, dict()) else: r = self._disjoin( level + 1, p, q, dict()) r = self.find_or_add(var, r, r) else: r = self.find_or_add(var, p, q) if min(level, u.level) > r.level: raise AssertionError( (level, u.level, r.level, 'u, r')) cache[t] = r if level > r.level: raise AssertionError((level, r.level, 'r')) return r def _quantify_optimized( self, level: _Level, u: Function, qvars: _abc.Container[ _VariableName], forall: _Yes, cache: dict[Function, Function] ) -> Function: """Recurse to quantify variables.""" # terminal ? if u == self.false or self._gt_var_levels(level): return u if u in cache: return cache[u] u_level = u.level var = self.var_at_level(level) if level < u_level: r = self._quantify( level + 1, u, qvars, forall, cache) if var in qvars: r = self.find_or_add(var, r, r) else: _, v, w = self.succ(u) p = self._quantify( level + 1, v, qvars, forall, cache) q = self._quantify( level + 1, w, qvars, forall, cache) if var in qvars: if forall: r = self._conjoin( level + 1, p, q, dict()) else: r = self._disjoin( level + 1, p, q, dict()) r = self.find_or_add(var, r, r) else: r = self.find_or_add(var, p, q) cache[u] = r return r cpdef Function forall( self, variables: _abc.Iterable[ _VariableName], u: Function): """Quantify `variables` in `u` universally. Wraps method `quantify` to be more readable. """ return self.quantify(u, variables, forall=True) cpdef Function exist( self, variables: _abc.Iterable[ _VariableName], u: Function): """Quantify `variables` in `u` existentially. Wraps method `quantify` to be more readable. """ return self.quantify(u, variables, forall=False) cpdef assert_consistent( self): """Raise `AssertionError` if not consistent.""" if Cudd_DebugCheck(self.manager) != 0: raise AssertionError('`Cudd_DebugCheck` errored') n = len(self.vars) m = len(self._var_with_index) k = len(self._index_of_var) if n != m: raise AssertionError( f'`len(self.vars) == {n}` but ' f'`len(self._var_with_index) == {m}`\n' f'{self.vars = }\n' f'{self._var_with_index = }') if m != k: raise AssertionError( f'`len(self._var_with_index) == {m}` but ' f'`len(self._index_of_var) == {k}`\n' f'{self._var_with_index = }\n' f'{self._index_of_var = }') if set(self.vars) != set(self._index_of_var): raise AssertionError( '`set(self.vars) != ' 'set(self._index_of_var)`\n' f'{self.vars = }\n' f'{self._index_of_var = }') if set(self._var_with_index) != set( self._index_of_var.values()): raise AssertionError( '`set(self._var_with_index) != ' 'set(self._index_of_var.values())`\n' f'{self._var_with_index = }\n' f'{self._index_of_var = }') def add_expr( self, expr: _Formula ) -> Function: """Return node for expression `e`.""" return _parser.add_expr(expr, self) cpdef str to_expr( self, u: Function): """Return a Boolean expression for node `u`.""" if u.manager != self.manager: raise ValueError('`u.manager != self.manager`') cache = dict() level = 0 return self._to_expr(level, u, cache) cpdef str _to_expr( self, level: _Level, u: Function, cache: dict[Function, _Formula]): """Recursively compute an expression.""" if u == self.false: return 'FALSE' if self._gt_var_levels(level): return 'TRUE' if u in cache: return cache[u] v, w = self._top_cofactor(u, level) p = self._to_expr(level + 1, v, cache) q = self._to_expr(level + 1, w, cache) var = self.var_at_level(level) if p == 'FALSE' and q == 'TRUE': s = var elif p == q: s = p else: s = f'ite({var}, {q}, {p})' cache[u] = s return s cpdef dump( self, filename: str, roots: list[Function], filetype: _dd_abc.ImageFileType | None=None): """Write ZDD as a diagram to file `filename`. The file type is inferred from the extension (case insensitive), unless a `filetype` is explicitly given. `filetype` can have the values: - `'pdf'` for PDF - `'png'` for PNG - `'svg'` for SVG If `filetype is None`, then `filename` must have an extension that matches one of the file types listed above. Only the ZDD manager nodes that are reachable from the ZDD references in `roots` are included in the diagram. @param filename: file name, e.g., `"diagram.pdf"` @param roots: container of nodes """ if filetype is None: name = filename.lower() if name.endswith('.pdf'): filetype = 'pdf' elif name.endswith('.png'): filetype = 'png' elif name.endswith('.svg'): filetype = 'svg' elif name.endswith('.dot'): filetype = 'dot' else: raise ValueError( 'cannot infer file type ' 'from extension of file ' f'name "{filename}"') if filetype in _utils.DOT_FILE_TYPES: self._dump_figure( roots, filename, filetype) else: raise ValueError( f'unknown file type "{filetype}", ' 'the method `dd.cudd_zdd.ZDD.dump` ' 'supports writing diagrams as ' 'PDF, PNG, or SVG files.') def _dump_figure( self, roots: _abc.Collection[Function], filename: str, filetype: _dd_abc.ImageFileType, **kw ) -> None: """Write BDDs to `filename` as figure.""" g = _to_dot(roots) g.dump( filename, filetype=filetype, **kw) cpdef load( self, filename: str): raise NotImplementedError() # Same with the method `dd.cudd.BDD._cube_to_dict`. cpdef dict _cube_to_dict( self, f: Function): """Recurse to collect indices of support variables.""" if f.manager != self.manager: raise ValueError('`f.manager != self.manager`') n = self._number_of_cudd_vars() cdef int *x x = PyMem_Malloc(n * sizeof(DdRef)) try: Cudd_BddToCubeArray(self.manager, f.node, x) d = _cube_array_to_dict(x, self._index_of_var) finally: PyMem_Free(x) return d @property def false( self ) -> Function: """`Function` for Boolean value FALSE. Relevant properties: `ZDD.true` and `ZDD.true_node`. """ return self._bool(False) @property def true( self ) -> Function: """`Function` for Boolean value TRUE. Read also the docstring of the property `ZDD.true_node`. Relevant properties: `ZDD.false`, `ZDD.true_node` """ return self._bool(True) @property def true_node( self ) -> Function: """Return the constant ZDD node for TRUE. Compare with the property `true`, for example the value of `ZDD.true.level` with the value of `ZDD.true_node.level`, after at least one variable has been declared: ```python import dd.cudd_zdd as _zdd zdd = _zdd.ZDD() zdd.declare('x') a = zdd.true.level b = zdd.true_node.level print(f'level of `zdd.true`: {a}') print(f'level of `zdd.true_node`: {b}') ``` Relevant properties: `ZDD.false`, `ZDD.true` """ r: DdRef r = DD_ONE(self.manager) return wrap(self, r) cdef Function _bool( self, v: python_bool): """Return terminal node for Boolean `v`.""" r: DdRef if v: r = Cudd_ReadZddOne(self.manager, 0) else: r = Cudd_ReadZero(self.manager) return wrap(self, r) cdef Function wrap( bdd: ZDD, node: DdRef): """Return a `Function` that wraps `node`.""" # because `@classmethod` unsupported f = Function() f.init(node, bdd) return f cdef class Function: """Wrapper of ZDD `DdNode` from CUDD. For details, read the docstring of the class `dd.cudd.Function`. """ __weakref__: object cdef public ZDD bdd cdef public ZDD zdd cdef DdManager *manager node: DdRef cdef public int _ref cdef init( self, node: DdRef, zdd: ZDD): if node is NULL: raise ValueError('`DdNode *node` is `NULL` pointer.') self.zdd = zdd self.bdd = zdd # keep this attribute for writing # common algorithms for BDDs and ZDDs where possible self.manager = zdd.manager self.node = node self._ref = 1 # lower bound on reference count Cudd_Ref(node) def __hash__( self ) -> int: return int(self) @property def _index( self ) -> int: """Index of `self.node`.""" return Cudd_NodeReadIndex(self.node) @property def var( self ) -> ( _VariableName | None): """Variable at level where this node is. If node is constant, return `None`. """ if cuddIsConstant(self.node): return None return self.zdd._var_with_index[self._index] @property def level( self ) -> _Level: """Level where this node currently is.""" i = self._index return Cudd_ReadPermZdd(self.manager, i) @property def ref( self ) -> _Cardinality: """Reference count of node.""" return self.node.ref @property def low( self ) -> ( Function | None): """Return "else" node.""" if cuddIsConstant(self.node): return None u: DdRef # refer to the lines: # # else if (top_var == level) { # res = cuddE(P); # # inside the function `zdd_subset0_aux`, # in the file `cudd/cuddZddSetop.c`. u = cuddE(self.node) return wrap(self.zdd, u) @property def high( self ) -> ( Function | None): """Return "then" node.""" if cuddIsConstant(self.node): return None u: DdRef # refer to the lines: # # } else if (top_var == level) { # res = cuddT(P); # # inside the function `zdd_subset1_aux`, # in file `cudd/cuddZddSetop.c`. u = cuddT(self.node) return wrap(self.zdd, u) @property def negated( self ) -> _Yes: raise Exception( 'No complemented edges for ZDDs in CUDD, ' 'only for BDDs.') @property def support( self: ZDD ) -> set[_VariableName]: """Return `set` of variables in support.""" return self.zdd.support(self) def __dealloc__( self ) -> None: # when changing this method, # update also the function # `_test_call_dealloc` below if self._ref < 0: raise AssertionError( "The lower bound `_ref` on the node's " f'reference count has value {self._ref}, ' 'which is unexpected and should never happen. ' 'Was the value of `_ref` changed from outside ' 'this instance?') assert self._ref >= 0, self._ref if self._ref == 0: return if self.node is NULL: raise AssertionError( 'The attribute `node` is a `NULL` pointer. ' 'This is unexpected and should never happen. ' 'Was the value of `_ref` changed from outside ' 'this instance?') # anticipate multiple calls to `__dealloc__` self._ref -= 1 # deref Cudd_RecursiveDerefZdd(self.manager, self.node) # avoid future access to deallocated memory self.node = NULL def __int__( self ) -> int: # inverse is `ZDD._add_int` if sizeof(stdint.uintptr_t) != sizeof(DdRef): raise RuntimeError( 'expected equal pointer sizes') i = self.node # 0, 1 are true and false in logic syntax if 0 <= i: i += 2 if i in (0, 1): raise AssertionError(i) return i def __repr__( self ) -> str: return ( f'') def __str__( self ) -> str: return f'@{int(self)}' def __len__( self ) -> _Cardinality: # The function `Cudd_zddDagSize` # is deprecated because it duplicates # the function `Cudd_DagSize`. return Cudd_zddDagSize(self.node) @property def dag_size( self ) -> _Cardinality: """Return number of ZDD nodes. This is the number of ZDD nodes that are reachable from this ZDD reference, i.e., with `self` as root. """ return len(self) def __eq__( self: Function, other: _ty.Optional[Function] ) -> _Yes: if other is None: return False # guard against mixing managers if self.manager != other.manager: raise ValueError( '`self.manager != other.manager`') return self.node == other.node def __ne__( self: Function, other: _ty.Optional[Function] ) -> _Yes: if other is None: return True if self.manager != other.manager: raise ValueError( '`self.manager != other.manager`') return self.node != other.node def __le__( self: Function, other: Function ) -> _Yes: if self.manager != other.manager: raise ValueError( '`self.manager != other.manager`') return (other | ~ self) == self.zdd.true def __lt__( self: Function, other: Function ) -> _Yes: if self.manager != other.manager: raise ValueError( '`self.manager != other.manager`') return ( self.node != other.node and (other | ~ self) == self.zdd.true) def __ge__( self: Function, other: Function ) -> _Yes: if self.manager != other.manager: raise ValueError( '`self.manager != other.manager`') return (self | ~ other) == self.zdd.true def __gt__( self: Function, other: Function ) -> _Yes: if self.manager != other.manager: raise ValueError( '`self.manager != other.manager`') return ( self.node != other.node and (self | ~ other) == self.zdd.true) def __invert__( self ) -> Function: r: DdRef r = Cudd_zddDiff( self.manager, Cudd_ReadZddOne(self.manager, 0), self.node) return wrap(self.zdd, r) def __and__( self: Function, other: Function ) -> Function: if self.manager != other.manager: raise ValueError('`self.manager != other.manager`') r = Cudd_zddIntersect(self.manager, self.node, other.node) return wrap(self.zdd, r) def __or__( self: Function, other: Function ) -> Function: if self.manager != other.manager: raise ValueError('`self.manager != other.manager`') r = Cudd_zddUnion(self.manager, self.node, other.node) return wrap(self.zdd, r) def implies( self: Function, other: Function ) -> Function: if self.manager != other.manager: raise ValueError('`self.manager != other.manager`') r = Cudd_zddIte( self.manager, self.node, other.node, Cudd_ReadZddOne(self.manager, 0)) return wrap(self.zdd, r) def equiv( self: Function, other: Function ) -> Function: return self.zdd.apply('<=>', self, other) def let( self: Function, **definitions: _VariableName | python_bool | Function ) -> Function: return self.zdd.let(definitions, self) def exist( self: Function, *variables: _VariableName ) -> Function: return self.zdd.exist(variables, self) def forall( self: Function, *variables: _VariableName ) -> Function: return self.zdd.forall(variables, self) def pick( self: Function, care_vars: _abc.Set[ _VariableName] | None=None ) -> ( _Assignment | None): return self.zdd.pick(self, care_vars) def count( self: Function, nvars: _Cardinality | None=None ) -> _Cardinality: return self.zdd.count(self, nvars) # Similar to the function `dd.cudd._cube_array_to_dict` cdef dict _path_array_to_dict( int *x, index_of_var: dict): """Return assignment from array of literals `x`.""" d = dict() for var, j in index_of_var.items(): b = x[j] if b == 2: # absence of ZDD node d[var] = False elif b == 1: # "then" arc d[var] = True elif b == 0: # "else" arc d[var] = False else: raise ValueError( f'unknown polarity: {b}, ' f'for variable "{var}"') return d # Copy of the function `dd.cudd._cube_array_to_dict` # TODO: place in a header file, if used in this module cdef dict _cube_array_to_dict( int *x, index_of_var: dict): """Return assignment from array of literals `x`. @param x: read `dd.cudd._dict_to_cube_array` """ d = dict() for var, j in index_of_var.items(): b = x[j] if b == 2: continue elif b == 1: d[var] = True elif b == 0: d[var] = False else: raise ValueError( f'unknown polarity: {b}, ' f'for variable "{var}"') return d def to_nx( u: Function ) -> '_utils.MultiDiGraph': """Return graph for the ZDD rooted at `u`.""" _nx = _utils.import_module('networkx') g = _nx.MultiDiGraph() _to_nx(g, u, umap=dict()) return g def _to_nx( g: '_utils.MultiDiGraph', u: Function, umap: dict ) -> None: """Recursively construct a ZDD graph.""" u_int = int(u) # visited ? if u_int in umap: return u_nd = umap.setdefault(u_int, len(umap)) if u.var is None: label = 'FALSE' if u == u.bdd.false else 'TRUE' else: label = u.var g.add_node(u_nd, label=label) if u.var is None: return v, w = u.low, u.high if v is None: raise AssertionError(v) if w is None: raise AssertionError(w) v_int = int(v) w_int = int(w) _to_nx(g, v, umap) _to_nx(g, w, umap) v_nd = umap[v_int] w_nd = umap[w_int] g.add_edge(u_nd, v_nd, taillabel='0', style='dashed') g.add_edge(u_nd, w_nd, taillabel='1', style='solid') def _to_dot( roots: _abc.Collection[Function] ) -> _utils.DotGraph: """Return graph for the ZDD rooted at `u`.""" if not roots: raise ValueError( f'No `roots` given: {roots}') assert roots, roots g = _utils.DotGraph( graph_type='digraph') # construct graphs subgraphs = _add_nodes_for_zdd_levels(g, roots) # mapping CUDD ZDD node ID -> node name in DOT graph umap = dict() for u in roots: _to_dot_recurse( g, u, umap, subgraphs) _add_nodes_for_external_references( roots, umap, g, subgraphs[-1]) return g def _add_nodes_for_zdd_levels( g, roots: _abc.Collection[Function] ) -> dict[ int, _utils.DotGraph]: """Create nodes and subgraphs for ZDD levels. For each level of any ZDD node reachable from `roots`, a new node `u_level` and a new subgraph `h_level` are created. The node `u_level` is labeled with the level (as numeral), and added to the subgraph `h_level`. For each pair of consecutive levels in `sorted(set of levels of nodes reachable from roots)`, an edge is added to graph `g`, pointing from the node labeled with the smaller level, to the node labeled with the larger level. Level `-1` is considered to represent external references to ZDD nodes, i.e., instances of the class `Function`. The collection of subgraphs (`h_level` above) is returned. @return: mapping from each ZDD level to a subgraph, for the ZDD levels of nodes reachable from `roots`. """ # mapping level -> var level_to_var = _collect_var_levels(roots) # add layer for external ZDD references level_to_var[-1] = None subgraphs = dict() level_node_names = list() for level in sorted(level_to_var): h = _utils.DotGraph( rank='same') g.subgraphs.append(h) subgraphs[level] = h # add phantom node u = f'"L{level}"' level_node_names.append(u) if level == -1: # layer for external ZDD references label = 'ref' else: # ZDD level label = str(level) h.add_node( u, label=label, shape='none') # auxiliary edges for ranking of levels a, a1 = _itr.tee(level_node_names) next(a1, None) for u, v in zip(a, a1): g.add_edge( u, v, style='invis') return subgraphs def _to_dot_recurse( g: _utils.DotGraph, u: Function, umap: dict[int, int], subgraphs: dict[ _Level, _utils.DotGraph] ) -> None: """Recursively construct a ZDD graph.""" u_int = int(u) # visited ? if u_int in umap: return u_nd = umap.setdefault(u_int, len(umap)) if u.var is None: label = 'FALSE' if u == u.bdd.false else 'TRUE' else: label = u.var h = subgraphs[u.level] h.add_node( u_nd, label=label) if u.var is None: return v, w = u.low, u.high if v is None: raise AssertionError(v) if w is None: raise AssertionError(w) v_int = int(v) w_int = int(w) _to_dot_recurse(g, v, umap, subgraphs) _to_dot_recurse(g, w, umap, subgraphs) v_nd = umap[v_int] w_nd = umap[w_int] g.add_edge( u_nd, v_nd, taillabel='0', style='dashed') g.add_edge( u_nd, w_nd, taillabel='1', style='solid') def _add_nodes_for_external_references( roots: list[Function], umap: dict[int, int], g: _utils.DotGraph, h: _utils.DotGraph ) -> None: """Add nodes to `g` that represent the references in `roots`. @param roots: external references to ZDD nodes @param g: ZDD graph @param h: subgraph of `g` """ for u in roots: if u is None: raise ValueError(u) u_int = int(u) u_nd = umap[u_int] # add node to subgraph at level -1 ref_nd = f'ref{int(u)}' label = f'@{int(u)}' h.add_node( ref_nd, label=label) # add edge from external reference to ZDD node g.add_edge( ref_nd, u_nd, style='dashed') def _collect_var_levels( roots: list[Function] ) -> dict[ _Level, _VariableName]: """Add variables and levels reachable from `roots`. @param roots: container of ZDD nodes @return: `dict` that maps each level (as `int`) to a variable (as `str`), only for levels of nodes that are reachable from the ZDD node `u` """ level_to_var = dict() visited = set() for u in roots: _collect_var_levels_recurse( u, level_to_var, visited) return level_to_var def _collect_var_levels_recurse( u: Function, level_to_var: dict[ _Level, _VariableName], visited: set[int] ) -> None: """Recursively collect variables and levels. @param level_to_var: maps each level to a variable, only for levels of nodes that are reachable from the ZDD node `u` @param visited: already visited ZDD nodes """ u_int = int(u) if u_int in visited: return visited.add(u_int) level_to_var[u.level] = u.var if u.var is None: return v, w = u.low, u.high if v is None: raise AssertionError(v) if w is None: raise AssertionError(w) _collect_var_levels_recurse(v, level_to_var, visited) _collect_var_levels_recurse(w, level_to_var, visited) cpdef Function _dict_to_zdd( qvars: _abc.Iterable[ _VariableName], zdd: ZDD): """Return a ZDD that is TRUE over `qvars`. This ZDD has nodes at levels of variables in `qvars`. Each such node has same low and high. """ levels = {zdd.level_of_var(var) for var in qvars} r = zdd.true_node for level in sorted(levels, reverse=True): var = zdd.var_at_level(level) r = zdd.find_or_add(var, r, r) return r cpdef set _cube_to_universe_root( cube: Function, zdd: ZDD): """Map the conjunction `cube` to its support.""" qvars = set() _cube_to_universe(cube, qvars, zdd) qvars_ = zdd.support(cube) if qvars != qvars_: raise AssertionError((qvars, qvars_)) return qvars cpdef _cube_to_universe( cube: Function, qvars: set[_VariableName], zdd: ZDD): """Recursively map `cube` to its support.""" if cube == zdd.false: return if cube == zdd.true_node: return if cube.low != cube.high: var = cube.var qvars.add(var) if cube.low != zdd.false: raise ValueError((cube, cube.low, len(cube.low))) _cube_to_universe(cube.high, qvars, zdd) cpdef Function _ith_var( var: _VariableName, zdd: ZDD): """Return ZDD of variable `var`. This function requires that declared variables have a contiguous range of levels. """ n_declared_vars = len(zdd.vars) n_cudd_vars = zdd._number_of_cudd_vars() if n_declared_vars > n_cudd_vars: # note that `zdd.var_levels()` cannot # be called here, because not all declared # variables have levels in CUDD, given # that `n_declared_vars > n_cudd_vars` raise AssertionError(_tw.dedent(f''' Found unexpected number of declared variables ({n_declared_vars}), compared to number of CUDD variable indices ({n_cudd_vars}). Expected number of declared variables <= number of CUDD variable indices. The declared variables and their indices are: {zdd._index_of_var} ''')) if n_declared_vars != n_cudd_vars: counts = _utils.var_counts(zdd) contiguous = _utils.contiguous_levels( '_ith_var', zdd) raise AssertionError(f'{counts}\n{contiguous}') level = zdd.level_of_var(var) r = zdd.true_node for j in range(len(zdd.vars) - 1, -1, -1): v = zdd.var_at_level(j) if j == level: r = zdd.find_or_add(v, zdd.false, r) else: r = zdd.find_or_add(v, r, r) return r # changes to the function `_c_exist` # are copied here cpdef Function _c_forall( variables: _abc.Iterable[ _VariableName], u: Function): """Universally quantify `variables` in `u`.""" r: DdRef cube = _dict_to_zdd(variables, u.bdd) r = _forall_root(u.manager, u.node, cube.node) return wrap(u.bdd, r) # changes to the function `_exist_root` # are copied here cdef DdRef _forall_root( DdManager *mgr, u: DdRef, cube: DdRef ) except NULL: r"""Root of recursion for \A.""" if mgr is NULL: raise AssertionError('`mgr is NULL`') if u is NULL: raise AssertionError('`u is NULL`') if cube is NULL: raise AssertionError('`cube is NULL`') mgr.reordered = 1 while mgr.reordered == 1: mgr.reordered = 0 r = _forall(mgr, 0, u, cube) if r is NULL: raise AssertionError('`r is NULL`') return r cdef DdRef _forall_cache_id( DdManager *mgr, u: DdRef, cube: DdRef ) noexcept: """Used only as cache key. Passed inside the function `_forall()` to the C functions: - `cuddCacheLookup2Zdd()` - `cuddCacheInsert2()` """ # changes to the function `_exist` # are copied here cdef DdRef _forall( DdManager *mgr, level: _c_level, u: DdRef, cube: DdRef ) except? NULL: r"""Recursive \A. Asserts that: - `level` <= level of `u` - `level` <= level of `cube` """ if level < 0: raise AssertionError( f'`{level = } < 0`') if u is NULL: raise AssertionError('`u is NULL`') if cube is NULL: raise AssertionError('`cube is NULL`') index = Cudd_ReadInvPermZdd(mgr, level) if (u == DD_ZERO(mgr) or index == -1 or index == CUDD_CONST_INDEX): return u r = cuddCacheLookup2Zdd( mgr, _forall_cache_id, u, cube) if r is not NULL: return r u_index = Cudd_NodeReadIndex(u) u_level = Cudd_ReadPermZdd(mgr, u_index) cube_index = Cudd_NodeReadIndex(cube) cube_level = Cudd_ReadPermZdd(mgr, cube_index) if level > u_level: raise AssertionError((level, u_level)) if level > cube_level: raise AssertionError((level, cube_level)) # top cofactor if level < u_level: v, w = u, DD_ZERO(mgr) else: v, w = cuddE(u), cuddT(u) if level < cube_level: new_cube = cube else: if cuddE(cube) != cuddT(cube): raise AssertionError(cube) new_cube = cuddE(cube) p = _forall(mgr, level + 1, v, new_cube) if p is NULL: return NULL cuddRef(p) q = _forall(mgr, level + 1, w, new_cube) if q is NULL: Cudd_RecursiveDerefZdd(mgr, p) return NULL cuddRef(q) if level == cube_level: conj = _conjoin(mgr, level + 1, p, q) if conj is NULL: Cudd_RecursiveDerefZdd(mgr, p) Cudd_RecursiveDerefZdd(mgr, q) return NULL cuddRef(conj) r = _find_or_add(mgr, index, conj, conj) Cudd_RecursiveDerefZdd(mgr, conj) else: r = _find_or_add(mgr, index, p, q) if r is NULL: Cudd_RecursiveDerefZdd(mgr, p) Cudd_RecursiveDerefZdd(mgr, q) return NULL cuddRef(r) Cudd_RecursiveDerefZdd(mgr, p) Cudd_RecursiveDerefZdd(mgr, q) cuddCacheInsert2( mgr, _forall_cache_id, u, cube, r) cuddDeref(r) return r cpdef Function _c_exist( variables: _abc.Iterable[ _VariableName], u: Function): """Existentially quantify `variables` in `u`.""" r: DdRef cube: Function cube = _dict_to_zdd(variables, u.bdd) r = _exist_root(u.manager, u.node, cube.node) return wrap(u.bdd, r) cdef DdRef _exist_root( DdManager *mgr, u: DdRef, cube: DdRef ) except NULL: r"""Root of recursion for \E.""" if mgr is NULL: raise AssertionError('`mgr is NULL`') if u is NULL: raise AssertionError('`u is NULL`') if cube is NULL: raise AssertionError('`cube is NULL`') mgr.reordered = 1 while mgr.reordered == 1: mgr.reordered = 0 r = _exist(mgr, 0, u, cube) if r is NULL: raise AssertionError('`r is NULL`') return r cdef DdRef _exist_cache_id( DdManager *mgr, u: DdRef, cube: DdRef ) noexcept: """Used only as cache key. Passed inside the function `_exist()` to the C functions: - `cuddCacheLookup2Zdd()` - `cuddCacheInsert2()` """ raise NotImplementedError( 'This function is used only ' 'as cache key.') cdef DdRef _exist( DdManager *mgr, level: _c_level, u: DdRef, cube: DdRef ) except? NULL: r"""Recursive \E. Asserts that: - `level` <= level of `u` - `level` <= level of `cube` """ if level < 0: raise AssertionError( f'`{level = } < 0`') if u is NULL: raise AssertionError('`u is NULL`') if cube is NULL: raise AssertionError('`cube is NULL`') index = Cudd_ReadInvPermZdd(mgr, level) if (u == DD_ZERO(mgr) or index == -1 or index == CUDD_CONST_INDEX): return u r = cuddCacheLookup2Zdd( mgr, _exist_cache_id, u, cube) if r is not NULL: return r u_index = Cudd_NodeReadIndex(u) u_level = Cudd_ReadPermZdd(mgr, u_index) cube_index = Cudd_NodeReadIndex(cube) cube_level = Cudd_ReadPermZdd(mgr, cube_index) if level > u_level: raise AssertionError((level, u_level)) if level > cube_level: raise AssertionError((level, cube_level)) # top cofactor if level < u_level: v, w = u, DD_ZERO(mgr) else: v, w = cuddE(u), cuddT(u) if level < cube_level: new_cube = cube else: if cuddE(cube) != cuddT(cube): raise AssertionError('expected: E == T') new_cube = cuddE(cube) p = _exist(mgr, level + 1, v, new_cube) if p is NULL: return NULL cuddRef(p) q = _exist(mgr, level + 1, w, new_cube) if q is NULL: Cudd_RecursiveDerefZdd(mgr, p) return NULL cuddRef(q) if level > cube_level: raise AssertionError((level, cube_level)) if level == cube_level: disj = _disjoin(mgr, level + 1, p, q) if disj is NULL: Cudd_RecursiveDerefZdd(mgr, p) Cudd_RecursiveDerefZdd(mgr, q) return NULL cuddRef(disj) r = _find_or_add(mgr, index, disj, disj) Cudd_RecursiveDerefZdd(mgr, disj) else: r = _find_or_add(mgr, index, p, q) if r is NULL: Cudd_RecursiveDerefZdd(mgr, p) Cudd_RecursiveDerefZdd(mgr, q) return NULL cuddRef(r) Cudd_RecursiveDerefZdd(mgr, p) Cudd_RecursiveDerefZdd(mgr, q) cuddCacheInsert2( mgr, _exist_cache_id, u, cube, r) cuddDeref(r) return r cdef DdRef _find_or_add( DdManager *mgr, index: _c_int, v: DdRef, w: DdRef ) except? NULL: """Find node in table or add new node. Calls `cuddUniqueInterZdd` and ensures canonicity of ZDDs. Returns `NULL` when: - memory is exhausted - reordering occurred - a termination request was detected - a timeout expired These cases are based on the docstring of the CUDD function `cuddUniqueInterZdd()`. """ if mgr is NULL: raise AssertionError('`mgr is NULL`') if index < 0 or index > CUDD_CONST_INDEX: raise AssertionError( f'`{index = }`') if v is NULL: raise AssertionError('`v is NULL`') if w is NULL: raise AssertionError('`w is NULL`') if w == DD_ZERO(mgr): return v return cuddUniqueInterZdd(mgr, index, w, v) cpdef Function _c_disjoin( u: Function, v: Function): """Return the disjunction of `u` and `v`. @param u, v: ZDD node @rtype: `Function` """ r: DdRef cdef DdManager *mgr mgr = u.manager r = _disjoin_root( mgr, u.node, v.node) return wrap(u.bdd, r) cdef DdRef _disjoin_root( DdManager *mgr, u: DdRef, v: DdRef ) except NULL: """Return the disjunction of `u` and `v`.""" if mgr is NULL: raise AssertionError('`mgr is NULL`') if u is NULL: raise AssertionError('`u is NULL`') if v is NULL: raise AssertionError('`v is NULL`') mgr.reordered = 1 while mgr.reordered == 1: mgr.reordered = 0 r = _disjoin(mgr, 0, u, v) if r is NULL: raise AssertionError('`r is NULL`') return r # This function is used for the hash in cache. # # This function exists because # `except? NULL` cannot be # used in the signature of the # function `_disjoin_root()`. # # Doing so changes the signature # in a way that passing `_disjoin_root()` # to the C function `cuddCacheLookup2Zdd()` # raises a Cython compilation error. cdef DdRef _disjoin_cache_id( DdManager *mgr, u: DdRef, v: DdRef ) noexcept: """Used only as cache key. Passed inside the function `_disjoin()` to the C functions: - `cuddCacheLookup2Zdd()` - `cuddCacheInsert2()` """ # The function `_disjoin()` returns `NULL` # also in cases that are not due to # raising an exception. # # Those cases are due to reaching the point where # the tables need to be resized, and reordering # invoked. cdef DdRef _disjoin( DdManager *mgr, level: _c_level, u: DdRef, v: DdRef ) except? NULL: """Recursively disjoin `u` and `v`. Asserts that: - `level` <= level of `u` - `level` <= level of `v` """ # `mgr is not NULL` BY `_disjoin_root()`. # `mgr` remains unchanged throughout # the recursion, and it is impossible # to call `_disjoin()` from Python, # so `mgr` not checked here, for efficiency. if level < 0: raise AssertionError( f'`{level = } < 0`') if u is NULL: raise AssertionError('`u is NULL`') if v is NULL: raise AssertionError('`v is NULL`') index = Cudd_ReadInvPermZdd(mgr, level) # TODO: review reference counting if u == DD_ZERO(mgr): return v if v == DD_ZERO(mgr): return u if index == -1 or index == CUDD_CONST_INDEX: if u != DD_ONE(mgr): raise AssertionError( 'nonconstant node `u` given, ' f'when given level: {level}, ' f'with index: {index}') if v != DD_ONE(mgr): raise AssertionError( 'nonconstant node `v` given, ' f'when given level: {level}, ' f'with index: {index}') return u r = cuddCacheLookup2Zdd( mgr, _disjoin_cache_id, u, v) if r is not NULL: return r u_index = Cudd_NodeReadIndex(u) u_level = Cudd_ReadPermZdd(mgr, u_index) v_index = Cudd_NodeReadIndex(v) v_level = Cudd_ReadPermZdd(mgr, v_index) if level > u_level: raise AssertionError((level, u_level)) if level > v_level: raise AssertionError((level, v_level)) if level < u_level: pu, qu = u, DD_ZERO(mgr) else: pu, qu = cuddE(u), cuddT(u) if level < v_level: pv, qv = v, DD_ZERO(mgr) else: pv, qv = cuddE(v), cuddT(v) p = _disjoin(mgr, level + 1, pu, pv) if p is NULL: return NULL cuddRef(p) q = _disjoin(mgr, level + 1, qu, qv) if q is NULL: Cudd_RecursiveDerefZdd(mgr, p) return NULL cuddRef(q) r = _find_or_add(mgr, index, p, q) if r is NULL: Cudd_RecursiveDerefZdd(mgr, p) Cudd_RecursiveDerefZdd(mgr, q) return NULL cuddRef(r) Cudd_RecursiveDerefZdd(mgr, p) Cudd_RecursiveDerefZdd(mgr, q) cuddCacheInsert2( mgr, _disjoin_cache_id, u, v, r) cuddDeref(r) return r cpdef Function _c_conjoin( u: Function, v: Function): """Return the conjunction of `u` and `v`.""" r: DdRef cdef DdManager *mgr mgr = u.manager r = _conjoin_root( mgr, u.node, v.node) return wrap(u.bdd, r) cdef DdRef _conjoin_root( DdManager *mgr, u: DdRef, v: DdRef ) except NULL: """Return the conjunction of `u` and `v`.""" if mgr is NULL: raise AssertionError('`mgr is NULL`') if u is NULL: raise AssertionError('`u is NULL`') if v is NULL: raise AssertionError('`v is NULL`') mgr.reordered = 1 while mgr.reordered == 1: mgr.reordered = 0 r = _conjoin(mgr, 0, u, v) if r is NULL: raise AssertionError('`r is NULL`') return r # Similar to function `_disjoin_cache_id()`. # This function is used for the hash in cache. cdef DdRef _conjoin_cache_id( DdManager *mgr, u: DdRef, v: DdRef ) noexcept: """Used only as cache key. Passed inside the function `_conjoin()` to the C functions: - `cuddCacheLookup2Zdd()` - `cuddCacheInsert2()` """ cdef DdRef _conjoin( DdManager *mgr, level: _c_level, u: DdRef, v: DdRef ) except? NULL: """Recursively conjoin `u` and `v`. Asserts that: - `level` <= level of `u` - `level` <= level of `v` """ if level < 0: raise AssertionError( f'`{level = } < 0`') if u is NULL: raise AssertionError('`u is NULL`') if v is NULL: raise AssertionError('`v is NULL`') index = Cudd_ReadInvPermZdd(mgr, level) if u == DD_ZERO(mgr): return u if v == DD_ZERO(mgr): return v if index == -1 or index == CUDD_CONST_INDEX: if u != DD_ONE(mgr): raise AssertionError( 'nonconstant node `u` given, ' f'when given level: {level}, ' f'with index: {index}') if v != DD_ONE(mgr): raise AssertionError( 'nonconstant node `v` given, ' f'when given level: {level}, ' f'with index: {index}') return u r = cuddCacheLookup2Zdd( mgr, _conjoin_cache_id, u, v) if r is not NULL: return r u_index = Cudd_NodeReadIndex(u) u_level = Cudd_ReadPermZdd(mgr, u_index) v_index = Cudd_NodeReadIndex(v) v_level = Cudd_ReadPermZdd(mgr, v_index) if level > u_level: raise AssertionError((level, u_level)) if level > v_level: raise AssertionError((level, v_level)) if level < u_level: pu, qu = u, DD_ZERO(mgr) else: pu, qu = cuddE(u), cuddT(u) if level < v_level: pv, qv = v, DD_ZERO(mgr) else: pv, qv = cuddE(v), cuddT(v) p = _conjoin(mgr, level + 1, pu, pv) if p is NULL: return NULL cuddRef(p) q = _conjoin(mgr, level + 1, qu, qv) if q is NULL: Cudd_RecursiveDerefZdd(mgr, p) return NULL cuddRef(q) r = _find_or_add(mgr, index, p, q) if r is NULL: Cudd_RecursiveDerefZdd(mgr, p) Cudd_RecursiveDerefZdd(mgr, q) return NULL cuddRef(r) Cudd_RecursiveDerefZdd(mgr, p) Cudd_RecursiveDerefZdd(mgr, q) cuddCacheInsert2( mgr, _conjoin_cache_id, u, v, r) cuddDeref(r) return r cpdef Function _c_compose( u: Function, dvars: dict[ _VariableName, Function]): """Compute composition of `u` with `dvars`.""" r: DdRef cdef DdManager *mgr g: Function mgr = u.manager zdd = u.bdd n_declared_vars = len(zdd.vars) n_cudd_vars = zdd._number_of_cudd_vars() if n_declared_vars != n_cudd_vars: counts = _utils.var_counts(zdd) contiguous = _utils.contiguous_levels( '_c_compose', zdd) raise AssertionError(f'{counts}\n{contiguous}') # convert `dvars` to `DdRef *` cdef DdRef *vector vector = PyMem_Malloc( n_cudd_vars * sizeof(DdRef)) for var in zdd.vars: i = zdd._index_of_var[var] if var in dvars: g = dvars[var] else: g = zdd.var(var) cuddRef(g.node) if g.ref <= 0: raise AssertionError((var, g.ref)) vector[i] = g.node # compose r = NULL try: r = _compose_root(mgr, u.node, vector) finally: if r is not NULL: cuddRef(r) if r.ref <= 0: raise AssertionError(r.ref) for i in range(n_cudd_vars): Cudd_RecursiveDerefZdd(mgr, vector[i]) if r is not NULL: cuddDeref(r) PyMem_Free(vector) if r is NULL: raise AssertionError('r is NULL') return wrap(u.bdd, r) cdef DdRef _compose_root( DdManager *mgr, u: DdRef, DdRef *vector ) except NULL: """Root of recursive composition.""" if mgr is NULL: raise AssertionError('`mgr is NULL`') if u is NULL: raise AssertionError('`u is NULL`') if vector is NULL: raise AssertionError('`vector is NULL`') r: DdRef level = 0 mgr.reordered = 1 while mgr.reordered == 1: mgr.reordered = 0 table = dict() # table = cuddHashTableInit(mgr, 1, 2) # if table is NULL: # return NULL r = _compose(mgr, level, table, u, vector) # if mgr.reordered == 1: # if r is not NULL: # raise AssertionError(r) if r is not NULL: cuddRef(r) if r.ref <= 0: raise AssertionError(r.ref) # cuddHashTableQuitZdd(table) for nd in table.values(): Cudd_RecursiveDerefZdd(mgr, nd) if r is not NULL: cuddDeref(r) if r is NULL: raise AssertionError('`r is NULL`') return r cdef DdRef _compose( DdManager *mgr, level: _c_level, # DdHashTable *table, table: dict, u: DdRef, DdRef *vector ) except? NULL: """Recursively compute composition. The composition is defined in the array `vector`. Asserts that: - `level` <= level of `u` """ if level < 0: raise AssertionError( f'`{level = } < 0`') if u is NULL: raise AssertionError('`u is NULL`') if vector is NULL: raise AssertionError('`vector is NULL`') if u == DD_ZERO(mgr): return u if u == DD_ONE(mgr): r = Cudd_ReadZddOne(mgr, 0) if r is NULL: raise AssertionError( '`Cudd_ReadZddOne()` returned `NULL`.') return r t = (u, level) if t in table: return table[t] # r = cuddHashTableLookup1(table, u) # if r is not NULL: # return r u_index = Cudd_NodeReadIndex(u) u_level = Cudd_ReadPermZdd(mgr, u_index) if level > u_level: raise AssertionError((level, u_level)) index = Cudd_ReadInvPermZdd(mgr, level) g = vector[index] if g is NULL: raise AssertionError('`g is NULL`') if g.ref <= 0: raise AssertionError((index, g.ref)) if level < u_level: if level + 1 > u_level: raise AssertionError((level, u_level)) c = _compose( mgr, level + 1, table, u, vector) if c is NULL: return NULL cuddRef(c) if c.ref <= 0: raise AssertionError(c.ref) r = cuddZddIte(mgr, g, DD_ZERO(mgr), c) if r is NULL: Cudd_RecursiveDerefZdd(mgr, c) return NULL cuddRef(r) if r.ref <= 0: raise AssertionError(r.ref) Cudd_RecursiveDerefZdd(mgr, c) else: if level != u_level: raise AssertionError((level, u_level)) v, w = cuddE(u), cuddT(u) if v.ref <= 0: raise AssertionError(v.ref) if w.ref <= 0: raise AssertionError(w.ref) p = _compose( mgr, level + 1, table, v, vector) if p is NULL: return NULL cuddRef(p) if p.ref <= 0: raise AssertionError(p.ref) q = _compose( mgr, level + 1, table, w, vector) if q is NULL: Cudd_RecursiveDerefZdd(mgr, p) return NULL cuddRef(q) if q.ref <= 0: raise AssertionError(q.ref) r = cuddZddIte(mgr, g, q, p) if r is NULL: Cudd_RecursiveDerefZdd(mgr, q) Cudd_RecursiveDerefZdd(mgr, p) return NULL cuddRef(r) if r.ref <= 0: raise AssertionError(r.ref) Cudd_RecursiveDerefZdd(mgr, p) Cudd_RecursiveDerefZdd(mgr, q) # insert in the hash table cuddRef(r) table[t] = r # fanout = u.ref # tr = cuddHashTableInsert1(table, u, r, fanout) # if tr == 0: # Cudd_RecursiveDerefZdd(mgr, r) # return NULL cuddDeref(r) return r # This function is similar to `cuddHashTableQuit`. cdef bint cuddHashTableQuitZdd( DdHashTable * hash ) except False: """Shutdown a hash table. This function calls `Cudd_RecursiveDerefZdd()`. The function `cuddHashTableQuit()` calls `Cudd_RecursiveDeref()`. """ if hash is NULL: raise AssertionError('`hash is NULL`') cdef unsigned int i; cdef DdManager *dd = hash.manager; cdef DdHashItem *bucket; cdef DdHashItem **memlist cdef DdHashItem **nextmem; cdef unsigned int numBuckets = hash.numBuckets; for i in range(numBuckets): bucket = hash.bucket[i] while bucket is not NULL: Cudd_RecursiveDerefZdd(dd, bucket.value) bucket = bucket.next memlist = hash.memoryList while (memlist is not NULL): nextmem = memlist[0] FREE(memlist) memlist = nextmem FREE(hash.bucket) FREE(hash) return True cpdef set _c_support( u: Function): """Compute support of `u`.""" zdd = u.bdd n = max(zdd._var_with_index) + 1 cdef int *support support = PyMem_Malloc(n * sizeof(int)) for i in range(n): support[i] = 0 try: level = 0 if u.manager is NULL: raise AssertionError('`u.manager is NULL`') _support(u.manager, level, Cudd_Regular(u.node), support) _clear_markers(Cudd_Regular(u.node)) support_vars = set() for i in range(n): if support[i] == 1: var = zdd._var_with_index[i] support_vars.add(var) finally: PyMem_Free(support) return support_vars cdef bint _support( DdManager *mgr, level: _c_level, u: DdRef, int *support ) except False: """Recursively compute the support.""" if level < 0: raise AssertionError( f'`{level = } < 0`') if u is NULL: raise AssertionError('`u is NULL`') if support is NULL: raise AssertionError('`support is NULL`') index = Cudd_ReadInvPermZdd(mgr, level) # terminal ? if (u == DD_ZERO(mgr) or index == -1 or index == CUDD_CONST_INDEX): return True # visited ? if Cudd_IsComplement(u.next): return True u_index = Cudd_NodeReadIndex(u) u_level = Cudd_ReadPermZdd(mgr, u_index) if level > u_level: raise AssertionError((level, u_level)) v, w = Cudd_Regular(cuddE(u)), cuddT(u) if level < u_level: support[index] = 1 _support(mgr, level + 1, u, support) elif v == w: _support(mgr, level + 1, v, support) else: support[index] = 1 _support(mgr, level + 1, v, support) _support(mgr, level + 1, w, support) u.next = Cudd_Not(u.next) return True cdef bint _clear_markers( u: DdRef ) except False: """Recursively clear complementation bits.""" if u is NULL: raise AssertionError('`u is NULL`') if not Cudd_IsComplement(u.next): return True u.next = Cudd_Regular(u.next) if cuddIsConstant(u): return True v, w = Cudd_Regular(cuddE(u)), cuddT(u) _clear_markers(v) _clear_markers(w) return True cpdef _test_call_dealloc( u: Function): """Duplicates the code of `Function.__dealloc__`. For details read the docstring of the function `dd.cudd._test_call_dealloc`. """ self = u # the code of `Function.__dealloc__` follows: if self._ref < 0: raise AssertionError( "The lower bound `_ref` on the node's " f'reference count has value {self._ref}, ' 'which is unexpected and should never happen. ' 'Was the value of `_ref` changed from outside ' 'this instance?') assert self._ref >= 0, self._ref if self._ref == 0: return if self.node is NULL: raise AssertionError( 'The attribute `node` is a `NULL` pointer. ' 'This is unexpected and should never happen. ' 'Was the value of `_ref` changed from outside ' 'this instance?') # anticipate multiple calls to `__dealloc__` self._ref -= 1 # deref Cudd_RecursiveDerefZdd(self.manager, self.node) # avoid future access to deallocated memory self.node = NULL cpdef Function _call_method_disjoin( zdd: ZDD, level: _Level, u: Function, v: Function, cache: dict): """Wrapper of method `ZDD._disjoin()`. To enable testing the `cdef` method from Python. """ return zdd._disjoin(level, u, v, cache) cpdef Function _call_method_conjoin( zdd: ZDD, level: _Level, u: Function, v: Function, cache: dict): """Wrapper of method `ZDD._conjoin()`. Similar to `_call_method_disjoin()`. """ return zdd._conjoin(level, u, v, cache) cpdef Function _call_disjoin( level: _Level, u: Function, v: Function): """Wrapper of function `_disjoin()`. To enable testing the `cdef` function from Python. """ r = _disjoin( u.bdd.manager, level, u.node, v.node) return wrap(u.bdd, r) cpdef Function _call_conjoin( level: _Level, u: Function, v: Function): """Wrapper of function `_conjoin()`. Similar to the function `_call_disjoin()`. """ r = _conjoin( u.bdd.manager, level, u.node, v.node) return wrap(u.bdd, r) ================================================ FILE: dd/dddmp.py ================================================ """Parser for DDDMP file format. CUDD exports Binary Decision Diagrams (BDD) in DDDMP. For more details on the Decision Diagram DuMP (DDDMP) package, read the file [1] included in the CUDD distribution [2]. The text file format details can be found by reading the source code [3]. References ========== [1] Gianpiero Cabodi and Stefano Quer "DDDMP: Decision Diagram DuMP package" `cudd-X.Y.Z/dddmp/doc/dddmp-2.0-Letter.ps`, 2004 [2] [3] `cudd-X.Y.Z/dddmp/dddmpStoreBdd.c`, lines: 329--331, 345, 954 """ # Copyright 2014 by California Institute of Technology # All rights reserved. Licensed under BSD-3. # import logging import typing as _ty import astutils import ply.lex import ply.yacc import dd.bdd as _bdd logger = logging.getLogger(__name__) TABMODULE: _ty.Final = 'dd._dddmp_parser_state_machine' LEX_LOG: _ty.Final = 'dd.dddmp.lex_logger' YACC_LOG: _ty.Final = 'dd.dddmp.yacc_logger' PARSER_LOG: _ty.Final = 'dd.dddmp.parser_logger' class Lexer: """Token rules to build LTL lexer.""" def __init__( self, debug=False): reserved = { 'ver': 'VERSION', 'add': 'ADD', 'mode': 'FILEMODE', 'varinfo': 'VARINFO', 'dd': 'DD', 'nnodes': 'NNODES', 'nvars': 'NVARS', 'orderedvarnames': 'ORDEREDVARNAMES', 'nsuppvars': 'NSUPPVARS', 'suppvarnames': 'SUPPVARNAMES', 'ids': 'IDS', 'permids': 'PERMIDS', 'auxids': 'AUXIDS', 'nroots': 'NROOTS', 'rootids': 'ROOTIDS', 'rootnames': 'ROOTNAMES', # 'nodes': # 'NODES', # 'end': # 'END' } self.reserved = { f'.{k}': v for k, v in reserved.items()} self.misc = [ 'MINUS', 'DOT', 'NAME', 'NUMBER'] self.tokens = self.misc + list( sorted(self.reserved.values())) self.build(debug=debug) # token rules t_MINUS = r' \- ' t_DOT = r' \. ' t_NUMBER = r' \d+ ' t_ignore = ''.join(['\x20', '\t']) def t_KEYWORD( self, token): r""" \. [a-zA-Z] [a-zA-Z]* """ token.type = self.reserved.get( token.value, 'NAME') return token def t_NAME( self, token): r""" [a-zA-Z_] [a-zA-Z_@0-9'\.]* """ token.type = self.reserved.get( token.value, 'NAME') return token def t_comment( self, token): r' \# .* ' return def t_newline( self, token): r' \n+ ' def t_error( self, token): raise ValueError( f'Unexpected character "{token.value[0]}"') def build( self, debug=False, debuglog=None, **kwargs): """Create a lexer. @param kwargs: Same arguments as `ply.lex.lex`: - except for `module` (fixed to `self`) - `debuglog` defaults to `logger`. """ if debug and debuglog is None: debuglog = logging.getLogger(LEX_LOG) self.lexer = ply.lex.lex( module=self, debug=debug, debuglog=debuglog, **kwargs) class Parser: """Production rules to build LTL parser.""" def __init__( self): self.tabmodule = TABMODULE self._lexer = Lexer() self.tokens = self._lexer.tokens self.reset() self.parser = None def build( self, tabmodule=None, outputdir=None, write_tables=False, debug=False, debuglog=None): if tabmodule is None: tabmodule = self.tabmodule if debug and debuglog is None: debuglog = logger self._lexer.build(debug=debug) self.parser = ply.yacc.yacc( module=self, start='file', tabmodule=tabmodule, outputdir=outputdir, write_tables=write_tables, debug=debug, debuglog=debuglog) def parse( self, filename, debuglog=None): """Parse DDDMP file containing BDD.""" if self.parser is None: self.build() levels, roots = self._parse_header(filename, debuglog) self._parse_body(filename) return self.bdd, self.n_vars, levels, roots def _parse_header( self, filename, debuglog): self.reset() if debuglog is None: debuglog = logging.getLogger(PARSER_LOG) # parse header (small but inhomogeneous) with open(filename, 'r') as f: a = list() for line in f: if '.nodes' in line: break a.append(line) s = '\n'.join(a) lexer = self._lexer.lexer lexer.input(s) r = self.parser.parse(lexer=lexer, debug=debuglog) if r is None: raise Exception('failed to parse') self._assert_consistent() # prepare mapping from fixed var index to level among all vars # id2name = { # i: var # for i, var in zip(self.var_ids, self.support_vars)} c = self.var_extra_info if c == 0: logger.info('var IDs') id2permid = { i: k for i, k in zip(self.var_ids, self.permuted_var_ids)} self.info2permid = id2permid elif c == 1: logger.info('perm IDs') self.info2permid = {k: k for k in self.permuted_var_ids} elif c == 2: logger.info('aux IDs') raise NotImplementedError elif c == 3: logger.info('var names') self.info2permid = { var: k for k, var in enumerate(self.ordered_vars)} elif c == 4: logger.info('none') raise NotImplementedError else: raise Exception('unknown `varinfo` case') self.info2permid['T'] = self.n_vars + 1 # support_var_ord_ids = { # d['var_index'] for u, d in g.nodes(data=True)} # if len(support_var_ord_ids) != self.n_support_vars: # raise AssertionError(( # support_var_ord_ids, self.n_support_vars)) # prepare levels if self.ordered_vars is not None: levels = {var: k for k, var in enumerate(self.ordered_vars)} elif self.support_vars is not None: permid2var = { k: var for k, var in zip(self.permuted_var_ids, self.support_vars)} levels = { permid2var[k]: k for k in sorted(self.permuted_var_ids)} else: levels = { idx: level for level, idx in enumerate(self.permuted_var_ids)} roots = set(self.rootids) return levels, roots def _parse_body( self, filename): # parse nodes (large but very uniform) with open(filename, 'r') as f: for line in f: if '.nodes' in line: break for line in f: if '.end' in line: break u, info, index, v, w = line.split(' ') u, index, v, w = map(int, (u, index, v, w)) try: info = int(info) except ValueError: pass # info == 'T' or `str` var name if info not in self.info2permid: raise AssertionError( (info, self.info2permid)) self._add_node(u, info, index, v, w) if len(self.bdd) != self.n_nodes: raise AssertionError((len(self.bdd), self.n_nodes)) def _add_node( self, u, info, index, v, w): """Add new node to BDD. @type u, index, v, w: `int` @type info: `int` or `"T"` """ if v == 0: v = None elif v < 0: raise ValueError( 'only "else" edges ' f'can be complemented ({v = })') if w == 0: w = None # map fixed var index to level among all vars level = self.info2permid[info] # dddmp stores (high, low) # swap to (low, high), as used in `dd.bdd` self.bdd[u] = (level, w, v) def reset( self): self.bdd = dict() self.algebraic_dd = None self.var_extra_info = None self.n_nodes = None self.rootids = None self.n_roots = None # vars self.n_vars = None self.ordered_vars = None # support vars self.n_support_vars = None self.support_vars = None # permuted and aux vars self.var_ids = None self.permuted_var_ids = None self.aux_var_ids = None self.info2permid = None def _assert_consistent( self): """Check that the loaded attributes are reasonable.""" if self.support_vars is not None: if len(self.support_vars) != self.n_support_vars: raise AssertionError(( len(self.support_vars), self.n_support_vars, self.support_vars)) if self.ordered_vars is not None: if len(self.ordered_vars) != self.n_vars: raise AssertionError(( len(self.ordered_vars), self.n_vars, self.ordered_vars)) if len(self.var_ids) != self.n_support_vars: raise AssertionError(( len(self.var_ids), self.n_support_vars, self.var_ids)) if len(self.permuted_var_ids) != self.n_support_vars: raise AssertionError(( len(self.permuted_var_ids), self.n_support_vars, self.permuted_var_ids)) if self.aux_var_ids is not None: if len(self.aux_var_ids) != self.n_support_vars: raise AssertionError(( len(self.aux_var_ids), self.n_support_vars, self.aux_var_ids)) if len(self.rootids) != self.n_roots: raise AssertionError(( len(self.rootids), self.n_roots, self.rootids)) def p_file( self, p): """file : lines""" p[0] = True def p_lines_iter( self, p): """lines : lines line""" def p_lines_end( self, p): """lines : line""" def p_line( self, p): """line : version | mode | varinfo | diagram_name | nnodes | nvars | nsupportvars | supportvars | orderedvars | varids | permids | auxids | nroots | rootids | algdd | rootnames """ def p_version( self, p): """version : VERSION name MINUS number DOT number""" def p_text_mode( self, p): """mode : FILEMODE NAME""" f = p[2] if f == 'A': logger.debug('text mode') elif f == 'B': logger.debug('binary mode') raise Exception('This parser supports only text DDDMP format.') else: raise Exception(f'unknown DDDMP format: {f}') def p_varinfo( self, p): """varinfo : VARINFO number""" self.var_extra_info = p[2] def p_dd_name( self, p): """diagram_name : DD name""" self.bdd_name = p[2] def p_num_nodes( self, p): """nnodes : NNODES number""" self.n_nodes = p[2] def p_num_vars( self, p): """nvars : NVARS number""" self.n_vars = p[2] def p_nsupport_vars( self, p): """nsupportvars : NSUPPVARS number""" self.n_support_vars = p[2] def p_support_varnames( self, p): """supportvars : SUPPVARNAMES varnames""" self.support_vars = p[2] def p_ordered_varnames( self, p): """orderedvars : ORDEREDVARNAMES varnames""" self.ordered_vars = p[2] def p_varnames_iter( self, p): """varnames : varnames varname""" p[1].append(p[2]) p[0] = p[1] def p_varnames_end( self, p): """varnames : varname""" p[0] = [p[1]] def p_varname( self, p): """varname : name | number """ p[0] = p[1] def p_var_ids( self, p): """varids : IDS integers""" self.var_ids = p[2] def p_permuted_ids( self, p): """permids : PERMIDS integers""" self.permuted_var_ids = p[2] def p_aux_ids( self, p): """auxids : AUXIDS integers""" self.aux_var_ids = p[2] def p_integers_iter( self, p): """integers : integers number""" p[1].append(p[2]) p[0] = p[1] def p_integers_end( self, p): """integers : number""" p[0] = [p[1]] def p_num_roots( self, p): """nroots : NROOTS number""" self.n_roots = p[2] def p_root_ids( self, p): """rootids : ROOTIDS integers""" self.rootids = p[2] def p_root_names( self, p): """rootnames : ROOTNAMES varnames""" raise NotImplementedError def p_algebraic_dd( self, p): """algdd : ADD""" self.algebraic_dd = True def p_number( self, p): """number : NUMBER""" p[0] = int(p[1]) def p_neg_number( self, p): """number : MINUS NUMBER""" p[0] = -int(p[2]) def p_expression_name( self, p): """name : NAME""" p[0] = p[1] def p_error( self, p): raise Exception(f'Syntax error at "{p}"') def load( fname: str ) -> _bdd.BDD: """Return a `BDD` loaded from DDDMP file `fname`. If no `.orderedvarnames` appear in the file, then `.suppvarnames` and `.permids` are used instead. In the second case, the variable levels contains blanks. To avoid blanks, the levels are re-indexed here. This has no effect if `.orderedvarnames` appears in the file. DDDMP files are dumped by [CUDD]( http://vlsi.colorado.edu/~fabio/CUDD/). """ parser = Parser() bdd_succ, n_vars, levels, roots = parser.parse(fname) # reindex to ensure no blanks perm = {k: var for var, k in levels.items()} perm = {i: perm[k] for i, k in enumerate(sorted(perm))} new_levels = {var: k for k, var in perm.items()} old2new = {levels[var]: new_levels[var] for var in levels} # convert bdd = _bdd.BDD(new_levels) umap = {-1: -1, 1: 1} for j in range(len(new_levels) - 1, -1, -1): for u, (k, v, w) in bdd_succ.items(): # terminal ? if v is None: if w is not None: raise AssertionError(w) continue # non-terminal i = old2new[k] if i != j: continue p, q = umap[abs(v)], umap[w] if v < 0: p = -p r = bdd.find_or_add(i, p, q) umap[abs(u)] = r bdd.roots.update(roots) return bdd def _rewrite_tables( outputdir: str='./' ) -> None: """Write the parser table file, even if it exists.""" astutils.rewrite_tables(Parser, TABMODULE, outputdir) if __name__ == '__main__': _rewrite_tables() ================================================ FILE: dd/mdd.py ================================================ """Ordered multi-valued decision diagrams. References ========== Arvind Srinivasan, Timothy Kam, Sharad Malik, Robert K. Brayton "Algorithms for discrete function manipulation" IEEE International Conference on Computer-Aided Design (ICCAD), 1990 pages 92--95 Michael Miller and Rolf Drechsler "Implementing a multiple-valued decision diagram package" 28th International Symposium on Multiple-Valued Logic (ISMVL), 1998 pages 52--57 """ # Copyright 2015 by California Institute of Technology # All rights reserved. Licensed under BSD-3. # import collections.abc as _abc import itertools as _itr import logging import sys import typing as _ty import dd._abc import dd.bdd as _bdd import dd._utils as _utils logger = logging.getLogger(__name__) TABMODULE: _ty.Final = ( 'dd.mdd_parser_state_machine') PLY_LOG: _ty.Final = 'dd.mdd.ply' _Ref: _ty.TypeAlias = int _Level: _ty.TypeAlias = int _Successors: _ty.TypeAlias = tuple[ int | None, ...] class MDD: """Shared ordered multi-valued decision diagram. Represents a Boolean function of integer variables. Nodes are integers. The terminal node is 1. Complemented edges are represented as negated nodes. Values returned by methods are edges, possibly complemented. Edge 0 is never complemented. Attributes: - `vars` - `max_nodes` If you want a node to survive garbage collection, increment its reference counter: `mdd.incref(edge)` """ # dvars: # `dict` that maps each variable to a `dict` # that defines its: # - len # - level def __init__( self, dvars: dict[str, dict] | None=None): self._pred: dict[ tuple[int, ...], int]= dict() self._succ: dict[ int, _Successors ] = dict() self._ref: dict[ int, int ] = dict() self._max: int = 1 self._ite_table: dict = dict() if dvars is None: dvars = dict() else: i = len(dvars) self._succ[1] = (i, None) self._ref[1] = 0 self.vars: dict[str, dict] = dict(dvars) self._level_to_var: dict[ _Level, str ] | None = None self._parser = None self._free: set[int] = set() self.max_nodes: int = sys.maxsize def __len__( self): """Return number of BDD nodes.""" return len(self._succ) def __contains__( self, u: _Ref ) -> bool: """Return `True` if `u` is a BDD node.""" return abs(u) in self._succ def __iter__( self ) -> _abc.Iterable[int]: return iter(self._succ) def _allocate( self ) -> int: """Return free integer, mark it as occupied.""" if self._free: return self._free.pop() else: self._max += 1 return self._max def _release( self, u: int ) -> None: """Unmark integer from used ones.""" if u > self._max: raise AssertionError(u) if u in self._free: raise AssertionError(u) if u in self._succ: raise AssertionError(u) if u in self._pred: raise AssertionError(u) if u in self._ref: raise AssertionError(u) self._free.add(u) def incref( self, u: _Ref ) -> None: """Increment reference count of node `abs(u)`.""" self._ref[abs(u)] += 1 def decref( self, u: _Ref ) -> None: """Decrement reference count of node `abs(u)`, with 0 as min.""" if self._ref[abs(u)] > 0: self._ref[abs(u)] -= 1 def ref( self, u: _Ref ) -> int: """Return reference count for node `abs(u)`.""" return self._ref[abs(u)] def var_at_level( self, i: _Level ) -> str: """Return variable with level `i`.""" if self._level_to_var is None: self._level_to_var = { d['level']: var for var, d in self.vars.items()} return self._level_to_var[i] def level_of_var( self, var: str ) -> _Level: """Return level of variable `var`.""" return self.vars[var]['level'] def ite( self, g: _Ref, u: _Ref, v: _Ref ) -> _Ref: """Return node for `if g then u, else v`.""" # is g terminal ? if g == 1: return u elif g == -1: return v # g is non-terminal # already computed ? t = (g, u, v) w = self._ite_table.get(t) if w is not None: return w z = min(self._succ[abs(g)][0], self._succ[abs(u)][0], self._succ[abs(v)][0]) gc = self._top_cofactor(g, z) uc = self._top_cofactor(u, z) vc = self._top_cofactor(v, z) nodes = tuple(_itr.starmap( self.ite, zip(gc, uc, vc))) w = self.find_or_add(z, *nodes) # cache self._ite_table[t] = w return w def _top_cofactor( self, u: _Ref, level: _Level ) -> tuple[ _Ref, ...]: """Return topmost cofactors. If `level` is the topmost level of `u`, then return successors. Else return copies of `u`. """ varname = self.var_at_level(level) n = self.vars[varname]['len'] # leaf ? if abs(u) == 1: return (u,) * n u_level, *nodes = self._succ[abs(u)] if level < u_level: return (u,) * n def check( node ) -> int: if node: return node raise AssertionError(node) nodes = map(check, nodes) if level == u_level: if u > 0: return tuple(nodes) if u < 0: return tuple(-v for v in nodes) raise AssertionError( 'Expected `u != 0`, ' f'but: {u = }') raise AssertionError( 'for `u_level < level`, ' 'call instead the method ' '`cofactor()`. (here: ' f'{level = } and {u_level = }') def find_or_add( self, i: _Level, *nodes: _Ref ) -> _Ref: """Return node at level `i` with successors in `nodes`. @param i: level in `range(len(vars))` """ if not (0 <= i < len(self.vars)): raise ValueError(i) var = self.var_at_level(i) if len(nodes) != self.vars[var]['len']: raise ValueError( (var, len(nodes), self.vars[var]['len'])) if not nodes: # in case max == 0 raise ValueError(nodes) for u in nodes: if abs(u) not in self: raise ValueError(u) # canonicity of complemented edges if nodes[0] < 0: nodes = tuple(-u for u in nodes) r = -1 else: r = 1 # eliminate if len(set(nodes)) == 1: return r * nodes[0] # already exists ? t = (i, *nodes) u = self._pred.get(t) if u is not None: return r * u u = self._allocate() if u in self: raise AssertionError((self._succ, u, t)) # add node self._pred[t] = u self._succ[u] = t self._ref[u] = 0 # reference counting for v in nodes: self.incref(v) return r * u def collect_garbage( self, roots: _abc.Iterable[_Ref] | None=None ) -> None: """Recursively remove nodes with zero reference count.""" if roots is None: roots = self._ref unused = {u for u in roots if not self.ref(u)} # keep terminal if 1 in unused: unused.remove(1) while unused: u = unused.pop() if u == 1: raise AssertionError(u) # remove t = self._succ.pop(u) u_ = self._pred.pop(t) uref = self._ref.pop(u) self._release(u) if u != u_: raise AssertionError((u, u_)) if uref != 0: raise AssertionError(uref) if u not in self._free: raise AssertionError((u, self._free)) # recurse # decrement reference counters nodes = t[1:] for v in nodes: self.decref(v) # unused ? if not self._ref[abs(v)] and abs(v) != 1: unused.add(abs(v)) self._ite_table = dict() def to_expr( self, u: _Ref ) -> str: if u == 1: return str(u) elif u == -1: return '0' t = self._succ[abs(u)] i = t[0] nodes = t[1:] var = self.var_at_level(i) # group per target node c = tuple(set(nodes)) e = {x: self.to_expr(x) for x in c} cond = {v: set() for v in c} for j, x in enumerate(nodes): cond[x].add(j) # format cond_str = dict() for k, v in cond.items(): if len(v) == 1: (j,) = v cond_str[k] = f'= {j}' else: cond_str[k] = f'in {v}' x = c[0] s = 'if ({var} {j}): {p}, '.format( var=var, j=cond_str[x], p=e[x]) s += ', '.join( '\nelif ({var} {j}): {p}'.format( var=var, j=cond_str[x], p=e[x]) for x in c[1:]) if u < 0: s = f'! {s}' s = f'({s})' return s def apply( self, op: dd._abc.OperatorSymbol, u: _Ref, v: _Ref | None=None, w: _Ref | None=None ) -> _Ref: _utils.assert_operator_arity(op, v, w, 'bdd') if u not in self: raise ValueError(u) if v is not None and v not in self: raise ValueError(v) if w is not None and w not in self: raise ValueError(w) # unary if op in ('~', 'not', '!'): return -u # Implied by `assert_operator_arity()` above, # present here for type-checking. elif v is None: raise ValueError( '`v is None`') # binary elif op in ('or', r'\/', '|', '||'): return self.ite(u, 1, v) elif op in ('and', '/\\', '&', '&&'): return self.ite(u, v, -1) elif op in ('#', 'xor', '^'): return self.ite(u, -v, v) elif op in ('=>', '->', 'implies'): return self.ite(u, v, 1) elif op in ('<=>', '<->', 'equiv'): return self.ite(u, v, -v) elif op in ('diff', '-'): return self.ite(u, -v, -1) elif op in (r'\A', 'forall'): raise NotImplementedError( 'quantification is not implemented for MDDs.') elif op in (r'\E', 'exists'): raise NotImplementedError( 'quantification is not implemented for MDDs.') # Implied by `assert_operator_arity()` above, # present here for type-checking. elif w is None: raise ValueError( '`w is None`') # ternary elif op == 'ite': return self.ite(u, v, w) raise ValueError( f'unknown operator "{op}"') def dump( self, fname: str ) -> None: """Write MDD as a diagram to PDF file `fname`. @param fname: file name, ending with the substring `.pdf`. The diagram includes all nodes in the MDD. The diagram is created using GraphViz. """ g = _to_dot(self) if fname.endswith('.pdf'): filetype = 'pdf' elif fname.endswith('.dot'): filetype = 'dot' else: raise ValueError( f'unknown file extension: {fname}') g.dump( fname, filetype=filetype) def bdd_to_mdd( bdd, dvars: dict[str, dict] ) -> tuple[ MDD, dict]: """Return MDD for given BDD. Caution: collects garbage. `dvars` must: - map each MDD variable to the corresponding bits in `bdd` - give the order as "level" keys. """ # i = level in BDD # j = level in MDD # bit = BDD variable # var = MDD variable # # map from bits to integers bit_to_var = dict() for var, d in dvars.items(): bits = d['bitnames'] b = {bit: var for bit in bits} bit_to_var.update(b) # find target bit order order = list() # target levels = {d['level']: var for var, d in dvars.items()} m = len(levels) for j in range(m): var = levels[j] bits = dvars[var]['bitnames'] order.extend(bits) bit_to_sort = {bit: k for k, bit in enumerate(order)} # reorder bdd.collect_garbage() _bdd.reorder(bdd, order=bit_to_sort) # BDD -> MDD mdd = MDD(dvars) # zones of bits per integer var zones = dict() for var, d in dvars.items(): bits = d['bitnames'] lsb = bits[0] msb = bits[-1] min_level = bit_to_sort[lsb] max_level = bit_to_sort[msb] zones[var] = (min_level, max_level) # reverse edges pred = {u: set() for u in bdd} for u, (_, v, w) in bdd._succ.items(): if u <= 0: raise AssertionError(u) # terminal ? if u == 1: continue # non-terminal pred[abs(v)].add(u) pred[abs(w)].add(u) # find BDD nodes mentioned from above rm = set() for u, p in pred.items(): rc = bdd.ref(u) k = len(p) # number of predecessors # has external refs ? if rc > k: continue # has refs from outside zone ? i, _, _ = bdd._succ[u] bit = bdd.var_at_level(i) var = bit_to_var[bit] min_level, _ = zones[var] pred_levels = {bdd._succ[v][0] for v in p} min_pred_level = min(pred_levels) if min_pred_level < min_level: continue # referenced only from inside zone rm.add(u) pred = {u: p for u, p in pred.items() if u not in rm} # build layer by layer # TODO: use bins, instad of iterating through all nodes bdd.assert_consistent() # _debug_dump(pred, bdd) umap = dict() umap[1] = 1 for u, i, v, w in bdd.levels(skip_terminals=True): # ignore function ? if u not in pred: continue # keep `u` bit = bdd.var_at_level(i) var = bit_to_var[bit] bits = dvars[var]['bitnames'] bit_succ = list() for d in _enumerate_integer(bits): x = bdd.cofactor(u, d) bit_succ.append(x) # map edges int_succ = [umap[abs(z)] if z > 0 else -umap[abs(z)] for z in bit_succ] # add new MDD node at level j j = dvars[var]['level'] r = mdd.find_or_add(j, *int_succ) # cache # signed r, because low never inverted, # opposite to canonicity chosen for BDDs umap[u] = r return mdd, umap def _enumerate_integer( bits: list[str] ) -> _abc.Iterator[ dict[str, int]]: n = len(bits) for i in range(int(2**n)): values = list(reversed(bin(i).lstrip('-0b').zfill(n))) d = {bit: int(v) for bit, v in zip(bits, values)} for bit in bits[len(values):]: d[bit] = 0 yield d def _debug_dump( pred: _abc.Iterable, bdd ) -> None: """Dump nodes of `bdd`, coloring nodes in `pred`.""" g = _bdd._to_dot(bdd._succ, bdd) color = 'red' for u in pred: if u < 1: raise ValueError(u) g.add_node(u, color=color) for u in g.nodes: if u < 1: raise AssertionError(u) if u == 1: continue level, _, _ = bdd._succ[u] var = bdd.var_at_level(level) label = f'{var}-{u}' g.add_node(u, label=label) g.dump( 'bdd_colored.pdf', filetype='pdf') bdd.dump('bdd.pdf') def _to_dot( mdd: MDD ) -> _utils.DotGraph: g = _utils.DotGraph( graph_type='digraph') skeleton = list() subgraphs = dict() n = len(mdd.vars) + 1 for i in range(n): h = _utils.DotGraph( rank='same') g.subgraphs.append(h) subgraphs[i] = h # add phantom node u = f'"-{i}"' skeleton.append(u) h.add_node( u, label=str(i), shape='none') # auxiliary edges for ranking for i, u in enumerate(skeleton[:-1]): v = skeleton[i + 1] g.add_edge( u, v, style='invis') # add nodes for u, t in mdd._succ.items(): if u <= 0: raise AssertionError(u) i = t[0] nodes = t[1:] # terminal ? if nodes[0] is None: var = '1' else: var = mdd.var_at_level(i) # add node label = f'{var}-{u}' h = subgraphs[i] # level i h.add_node( u, label=label) # add edges if nodes[0] is None: continue # has successors for j, v in enumerate(nodes): label = str(j) # tail_label = '-1' if v < 0 else ' ' if v < 0: style = 'dashed' else: style = 'solid' g.add_edge( u, abs(v), label=label, style=style) return g ================================================ FILE: dd/py.typed ================================================ ================================================ FILE: dd/sylvan.pyx ================================================ """Cython interface to Sylvan. Reference ========= Tom van Dijk, Alfons Laarman, Jaco van de Pol "Multi-Core BDD Operations for Symbolic Reachability" PDMC 2012 """ # Copyright 2016 by California Institute of Technology # All rights reserved. Licensed under BSD-3. # import collections.abc as _abc import logging import pickle import signal import time import typing as _ty from cpython cimport bool as _py_bool from libcpp cimport bool import dd._abc as _dd_abc from dd import _parser from dd import bdd as _bdd from dd cimport c_sylvan as sy import dd._utils as _utils logger = logging.getLogger(__name__) _Yes: _ty.TypeAlias = _py_bool _Nat: _ty.TypeAlias = _dd_abc.Nat _Cardinality: _ty.TypeAlias = _dd_abc.Cardinality _VariableName: _ty.TypeAlias = _dd_abc.VariableName _Level: _ty.TypeAlias = _dd_abc.Level _VariableLevels: _ty.TypeAlias = _dd_abc.VariableLevels _Assignment: _ty.TypeAlias = _dd_abc.Assignment _Renaming: _ty.TypeAlias = _dd_abc.Renaming _Formula: _ty.TypeAlias = _dd_abc.Formula # TODO: check for invalid nodes returned by sylvan calls cdef class BDD: """Wrapper of Sylvan manager. Interface similar to `dd.bdd.BDD`. Variable names are strings. Attributes: - `vars`: `set` of bit names as strings """ cdef public object vars cdef public object _index_of_var cdef public object _var_with_index def __cinit__( self ) -> None: """Initialize BDD manager. Due to the architecture of `sylvan`, there is a single unique table, so you can create only one `BDD` instance at any given time. If the current `BDD` instance is `del`eted, then a new `BDD` instance can be created. But two `BDD` instances cannot coexist in the same process. """ sy.lace_init(0, 10**6) sy.lace_startup(0, NULL, NULL) sy.LACE_ME_WRAP sy.sylvan_init_package(1LL<<25, 1LL<<26, 1LL<<24, 1LL<<25) sy.sylvan_init_bdd(1) def __init__( self, memory_estimate=None, initial_cache_size=None ) -> None: # self.configure(reordering=True, max_cache_hard=MAX_CACHE) self.vars = set() self._index_of_var = dict() # map: str -> unique fixed int self._var_with_index = dict() def __dealloc__( self ) -> None: # n = len(self) # if n != 0: # raise AssertionError( # f'Still {n} nodes ' # 'referenced upon shutdown.') sy.LACE_ME_WRAP sy.sylvan_quit() sy.lace_exit() def __eq__( self: BDD, other: BDD | None ) -> _Yes: """Return `True` if `other` has same manager. If `other is None`, then return `False`. """ if other is None: return False # `sylvan` supports one manager only return True def __ne__( self: BDD, other: BDD | None ) -> _Yes: """Return `True` if `other` has different manager. If `other is None`, then return `True`. """ if other is None: return True # `sylvan` supports one manager only return False def __len__( self ) -> _Cardinality: """Return number of nodes with non-zero references.""" sy.LACE_ME_WRAP return sy.sylvan_count_refs() def __contains__( self, u: Function ) -> _Yes: if self is not u.bdd: raise ValueError(u) try: self.apply('not', u) return True except: return False def __str__( self ) -> str: return 'Binary decision diagram (Sylvan wrapper)' def configure( self, **kw ) -> dict[ str, _ty.Any]: """Has no effect, present for compatibility only. Compatibility here refers to `BDD` classes in other modules of the package `dd`. """ # TODO: use `sy.gc_enabled == 1` when not `static` garbage_collection = None d = dict( reordering=False, garbage_collection=garbage_collection, max_memory=None, loose_up_to=None, max_cache_soft=None, max_cache_hard=None, min_hit=None, max_growth=None, max_swaps=None, max_vars=None) return d cpdef tuple succ( self, u: Function): """Return `(level, low, high)` for `u`.""" i = u._index # level, assuming # static variable order v = u.low w = u.high # account for complement bit propagation if u.negated: v, w = ~ v, ~ w return i, v, w cdef incref( self, u: sy.BDD): sy.sylvan_ref(u) cdef decref( self, u: sy.BDD): sy.sylvan_deref(u) def declare( self, *variables: _VariableName ) -> None: """Add names in `variables` to `self.vars`.""" for var in variables: self.add_var(var) cpdef int add_var( self, var: _VariableName, index: int | None=None): """Return index of variable named `var`. If a variable named `var` exists, the assert that it has `index`. Otherwise, create a variable named `var` with `index` (if given). If no reordering has yet occurred, then the returned index equals the level, provided `add_var` has been used so far. """ sy.LACE_ME_WRAP # var already exists ? j = self._index_of_var.get(var) if j is not None: if not (j == index or index is None): raise AssertionError(j, index) return j # new var if index is None: index = len(self._index_of_var) j = index u = sy.sylvan_ithvar(j) if u == sy.sylvan_invalid: raise RuntimeError( f'failed to add var "{var}"') self._add_var(var, j) return j cpdef insert_var( self, var: _VariableName, level: _Level): """Create a new variable named `var`, at `level`.""" raise Exception( 'in `sylvan`, variable indices equal levels.\n' 'Call method `BDD.add_var` instead.') cdef _add_var( self, var: _VariableName, index: int): """Add to `self` a *new* variable named `var`.""" if var in self.vars: raise ValueError((var, self.vars)) if var in self._index_of_var: raise ValueError((var, self._index_of_var)) if index in self._var_with_index: raise ValueError((index, self._var_with_index)) self.vars.add(var) self._index_of_var[var] = index self._var_with_index[index] = var if (len(self._index_of_var) != len(self._var_with_index)): raise AssertionError(( len(self._index_of_var), len(self._var_with_index), self._index_of_var, self._var_with_index)) cpdef Function var( self, var: _VariableName): """Return node for variable named `var`.""" if var not in self._index_of_var: raise ValueError( f'undeclared variable "{var}", ' 'the declared variables are:\n' f'{self._index_of_var}') sy.LACE_ME_WRAP j = self._index_of_var[var] r = sy.sylvan_ithvar(j) return wrap(self, r) def var_at_level( self, level: _Level ) -> _VariableName: """Return name of variable at `level`.""" j = level # indices equal levels in `sylvan` if j not in self._var_with_index: levels = { var: self.level_of_var(var) for var in self._index_of_var} raise ValueError( f'no variable has level: {level}, ' 'the current levels of all variables ' f'are: {levels}') var = self._var_with_index[j] return var def level_of_var( self, var: _VariableName ) -> _Level: """Return level of variable named `var`.""" if var not in self._index_of_var: raise ValueError( f'undeclared variable "{var}", ' 'the declared variables are:' f'\n{self._index_of_var}') j = self._index_of_var[var] level = j return level cpdef set support( self, f: Function): """Return the variables that node `f` depends on.""" if self is not f.bdd: raise ValueError(f) sy.LACE_ME_WRAP cube: sy.BDD cube = sy.sylvan_support(f.node) ids = set() while cube != sy.sylvan_true: # get var j = sy.sylvan_var(cube) ids.add(j) # descend u = sy.sylvan_low(cube) v = sy.sylvan_high(cube) if u != sy.sylvan_false: raise AssertionError(u) cube = v support = {self._var_with_index[j] for j in ids} return support cpdef Function let( self, definitions: _Renaming | _Assignment | dict[_VariableName, Function], u: Function): """Replace variables with `definitions` in `u`.""" d = definitions if not d: logger.warning( 'Call to `BDD.let` with no effect: ' '`defs` is empty.') return u var = next(iter(d)) value = d[var] if isinstance(value, _py_bool): return self._cofactor(u, d) elif isinstance(value, Function): return self._compose(u, d) try: value + 's' except TypeError: raise ValueError( 'Key must be variable name as `str`, ' 'or Boolean value as `bool`, ' f'or BDD node as `int`. Got: {value}') return self._rename(u, d) cpdef Function _compose( self, u: Function, var_sub: dict[_VariableName, Function]): if self is not u.bdd: raise ValueError(u) sy.LACE_ME_WRAP map: sy.BDDMAP j: sy.BDDVAR r: sy.BDD g: Function map = sy.sylvan_map_empty() for var, g in var_sub.items(): j = self._index_of_var[var] map = sy.sylvan_map_add(map, j, g.node) r = sy.sylvan_compose(u.node, map) return wrap(self, r) cpdef Function _cofactor( self, u: Function, values: _Assignment): """Return the cofactor f|_g.""" var_sub = { var: self.true if value else self.false for var, value in values.items()} return self._compose(u, var_sub) cpdef Function _rename( self, u: Function, dvars: _Renaming): """Return node `u` after renaming variables in `dvars`.""" if self is not u.bdd: raise ValueError(u) var_sub = { var: self.var(sub) for var, sub in dvars.items()} r = self._compose(u, var_sub) return r def pick( self, u: Function, care_vars: _abc.Collection[ _VariableName] | None=None ) -> _Assignment: """Return a single assignment as `dict`.""" return next(self.pick_iter(u, care_vars), None) def pick_iter( self, u: Function, care_vars: _abc.Collection[ _VariableName] | None=None ) -> _abc.Iterable[ _Assignment]: """Return generator over assignments.""" support = self.support(u) if care_vars is None: care_vars = support missing = {v for v in support if v not in care_vars} if missing: logger.warning( 'Missing bits: ' f'support - care_vars = {missing}') cube = dict() value = True config = self.configure(reordering=False) for cube in self._sat_iter(u, cube, value, support): for m in _bdd._enumerate_minterms(cube, care_vars): yield m self.configure(reordering=config['reordering']) def _sat_iter( self, u: Function, cube: _Assignment, value: _py_bool, support: set[_VariableName] ) -> _Assignment: """Recurse to enumerate models.""" if u.negated: value = not value # terminal ? if u.var is None: # high nodes are negated # the constant node is 0 if not value: if not set(cube).issubset(support): raise AssertionError( set(cube).difference(support)) yield cube return # non-terminal _, v, w = self.succ(u) var = u.var d0 = dict(cube) d0[var] = False d1 = dict(cube) d1[var] = True for x in self._sat_iter(v, d0, value, support): yield x for x in self._sat_iter(w, d1, value, support): yield x cpdef int count( self, u: Function): """Return number of models of node `u`.""" sy.LACE_ME_WRAP support = self.support(u) cube = self.cube(support) n_models = sy.sylvan_satcount( u.node, cube.node) if n_models < 0: raise AssertionError(n_models) return int(n_models) cpdef Function ite( self, g: Function, u: Function, v: Function): if self is not g.bdd: raise ValueError(g) if self is not u.bdd: raise ValueError(u) if self is not v.bdd: raise ValueError(v) sy.LACE_ME_WRAP r: sy.BDD r = sy.sylvan_ite(g.node, u.node, v.node) return wrap(self, r) cpdef Function apply( self, op: _dd_abc.OperatorSymbol, u: Function, v: _ty.Optional[Function] =None, w: _ty.Optional[Function] =None): """Return as `Function` the result of applying `op`.""" _utils.assert_operator_arity(op, v, w, 'bdd') if self is not u.bdd: raise ValueError(u) if v is not None and v.bdd is not self: raise ValueError(v) if w is not None and w.bdd is not self: raise ValueError(w) r: sy.BDD sy.LACE_ME_WRAP # unary if op in ('~', 'not', '!'): r = sy.sylvan_not(u.node) # binary elif op in ('and', '/\\', '&', '&&'): r = sy.sylvan_and(u.node, v.node) elif op in ('or', r'\/', '|', '||'): r = sy.sylvan_or(u.node, v.node) elif op in ('#', 'xor', '^'): r = sy.sylvan_xor(u.node, v.node) elif op in ('=>', '->', 'implies'): r = sy.sylvan_imp(u.node, v.node) elif op in ('<=>', '<->', 'equiv'): r = sy.sylvan_biimp(u.node, v.node) elif op in ('diff', '-'): r = sy.sylvan_diff(u.node, v.node) elif op in (r'\A', 'forall'): r = sy.sylvan_forall(u.node, v.node) elif op in (r'\E', 'exists'): r = sy.sylvan_exists(u.node, v.node) elif op == 'ite': r = sy.sylvan_ite(u.node, v.node, w.node) else: raise AssertionError(op) if r == sy.sylvan_invalid: raise ValueError( f'unknown operator: "{op}"') return wrap(self, r) cpdef Function cube( self, dvars: _Assignment | set[_VariableName]): """Return node for cube over `dvars`. @param dvars: maps each variable to a `bool`. If `set` given, then all values assumed `True`. """ # TODO: call sylvan cube function u = self.true if isinstance(dvars, set): for var in dvars: u &= self.var(var) return u for var, sign in dvars.items(): v = self.var(var) if sign is False: v = ~v u &= v return u cpdef Function quantify( self, u: Function, qvars: _abc.Iterable[ _VariableName], forall: _Yes=False): """Abstract variables `qvars` from node `u`.""" if self is not u.bdd: raise ValueError(u) sy.LACE_ME_WRAP c = set(qvars) cube = self.cube(c) # quantify if forall: r = sy.sylvan_forall(u.node, cube.node) else: r = sy.sylvan_exists(u.node, cube.node) return wrap(self, r) cpdef Function forall( self, qvars: _abc.Iterable[ _VariableName], u: Function): """Quantify `qvars` in `u` universally. Wraps method `quantify` to be more readable. """ return self.quantify(u, qvars, forall=True) cpdef Function exist( self, qvars: _abc.Iterable[ _VariableName], u: Function): """Quantify `qvars` in `u` existentially. Wraps method `quantify` to be more readable. """ return self.quantify(u, qvars, forall=False) cpdef assert_consistent( self): """Raise `AssertionError` if not consistent.""" # c = Cudd_DebugCheck(self.manager) # if c != 0: # raise AssertionError(c) n = len(self.vars) m = len(self._var_with_index) k = len(self._index_of_var) if n != m: raise AssertionError((n, m)) if m != k: raise AssertionError((m, k)) def add_expr( self, expr: _Formula ) -> Function: """Return node for `str` expression `e`.""" return _parser.add_expr(expr, self) def to_expr( self, u: Function ) -> _Formula: if self is not u.bdd: raise ValueError(u) raise NotImplementedError() cpdef dump( self, u: Function, fname: str): """Dump BDD as DDDMP file `fname`.""" if self is not u.bdd: raise ValueError(u) raise NotImplementedError() cpdef load( self, fname: str): """Return `Function` loaded from file `fname`.""" raise NotImplementedError() @property def false( self ) -> Function: """`Function` for Boolean value false.""" return self._bool(False) @property def true( self ) -> Function: """`Function` for Boolean value true.""" return self._bool(True) cdef Function _bool( self, v: _py_bool): """Return terminal node for Boolean `v`.""" r: sy.BDD if v: r = sy.sylvan_true else: r = sy.sylvan_false return wrap(self, r) cpdef Function restrict( u: Function, care_set: Function): if u.bdd is not care_set.bdd: raise ValueError((u, care_set)) sy.LACE_ME_WRAP r: sy.BDD r = sy.sylvan_restrict(u.node, care_set.node) return wrap(u.bdd, r) cpdef Function and_exists( u: Function, v: Function, qvars: set[_VariableName]): r"""Return `\E qvars: u /\ v`.""" if u.bdd is not v.bdd: raise ValueError((u, v)) bdd = u.bdd sy.LACE_ME_WRAP cube = bdd.cube(qvars) r = sy.sylvan_and_exists(u.node, v.node, cube.node) return wrap(u.bdd, r) cpdef Function or_forall( u: Function, v: Function, qvars: set[_VariableName]): r"""Return `\A qvars: u \/ v`.""" if u.bdd is not v.bdd: raise ValueError((u, v)) bdd = u.bdd sy.LACE_ME_WRAP cube = bdd.cube(qvars) r = sy.sylvan_and_exists( sy.sylvan_not(u.node), sy.sylvan_not(v.node), cube.node) r = sy.sylvan_not(r) return wrap(u.bdd, r) cpdef reorder( bdd: BDD, dvars: _VariableLevels | None=None): """Reorder `bdd` to order in `dvars`. If `dvars` is `None`, then invoke group sifting. """ raise NotImplementedError def copy_vars( source: BDD, target: BDD ) -> None: """Copy variables, preserving Sylvan indices.""" for var, index in source._index_of_var.items(): target.add_var(var, index=index) cdef Function wrap( bdd: BDD, node: sy.BDD): """Return a `Function` that wraps `node`.""" f = Function() f.init(node, bdd) return f cdef class Function: """Wrapper of `BDD` from Sylvan. Attributes (those that are properties are described in their docstrings): - `_index` - `var` - `low` - `high` - `negated` - `support` - `dag_size` In Python, use as: ```python from dd import sylvan bdd = sylvan.BDD() u = bdd.true v = bdd.false w = u | ~ v ``` In Cython, use as: ```cython bdd = BDD() u: sy.BDD u = sylvan_true f = Function() f.init(u) ``` """ __weakref__: object cdef public BDD bdd node: sy.BDD cdef init( self, u: sy.BDD, bdd: BDD): if u == sy.sylvan_invalid: raise ValueError( '`sy.BDD u` is `NULL` pointer.') self.bdd = bdd self.node = u sy.sylvan_ref(u) def __hash__( self ) -> int: return int(self) @property def _index( self ) -> _Nat: """Index of `self.node`.""" return sy.sylvan_var(self.node) @property def var( self ) -> _VariableName: """Variable at level where this node is.""" if sy.sylvan_isconst(self.node): return None return self.bdd._var_with_index[self._index] @property def level( self ) -> _Level: """Level where this node is.""" raise NotImplementedError @property def ref( self ) -> _Cardinality: """Sum of reference counts of node and its negation.""" # u = Cudd_Regular(self.node) # return u.ref raise NotImplementedError @property def low( self ) -> Function: """Return "else" node as `Function`.""" # propagates complement bit u = sy.sylvan_low(self.node) return wrap(self.bdd, u) @property def high( self ) -> Function: """Return "then" node as `Function`.""" u = sy.sylvan_high(self.node) return wrap(self.bdd, u) @property def negated( self ) -> _Yes: """Return `True` if `self` is a complemented edge.""" # read the definition of `BDD_HASMARK` # in `sylvan_bdd` if self.node & sy.sylvan_complement: return True else: return False @property def support( self ) -> set[_VariableName]: """Return `set` of variables in support.""" return self.bdd.support(self) def __dealloc__( self ) -> None: sy.sylvan_deref(self.node) self.node = 0 def __str__( self ) -> str: return ( 'Function(DdNode with: ' f'node={self.node}, ' f'var_index={self._index}, ' f'ref_count={None})') def __len__( self ) -> _Cardinality: return sy.sylvan_nodecount(self.node) @property def dag_size( self ) -> _Cardinality: """Return number of BDD nodes. This is the number of BDD nodes that are reachable from this BDD reference, i.e., with `self` as root. """ return len(self) def __eq__( self: Function, other: Function | None ) -> _Yes: if other is None: return False other_: Function = other if self.bdd is not other_.bdd: raise ValueError((self, other_)) return self.node == other_.node def __ne__( self: Function, other: Function | None ) -> _Yes: if other is None: return True other_: Function = other if self.bdd is not other_.bdd: raise ValueError((self, other_)) return self.node != other_.node def __invert__( self: Function ) -> Function: r = sy.sylvan_not(self.node) return wrap(self.bdd, r) def __and__( self: Function, other: Function ) -> Function: if self.bdd is not other.bdd: raise ValueError((self, other)) sy.LACE_ME_WRAP r = sy.sylvan_and(self.node, other.node) return wrap(self.bdd, r) def __or__( self: Function, other: Function ) -> Function: if self.bdd is not other.bdd: raise ValueError((self, other)) sy.LACE_ME_WRAP r = sy.sylvan_or(self.node, other.node) return wrap(self.bdd, r) def __xor__( self: Function, other: Function ) -> Function: if self.bdd is not other.bdd: raise ValueError((self, other)) sy.LACE_ME_WRAP r = sy.sylvan_xor(self.node, other.node) return wrap(self.bdd, r) def count( self: Function ) -> _Cardinality: return self.bdd.count(self) ================================================ FILE: doc.md ================================================ # `dd` documentation # Table of Contents - [Design principles](#design-principles) - [Create and plot a binary decision diagram](#create-and-plot-a-binary-decision-diagram) - [Using a `BDD` manager](#using-a-bdd-manager) - [Plotting](#plotting) - [Alternatives](#alternatives) - [Reminders about the implementation beneath](#reminders-about-the-implementation-beneath) - [Pickle](#pickle) - [Nodes as `Function` objects](#nodes-as-function-objects) - [BDD equality](#bdd-equality) - [Other methods](#other-methods) - [CUDD interface: `dd.cudd`](#cudd-interface-ddcudd) - [Functions](#functions) - [Lower level: `dd.bdd`](#lower-level-ddbdd) - [Reference counting](#reference-counting) - [Reordering](#reordering) - [Other methods](#other-methods) - [Example: Reachability analysis](#example-reachability-analysis) - [Syntax for quantified Boolean formulas](#syntax-for-quantified-boolean-formulas) - [Multi-valued decision diagrams (MDD)](#multi-valued-decision-diagrams-mdd) - [Installation of C extension modules](#installation-of-c-extension-modules) - [Environment variables that activate C extensions](#environment-variables-that-activate-c-extensions) - [Alternative: directly running `setup.py`](#alternative-directly-running-setuppy) - [Using the package `build`](#using-the-package-build) - [Customizing the C compilation](#customizing-the-c-compilation) - [Installing the development version](#installing-the-development-version) - [Footnotes](#footnotes) - [Copying](#copying) ## Design principles The interface is in Python. The representation depends on what you want and have installed. For solving small to medium size problems, say for teaching, or prototyping new algorithms, pure Python can be more convenient. To work with larger problems, it works better if you install the C library [CUDD](https://web.archive.org/web/http://vlsi.colorado.edu/~fabio/CUDD/html/index.html). Let's call these “backends”. The same user code can run with both the Python and C backends. You only need to modify an `import dd.autoref as _bdd` to `import dd.cudd as _bdd`, or import the best available interface: ```python try: import dd.cudd as _bdd except ImportError: import dd.autoref as _bdd ``` The following sections describe how to use the high level interface (almost identical in `autoref` and `cudd`). The lower level interface to the pure-Python implementation in `dd.bdd` is also described, for those interested in more details. ## Create and plot a binary decision diagram The starting point for using a BDD library is a *shared* reduced ordered binary decision diagram. Implementations call this *manager*. The adjectives mean: - binary: each node represents a propositional formula - ordered: variables have a fixed order that changes in a controlled way - reduced: given variable order, equivalent propositional formulas are represented by a unique diagram - shared: common subformulas are represented using common elements in memory. The manager is a directed graph, with each node representing a formula. Each formula can be understood as a collection of assignments to variables. As mentioned above, the variables are ordered. The *level* of a variable is its index in this order, starting from 0. The *terminal* nodes correspond to `TRUE` and `FALSE`, with maximal index. Each manager is a `BDD` class, residing in a different module: - `dd.autoref.BDD`: high-level interface to pure Python implementation - `dd.cudd.BDD`: same high-level interface to a C implementation - `dd.sylvan.BDD`: interface to another C implementation (multi-core) - `dd.bdd.BDD`: low-level interface to pure Python implementation (wrapped by `dd.autoref.BDD`). The main difference between these modules is how a BDD node is represented: - `autoref`: `autoref.Function` - `cudd`: `cudd.Function` - `sylvan`: `sylvan.Function` - `bdd`: `int` In `autoref` and `cudd`, a `Function` class represents a node. In `bdd`, a signed integer represents a node. All implementations use negated edges, so logical negation takes *constant* time. ### Using a `BDD` manager Roughly four kinds of operations suffice to perform most tasks: - creating BDDs from formulas - quantification (`forall`, `exist`) - substitution (`let`) - enumerating models that satisfy a BDD First, instantiate a manager and declare variables ```python import dd.autoref as _bdd bdd = _bdd.BDD() bdd.declare('x', 'y', 'z') ``` To create a BDD node for a propositional formula, call the parser ```python u = bdd.add_expr(r'x /\ y') # conjunction v = bdd.add_expr(r'z \/ ~ y') # disjunction and negation w = u & ~ v ``` The formulas above are in [TLA+](https://en.wikipedia.org/wiki/TLA%2B) syntax. If you prefer the syntax `&, |, !`, the parser recognizes those operators too. The inverse of `BDD.add_expr` is `BDD.to_expr`: ```python s = bdd.to_expr(u) print(s) # 'ite(x, y, False)' ``` Lets create the BDD of a more colorful Boolean formula ```python s = r'(x /\ y) <=> (~ z \/ ~ (y <=> x))' v = bdd.add_expr(s) ``` In natural language, the expression `s` reads: “(x and y) if and only if ( (not z) or (y xor x) )”. The Boolean constants are `bdd.false` and `bdd.true`, and in syntax `FALSE` and `TRUE`. The available operators are listed in the [Syntax for quantified Boolean formulas](#syntax-for-quantified-boolean-formulas) section. Variables can be quantified by calling the methods `exist` and `forall`. They symbolically consider all values of the variables mentioned in the quantification, and these variables are therefore not present in the result. ```python u = bdd.add_expr(r'x /\ y') v = bdd.exist(['x'], u) print(v.to_expr()) # 'y' ``` Existential quantification can also be done by writing quantified formulas ```python # there exists a value of x, such that (x and y) u = bdd.add_expr(r'\E x: x /\ y') y = bdd.add_expr('y') assert u == y, (u, y) # forall x, there exists y, such that (y or x) u = bdd.add_expr(r'\A x: \E y: y \/ z') assert u == bdd.true, u ``` `dd` supports "inline BDD references" via the `@` operator. Each BDD node `u` has an integer representation `int(u)` and a string representation `str(u)`. For example, if the integer representation is `5`, then the string representation is `@5`. These enable you to mention existing BDD nodes in formulas, without the need to expand them as formulas. For example: ```python u = bdd.add_expr(r'y \/ z') s = rf'x /\ {u}' v = bdd.add_expr(s) v_ = bdd.add_expr(r'x /\ (y \/ z)') assert v == v_ ``` Substitution comes in several forms: - replace some variable names by other variable names - replace some variable names by Boolean constants - replace some variable names by BDD nodes, so by arbitrary formulas All these kinds of substitution are performed via the method `let`, which takes a `dict` that maps the variable names to replacements. To substitute some variables for some other variables ```python bdd.declare('x', 'p', 'y', 'q', 'z') u = bdd.add_expr(r'x \/ (y /\ z)') # substitute variables for variables (rename) d = dict(x='p', y='q') v = bdd.let(d, u) print(f'support = {v.support}') # support = {'p', 'q', 'z'} ``` The other forms of substitution are similar ```python # substitute constants for variables (cofactor) values = dict(x=True, y=False) v = bdd.let(values, u) # substitute BDDs for variables (compose) d = dict(x=bdd.add_expr(r'z \/ w')) v = bdd.let(d, u) ``` Replacement variables are (conceptually) inserted in-place in the expression. If the expression also contains other occurrences of the replacements (like variable `p` in the example below), the result may not be what is expected: ```python bdd.declare('x', 'p') u = bdd.add_expr(r'x /\ ~ p') d = dict(x='p') # rename `x` to `p` v = bdd.let(d, u) # equivalent to `p /\ ~ p`, which is `FALSE` assert v == bdd.false ``` To rename variables without side-effects like above, it is useful to first remove all occurrences of the replacement variables in the expression. Depending on the algorithm being implemented, it might be appropriate to first rename variable `p` above before the substitution, or to quantify it. A BDD represents a formula, a syntactic object. Semantics is about how syntax is used to describe the world. We could interpret the same formula using different semantics and reach different conclusions. A formula is usually understood as describing some assignments of values to variables. Such an assignment is also called a *model*. A model is represented as a `dict` that maps variable names to values. ```python u = bdd.add_expr(r'x \/ y') assignment = bdd.pick(u) # choose an assignment, `u.pick()` works too print(f'{assignment = }') # assignment = {'x': False, 'y': True} ``` When working with BDDs, two issues arise: - which variable names are present in an assignment? - what values do the variables take? The values are Boolean, because BDD machinery is designed to reason for that case only, so `1 /\ 5` is a formula outside the realm of BDD reasoning. The choice of variable names is a matter we discuss below. Consider the example ```python bdd.declare('x', 'y', 'z') u = bdd.add_expr(r'x \/ y') support = u.support print(f'{support = }') # support = {'x', 'y'} ``` This tells us that the variables `x` and `y` occur in the formula that the BDD node `u` represents. Knowing what (Boolean) values a model assigns to the variables `x` and `y` suffices to decide whether the model satisfies `u`. In other words, the values of other variables, like `z`, are irrelevant to evaluating the expression `x \/ y`. The choice of semantics is yours. Which variables you want an assignment to mention depends on what you are doing with the assignment in your algorithm. ```python u = bdd.add_expr('x') # default: variables in support(u) models = list(bdd.pick_iter(u)) print(models) # [{'x': True}] # variables in `care_vars` models = list(bdd.pick_iter(u, care_vars=['x', 'y'])) print(models) # [{'x': True, 'y': False}, {'x': True, 'y': True}] ``` By default, `pick_iter` returns assignments to all variables in the support of the BDD node `u` given as input. In this example, the support of `u` contains one variable: `x` (because the value of the expression `'x'` is independent of variable `y`). We can use the argument `care_vars` to specify the variables that we want the assignment to include. The assignments returned will include all variables in `care_vars`, plus the variables that appear along each path traversed in the BDD. Variables in `care_vars` that are unassigned along each path will be exhaustively enumerated (i.e., all combinations of `True` and `False`). For example, if `care_vars == []`, then the assignments will contain only those variables that appear along the recursive traversal of the BDD. If `care_vars == support(u)`, then the result equals the default result. For `care_vars > support(u)` we will observe more variables in each assignment than the variables in the support. We can also count how many assignments satisfy a BDD. The number depends on how many variables occur in an assignment. The default number is as many variables are contained in the support of that node. You can pass a larger number ```python bdd.declare('x', 'y') u = bdd.add_expr('x') count = u.count() print(f'{count = }') # count = 1 models = list(bdd.pick_iter(u)) print(models) # [{'x': True}] # pass a larger number of variables count = u.count(nvars=3) print(f'{count = }') # count = 4 models = list(bdd.pick_iter(u, ['x', 'y', 'z'])) print(models) # [{'x': True, 'y': False, 'z': False}, # {'x': True, 'y': True, 'z': False}, # {'x': True, 'y': False, 'z': True}, # {'x': True, 'y': True, 'z': True}] ``` A convenience method for creating a BDD from an assignment `dict` is ```python d = dict(x=True, y=False, z=True) u = bdd.cube(d) v = bdd.add_expr(r'x /\ ~ y /\ z') assert u == v, (u, v) ``` The interface is specified in the module `dd._abc`. Although [internal]( https://www.python.org/dev/peps/pep-0008/#descriptive-naming-styles), you may want to take a look at the `_abc` module. Above we discussed semantics from a [proof-theoretic viewpoint]( https://en.wikipedia.org/wiki/Metamathematics). The same discussion can be rephrased in terms of function domains containing assignments to variables, so a [model-theoretic viewpoint]( https://en.wikipedia.org/wiki/Model_theory). ### Plotting You can dump a PDF of all nodes in the manager as follows ```python import dd.autoref as _bdd bdd = _bdd.BDD() bdd.declare('x', 'y', 'z') u = bdd.add_expr(r'(x /\ y) \/ ~ z') bdd.collect_garbage() # optional bdd.dump('awesome.pdf') ``` The result is shown below, with the meaning: - integers on the left signify levels (thus variables) - each node is annotated with a variable name (like `x`) dash the node index in `dd.bdd.BDD._succ` (mainly for debugging purposes) - solid arcs represent the “if” branches - dashed arcs the “else” branches - only an “else” branch can be negated, signified by a `-1` annotation Negated edges mean that logical negation, i.e., `~`, is applied to the node that is pointed to. Negated edges and BDD theory won't be discussed here, please refer to a reference from those listed in the docstring of the module `dd.bdd`. For example, [this document]( http://www.ecs.umass.edu/ece/labs/vlsicad/ece667/reading/somenzi99bdd.pdf) by Fabio Somenzi (CUDD's author). In the following diagram, the BDD rooted at node `x-7` represents the Boolean function `(x /\ y) \/ ~ z`. For example, for the assignment `dict(x=False)`, the dashed arc from node `x-7` leads to the negation (due to the negated edge, signified by a `-1`) of the node `z-5`. The BDD rooted at `z-5` represents the Boolean function `z`, so its negation is `~ z`. The nodes `x-2`, `x-4`, `y-3` are intermediate results that result while constructing the BDD for the Boolean function `(x /\ y) \/ ~ z`. The BDD rooted at node `x-2` represents the Boolean function `x`, and the BDD rooted at node `x-4` represents the Boolean function `x /\ y`. ![example_bdd](https://rawgithub.com/johnyf/binaries/main/dd/awesome.png) An external reference to a BDD is an arc that points to a node. For example, `u` above is an external reference. An external reference can be a complemented arc. External references can be included in a BDD diagram by using the argument `roots` of the method `BDD.dump`. For example ```python import dd.autoref as _bdd bdd = _bdd.BDD() bdd.declare('x', 'y', 'z') u = bdd.add_expr(r'(x /\ y) \/ ~ z') print(u.negated) v = ~ u print(v.negated) bdd.collect_garbage() bdd.dump('rooted.pdf', roots=[v]) ``` The result is the following diagram, where the node `@-7` is the external reference `v`, which is a complemented arc. ![example_bdd](https://rawgithub.com/johnyf/binaries/main/dd/rooted.png) It is instructive to dump the `bdd` with and without collecting garbage. ### Alternatives As mentioned above, there are various ways to apply propositional operators ```python x = bdd.var('x') y = bdd.var('y') u = x & y u = bdd.apply('and', x, y) u = bdd.apply('/\\', x, y) # TLA+ syntax u = bdd.apply('&', x, y) # Promela syntax ``` Infix Python operators work for BDD nodes in `dd.autoref` and `dd.cudd`, not in `dd.bdd`, because nodes there are plain integers (`int`). Besides the method `apply`, there is also the ternary conditional method `ite`, but that is more commonly used internally. For single variables, the following are equivalent ```python u = bdd.add_expr('x') u = bdd.var('x') # faster ``` In `autoref`, a few functions (not methods) are available for efficient operations that arise naturally in fixpoint algorithms when transition relations are involved: - `image`, `preimage`: rename some variables, conjoin, existentially quantify, and rename some variables, all at once - `copy_vars`: copy the variables of one BDD manager to another manager ### Reminders about the implementation beneath The Python syntax for Boolean operations (`u & v`) and the method `apply` are faster than the method `add_expr`, because the latter invokes the parser (generated using [`ply.yacc`](https://github.com/dabeaz/ply)). Using `add_expr` is generally quicker and more readable. In practice, prototype new algorithms using `add_expr`, then profile, and if it matters convert the code to use `~`, `&`, `|`, and to call directly `apply`, `exist`, `let`, and other methods. In the future, a compiler may be added, to compile expressions into functions that can be called multiple times, without invoking again the parser. The number of nodes in the manager `bdd` is `len(bdd)`. As noted earlier, each variable corresponds to a level, which is an index in the variable order. This mapping can be obtained with ```python level = bdd.level_of_var('x') var = bdd.var_at_level(level) assert var == 'x', var ``` In `autoref.BDD`, the `dict` that maps each defined variable to its corresponding level can be obtained also from the attribute `BDD.vars` ```python print(bdd.vars) # {'x': 0, 'y': 1, 'z': 2} ``` To copy a node from one BDD manager to another manager ```python a = _bdd.BDD() a.declare('x', 'y', 'z') u = a.add_expr(r'(x /\ y) \/ z') # copy to another BDD manager b = _bdd.BDD() b.declare(*a.vars) v = a.copy(u, b) ``` In each of the modules `dd.autoref` and `dd.cudd`, references to BDD nodes are represented with a class named `Function`. When `Function` objects are not referenced by any Python variable, CPython deallocates them, thus in the next BDD garbage collection, the relevant BDD nodes can be deallocated. For this reason, it is useful to avoid unnecessary references to nodes. This includes the [underscore variable `_`]( https://docs.python.org/3/reference/lexical_analysis.html#reserved-classes-of-identifiers), for example: ```python import dd.autoref bdd = dd.autoref.BDD() bdd.declare('x', 'y') c = [bdd.add_expr(r'x /\ y'), bdd.add_expr(r'x \/ ~ y')] u, _ = c # `_` is assigned the `Function` that references # the root of the BDD that represents x \/ ~ y c = list() # Python deallocates the `list` object created above # so `u` refers to the root of the BDD that represents x /\ y, # as expected, # but `_` still refers to the BDD that represents x \/ ~ y print(bdd.to_expr(_)) ``` The Python reference by `_` in the previous example can be avoided by indexing, i.e., `u = c[0]`. ### Pickle The `dd.autoref` and `dd.bdd` modules can dump the manager to a [pickle file](https://en.wikipedia.org/wiki/Pickle_%28Python%29) and load it back ```python bdd.dump('manager.p') ``` and later, or in another run: ```python bdd = BDD.load('manager.p') ``` ### Nodes as `Function` objects As mentioned earlier, the main difference between the main `dd` modules is what type of object appears at the user interface as a “node”: - `dd.bdd` gives to the user signed integers as nodes - `dd.autoref` and `dd.cudd` give her `Function` objects as nodes. Seldom should this make a difference to the user. However, for integers, the meaning of the Python operators `~`, `&`, `|`, `^` is *unrelated* to the BDD manager. So, if `u = -3` and `v = 25` are nodes in the `dd.bdd.BDD` manager `bdd`, you cannot write `w = u & v` to get the correct result. You have to use either: - `BDD.apply('and', u, v)` or - `BDD.add_expr(rf'{u} /\ {v}')`. Unlike `dd.bdd`, the nodes in `autoref` and `cudd` are of class `Function`. This abstracts away the underlying node representation, so that you can run the same code in both pure Python (with `dd.bdd.BDD` underneath as manager), as well as C (with the `struct` named [`DdManager`]( https://github.com/johnyf/cudd/blob/80c9396b7efcb24c33868aeffb89a557af0dc356/cudd/cudd/cudd.h#L281) in `cudd.h` underneath as manager). The [magic methods](https://github.com/RafeKettler/magicmethods) for `~`, `&`, `|`, `^` implemented by `Function` are its most frequently used aspect. Two methods called `implies` and `equiv` are available. But it is more readable to use `bdd.apply` or `bdd.add_expr`, or just `v | ~ u` for `u.implies(v)` and `~ (u ^ v)` for `u.equiv(v)`. #### BDD equality If `u` and `v` are instances of `Function`, and `u == v`, then `u` represents the same BDD as `v`. This is **NOT** true for nodes in `dd.bdd`, because `u` and `v` may be nodes from *different* manager instances. So, that `u == -3 == v` does not suffice to deduce that `u` and `v` represent the same BDD. In this case, we have to ensure that `u` and `v` originate from the same manager. Thus, using `dd.bdd` offers less protection against subtle errors that will go undetected. #### Other methods Information about a node `u` can be read with a few attributes and magic: - `len(u)` is the number of nodes in the graph that represents `u` in memory - `u.var` is the name (`str`) of the variable in `BDD.support(u)` with minimal level (recall that variables are ordered) - `u.level` is the level of `u.var` - `u.ref` is the reference count of `u`, meaning the number of other nodes `v` with an edge `(v, u)`, plus external references to `u` by the user. We say that the node `u` is “labeled” with variable `u.var`. At this point, recall that these diagrams track *decisions*. At each node, we decide where to go next, depending on the value of `u.var`. If `u.var` is true, then we go to node `u.high`, else to node `u.low`. For this reason, `u.high` is also called the “then” child of `u`, and `u.low` the “else” child. This object-oriented appearance is only an external interface for the user's convenience. Typically, the bulk of the nodes aren't referenced externally. Internally, the nodes are managed efficiently en masse, no `Function`s there. A `Function.to_expr` method is present, but using `BDD.to_expr` looks tidier. ## CUDD interface: `dd.cudd` We said earlier that you can develop with `autoref`, deferring usage of `CUDD` for when really needed. This raises two questions: 1. Why not use `cudd` from the start ? 2. When should you switch from `autoref` to `cudd` ? The answer to the second question is simple: when your problem takes more time and memory than available. For light to moderate use, `cudd` probably won't be needed. Regarding the first question, `dd.cudd` requires to: - *compile* CUDD, and - *cythonize, compile, and link* `dd/cudd.pyx`. The `setup.py` of `dd` can do these for you, as described in the file [`README.md`]( https://github.com/tulip-control/dd/blob/main/README.md#cython-bindings). However, this may require more attention than appropriate for the occassion. An example is teaching BDDs in a class on data structures, with the objective for students to play with BDDs, not with [`gcc`]( https://en.wikipedia.org/wiki/GNU_Compiler_Collection) and [linking]( https://en.wikipedia.org/wiki/Linker_%28computing%29) errors (that's enlightening too, but in the realm of a slightly different class). If you are interested in tuning CUDD to get most out of it (or because some problem demands it due to its size), then use: - `BDD.statistics` to obtain the information described in [CUDD Programmer's manual / Gathering and interpreting statistics]( https://www.cs.rice.edu/~lm30/RSynth/CUDD/cudd/doc/node4.html#SECTION00048000000000000000). - `BDD.configure` to read and set the parameters “max memory”, “loose up to”, “max cache hard”, “min hit”, and “max growth”. Due to how CUDD manages variables, the method `add_var` takes as keyword argument the variable index, *not* the level (which `autoref` does). The level can still be set with the method `insert_var`. The methods `dump` and `load` store the BDD of a selected node in a DDDMP file. Pickling and PDF plotting are not available yet in `dd.cudd`. An interface to the BuDDy C libary also exists, as `dd.buddy`. However, experimentation suggests that BuDDy does not contain as successful heuristics for deciding *when* to invoke reordering. CUDD is initialized with a `memory_estimate` of 1 GiB (1 [gibibyte]( https://en.wikipedia.org/wiki/Gibibyte)). If the machine has less RAM, then `cudd.BDD` will raise an error. In this case, pass a smaller initial memory estimate, for example ```python cudd.BDD(memory_estimate=0.5 * 2**30) ``` Note that `2**30` bits is 1 gi*bi*byte (GiB), not 1 gi*ga*byte (GB). Relevant reading about [gigabyte](https://en.wikipedia.org/wiki/Gigabyte), [IEC prefixes for binary multiples]( https://en.wikipedia.org/wiki/Binary_prefix#IEC_prefixes), and the [ISO/IEC 80000 standard]( https://en.wikipedia.org/wiki/ISO/IEC_80000#Information_science_and_technology). ### Functions The functions `and_exists`, `or_forall` in `dd.cudd` offer the functionality of relational products (meaning neighboring variable substitution, conjunction, and quantification, all at one pass over BDDs). This functionality is implemented with `image`, `preimage` in `dd.autoref`. Note that (pre)image contains substitution, unlike `and_exists`. The function `cudd.reorder` is similar to `autoref.reorder`, but does not default to invoking automated reordering. Typical use of CUDD enables dynamic reordering. ### Checking for reference counting errors When a BDD manager `dd.cudd.BDD` is deallocated, it asserts that no BDD nodes have nonzero reference count in CUDD. By default, this assertion should never fail, because automated reference counting makes it impossible. If the assertion fails, then the exception is ignored and a message is printed instead, and Python continues execution (read also the Cython documentation of [`__dealloc__`]( https://cython.readthedocs.io/en/latest/src/userguide/special_methods.html#finalization-method-dealloc), the Python documentation of ["Finalization and De-allocation"]( https://docs.python.org/3/extending/newtypes.html#finalization-and-de-allocation), and of [`tp_dealloc`]( https://docs.python.org/3/c-api/typeobj.html#c.PyTypeObject.tp_dealloc)). In case the user decides to explicitly modify the reference counts, ignoring exceptions can make it easier for reference counting errors to go unnoticed. To make Python exit when reference counting errors exist before a BDD manager is deallocated, use: ```python import dd.cudd as cudd bdd = cudd.BDD() # ... statements ... # raise `AssertionError` if any nodes have nonzero reference count # just before deallocating the BDD manager assert len(bdd) == 0, len(bdd) ``` Note that the meaning of `len` for the class `dd.autoref.BDD` is slightly different. As a result, the code for checking that no BDD nodes have nonzero reference count in `dd.autoref` is: ```python import dd.autoref as _bdd bdd = _bdd.BDD() # ... statements ... # raise `AssertionError` if any nodes have nonzero reference count # just before deallocating the BDD manager bdd._bdd.__del__() # directly calling `__del__` does raise # any exception raised inside `__del__` ``` Note that if an assertion fails inside `__del__`, then [the exception is ignored and a message is printed to `sys.stderr` instead]( https://docs.python.org/3/reference/datamodel.html#object.__del__), and Python continues execution. This is similar to what happens with exceptions raised inside `__dealloc__` of extension types in Cython. When `__del__` is called directly, exceptions raised inside it are not ignored. ## Lower level: `dd.bdd` We discuss now some more details about the pure Python implementation in `dd.bdd`. Two interfaces are available: - convenience: the module [`dd.autoref`]( https://github.com/tulip-control/dd/blob/main/dd/autoref.py) wraps `dd.bdd` and takes care of reference counting using [`__del__`]( https://docs.python.org/3/reference/datamodel.html#object.__del__). - "low level": the module [`dd.bdd`]( https://github.com/tulip-control/dd/blob/main/dd/bdd.py) requires that the user in/decrement the reference counters associated with nodes that are used outside of a `BDD`. The pure-Python module `dd.bdd` can be used directly, which allows access more extensive than `dd.autoref`. The `n` variables in a `dd.bdd.BDD` are ordered from `0` (top level) to `n - 1` (bottom level). The terminal node `1` is at level `n`. The constant `TRUE` is represented by `+1`, and `FALSE` by `-1`. To avoid running out of memory, a BDD manager deletes nodes when they are not used anymore. This is called [garbage collection]( https://en.wikipedia.org/wiki/Garbage_collection_%28computer_science%29). So, two things need to happen: 1. keep track of node “usage” 2. invoke garbage collection Garbage collection is triggered either explicitly by the user, or when invoking the reordering algorithm. To prevent nodes from being garbage collected, their reference counts should be incremented, which is discussed in the next section. Node usage is tracked with reference counting, for each node. In `autoref`, the reference counts are maintained by the constructor and destructor methods of `Function` (hence the “auto”). These methods are invoked when the `Function` object is not referenced any more by variables, so [Python decides]( https://docs.python.org/3/glossary.html#term-garbage-collection) to delete it. In `dd.bdd`, you have to perform the reference counting by suitably adding to and subtracting from the counter associated to the node you reference. Also, garbage collection is invoked either explicitly or by reordering (explicit or dynamic). So if you don't need to collect garbage, then you can skip the reference counting (not recommended). ### Reference counting The reference counters live inside [`dd.bdd.BDD._ref`]( https://github.com/tulip-control/dd/blob/cbbc96f93da68d3d10f161ef27ccc5e3756c5ae2/dd/bdd.py#L81). To guard against corner cases, like attempting to decrement a zero counter, use - [`BDD.incref(u)`]( https://github.com/tulip-control/dd/blob/cbbc96f93da68d3d10f161ef27ccc5e3756c5ae2/dd/bdd.py#L126): +1 to the counter of node `u` - [`BDD.decref(u)`]( https://github.com/tulip-control/dd/blob/cbbc96f93da68d3d10f161ef27ccc5e3756c5ae2/dd/bdd.py#L130): -1 to the same counter. The method names `incref` and `decref` originate from the [Python reference counting](https://docs.python.org/3/c-api/refcounting.html) implementation. If we want node `u` to persist after garbage collection, then it needs to be actively referenced at least once ```python u = bdd.add_expr(s) bdd.incref(u) ``` Revisiting an earlier example, manual reference counting looks like: ```python import dd.bdd as _bdd bdd = _bdd.BDD() bdd.declare('x', 'y', 'z') s = r'(x /\ y) \/ ~ z' # TLA+ syntax s = '(x & y) | ! z' # Promela syntax u = bdd.add_expr(s) bdd.incref(u) bdd.dump('before_collections.pdf') bdd.collect_garbage() bdd.dump('middle.pdf') bdd.decref(u) bdd.collect_garbage() bdd.dump('after_collections.pdf') ``` A formula may depend on a variable, or not. There are two ways to find out ```python u = bdd.add_expr(r'x /\ y') # TLA+ syntax u = bdd.add_expr('x & y') # Promela syntax c = 'x' in bdd.support(u) # more readable c_ = bdd.is_essential(u, 'x') # slightly more efficient assert c == True, c assert c == c_, (c, c_) c = 'z' in bdd.support(u) assert c == False, c ``` ### Reordering Given a BDD, the size of its graph representation depends on the variable order. Reordering changes the variable order. Reordering *optimization* searches for a variable order better than the current one. *Dynamic* reordering is the automated invocation of reordering optimization. BDD managers use heuristics to decide when to invoke reordering, because it is [NP-hard](https://en.wikipedia.org/wiki/NP-hardness) to find a variable order that minimizes a given BDD. The function `dd.bdd.reorder` implements [Rudell's sifting algorithm]( http://www.eecg.toronto.edu/~ece1767/project/rud.pdf). This reordering heuristic is the most commonly used, also in CUDD. Dynamic variable reordering can be enabled by calling: ```python import dd.bdd as _bdd bdd = _bdd.BDD() bdd.configure(reordering=True) ``` By default, dynamic reordering in `dd.bdd.BDD` is disabled. This default is unlike `dd.cudd` and will change in the future to enabled. You can also invoke reordering explicitly when desired, besides dynamic invocation. For example: ```python import dd.bdd as _bdd bdd = _bdd.BDD() vrs = [f'x{i}' for i in range(3)] vrs.extend(f'y{i}' for i in range(3)) bdd.declare(*vrs) print(bdd.vars) # {'x0': 0, 'x1': 1, 'x2': 2, 'y0': 3, 'y1': 4, 'y2': 5} s = r'(x0 /\ y0) \/ (x1 /\ y1) \/ (x2 /\ y2)' # TLA+ syntax s = '(x0 & y0) | (x1 & y1) | (x2 & y2)' # Promela syntax u = bdd.add_expr(s) bdd.incref(u) number_of_nodes = len(bdd) print(f'{number_of_nodes = }') # number_of_nodes = 22 # collect intermediate results produced while making u bdd.collect_garbage() number_of_nodes = len(bdd) print(f'{number_of_nodes = }') # number_of_nodes = 15 bdd.dump('before_reordering.pdf') # invoke variable order optimization by sifting _bdd.reorder(bdd) number_of_nodes = len(bdd) print(f'{number_of_nodes = }') # number_of_nodes = 7 print(bdd.vars) # {'x0': 0, 'x1': 3, 'x2': 5, 'y0': 1, 'y1': 2, 'y2': 4} bdd.dump('after_reordering.pdf') ``` If you want to obtain a particular variable order, then give the desired variable order as a `dict` to the function `reorder`. ```python my_favorite_order = dict( x0=0, x1=1, x2=2, y0=3, y1=4, y2=5) number_of_nodes = len(bdd) print(f'{number_of_nodes = }') # number_of_nodes = 7 _bdd.reorder(bdd, my_favorite_order) number_of_nodes = len(bdd) print(f'{number_of_nodes = }') # number_of_nodes = 15 ``` (Request such inefficient reordering only if you have some special purpose.) You can turn [`logging`](https://docs.python.org/3/library/logging.html) to `DEBUG`, if you want to watch reordering in action. In some cases you might want to make some pairs of variables adjacent to each other, but don't care about the location of each pair in the variable order (e.g., this enables efficient variable renaming). Use `reorder_to_pairs`. All reordering algorithms rely on the elementary operation of swapping two adjacent levels in the manager. You can do this by calling `BDD.swap`, so you can implement some reordering optimization algorithm different than Rudell's. The difficult part to implement is `swap`, not the optimization heuristic. The garbage collector in [`dd.bdd.BDD.collect_garbage`]( https://github.com/tulip-control/dd/blob/cbbc96f93da68d3d10f161ef27ccc5e3756c5ae2/dd/bdd.py#L614) works by scanning all nodes, marking the unreferenced ones, then collecting those (mark-and-sweep). The function `dd.bdd.to_nx(bdd, roots)` converts the subgraph of `bdd` rooted at `roots` to a [`networkx.MultiDiGraph`]( https://networkx.org/documentation/stable/tutorial.html#multigraphs). ### Other methods The remaining methods of `dd.bdd.BDD` will be of interest more to developers of algorithms that manipulate or read the graph of BDD nodes itself. For example, say you wanted to write a little function that explores the BDD graph rooted at node `u`. ```python def print_descendants_forgetful(bdd, u): i, v, w = bdd._succ[abs(u)] print(u) # u is terminal ? if v is None: return print_descendants_forgetful(bdd, v) print_descendants_forgetful(bdd, w) ``` In the worst case, this can take time exponential in the nodes of `bdd`. To make sure that it takes linear time, we have to remember visited nodes ```python def print_descendants(bdd, u, visited): p = abs(u) i, v, w = bdd._succ[p] # visited ? if p in visited: return # remember visited.add(p) print(u) # u is terminal ? if v is None: return print_descendants(bdd, v, visited) print_descendants(bdd, w, visited) ``` Run it with `visited = set()`. New nodes are created with `BDD.find_or_add(level, low, high)`. Always use this method to make a new node, because it first checks in the *unique table* `BDD._pred` whether the node at `level` with successors `(low, high)` already exists. This uniqueness is at the heart of reduced ordered BDDs, the reason of their efficiency. Throughout `dd.bdd`, nodes are frequently referred to as *edges*. The reason is that all nodes stored are positive integers. Negative integers signify negation, and appear only as either edges to successors (negated edges), or references given to the user (because a negation is popped to above the root node, for reasons of [representation uniqueness](https://dx.doi.org/10.1145/123186.123222)). The method `BDD.levels` returns a generator of tuples `(u, i, v, w)`, over all nodes `u` in `BDD._succ`, where: - `u` is a node in the iteration - `i` is the level that `u` lives at - `v` is the low (“else”) successor - `w` is the high (“then”) successor The iteration starts from the bottom (largest level), just above the terminal node for “true” and “false” (which is `-1` in `dd.bdd`). It scans each level, moving upwards, until it reaches the top level (indexed with 0). The nodes contained in the graph rooted at node `u` are `bdd.descendants([u])`. An efficient implementation of `let` that works only for variables with level <= the level of any variable in the support of node `u` is `_top_cofactor(u, level)`. Finally, `BDD.reduction` is of only academic interest. It takes a binary decision diagram that contains redundancy in its graph representation, and *reduces* it to the non-redundant, canonical form that corresponds to the chosen variable order. This is the function described originally [by Bryant]( https://dx.doi.org/10.1109/TC.1986.1676819). It is never used, because all BDD graphs are *constructed* bottom-up to be reduced. To observe `reduction` in action, you have to manually create a BDD graph that is not reduced. ## Example: Reachability analysis We have been talking about BDDs, but you're probably here because you want to *use* them. A common application is manipulation of Boolean functions in the context of relations that represent dynamics, sometimes called transition relations. Suppose that we have an elevator that moves between three floors. We are interested in the elevator's location, which can be at one of the three floors. So, we can pretend that 0, 1, 2 are the three floors. Using bits, we need at least two bits to represent the triple `{0, 1, 2}`. Two bits can take a few too many values, so we should tell the computer that 3 is not possible in our model of the three floors. Suppose that now the elevator is at floor `x0`, `x1`, and next at floor `x0'`, `x1'` (read “x prime”). The identifiers `["x0", "x1", "x0'", "x1'"]` are just four bits. The elevator can move as follows ```python import dd.autoref as _bdd bdd = _bdd.BDD() bdd.declare("x0", "x0'", "x1", "x1'") # TLA+ syntax s = ( r"((~ x0 /\ ~ x1) => ( (~ x0' /\ ~ x1') \/ (x0' /\ ~ x1') )) /\ " r"((x0 /\ ~ x1) => ~ (x0' /\ x1')) /\ " r"((~ x0 /\ x1) => ( (~ x0' /\ x1') \/ (x0' /\ ~ x1') )) /\ " r" ~ (x0 /\ x1)") transitions = bdd.add_expr(s) ``` We can now find from which floors the elevator can reach floor 2. To compute this, we find the floors that either: - are already inside the set `{2}`, or - can reach `q` after one transition. This looks for existence of floors, hence the existential quantification. We enlarge `q` by the floors we found, and repeat. We continue this backward iteration, until reaching a [least fixpoint]( https://en.wikipedia.org/wiki/Knaster%E2%80%93Tarski_theorem) (meaning that two successive iterates are equal). ```python # target is the set {2} target = bdd.add_expr(r'~ x0 /\ x1') # start from empty set q = bdd.false qold = None prime = {"x0": "x0'", "x1": "x1'"} qvars = {"x0'", "x1'"} # fixpoint reached ? while q != qold: qold = q next_q = bdd.let(prime, q) u = transitions & next_q # existential quantification over x0', x1' pred = bdd.quantify(u, qvars, forall=False) q = q | pred | target ``` At the end, we obtain ```python expr = q.to_expr() print(expr) # '(! ite(x0, x1, False))' ``` which is the set `{0, 1, 2}` (it does not contain 3, because that would evaluate to `! ite(True, True, False)` which equals `! True`, so `False`). More about building symbolic algorithms, together with infrastructure for arithmetic and automata, and examples, can be found in the package [`omega`]( https://github.com/tulip-control/omega/blob/main/doc/doc.md). ## Syntax for quantified Boolean formulas The method `BDD.add_expr` parses the following grammar. The TLA+ module `dd_expression_grammar` extends the TLA+ module `BNFGrammars`, which is defined on page 184 of the book ["Specifying Systems"]( https://lamport.azurewebsites.net/tla/book.html). ```tla ------- MODULE dd_expression_grammar ------- (* Grammar of expressions parsed by the function `dd._parser.Parser.parse`. *) EXTENDS BNFGrammars COMMA == tok(",") maybe(x) == | Nil | x comma1(x) == x & (COMMA & x)^* NUMERAL == OneOf("0123456789") is_dd_lexer_grammar(L) == /\ L.A = tok("\\A") /\ L.E = tok("\\E") /\ L.S = tok("\\S") /\ L.COLON = tok(":") /\ L.COMMA = COMMA /\ L.NOT = tok("~") /\ L.AND = tok("/\\") /\ L.OR = tok("\\/") /\ L.IMPLIES = tok("=>") /\ L.IFF = tok("<=>") /\ L.EQ = tok("=") /\ L.NEQ = tok("#") /\ L.EXCLAMATION = tok("!") /\ L.ET = tok("&") /\ L.PIPE = tok("|") /\ L.RARROW = tok("->") /\ L.LR_ARROW = tok("<->") /\ L.CIRCUMFLEX = tok("^") /\ L.SLASH = tok("/") /\ L.AT = tok("@") /\ L.LPAREN = tok("(") /\ L.RPAREN = tok(")") /\ L.FALSE = Tok({ "FALSE", "false" }) /\ L.TRUE = Tok({ "TRUE", "true" }) /\ L.ITE = tok("ite") /\ L.NAME = LET LETTER == | OneOf("abcdefghijklmnopqrstuvwxyz") | OneOf("ABCDEFGHIJKLMNOPQRSTUVWXYZ") UNDERSCORE == tok("_") start == | LETTER | UNDERSCORE DOT == tok(".") PRIME == tok("'") symbol == | start | NUMERAL | DOT | PRIME tail == symbol^* IN start & tail /\ L.INTEGER = LET DASH == tok("-") _dash == maybe(DASH) numerals == NUMERAL^+ IN _dash & numerals is_dd_parser_grammar(L, G) == /\ \A symbol \in DOMAIN L: G[symbol] = L[symbol] /\ G.expr = (* predicate logic *) | L.A & G.names & L.COLON & G.expr (* universal quantification ("forall") *) | L.E & G.names & L.COLON & G.expr (* existential quantification ("exists") *) | L.S & G.pairs & G.COLON & G.expr (* renaming of variables ("substitution") *) (* propositional *) (* TLA+ syntax *) | G.NOT & G.expr (* negation ("not") *) | G.expr & G.AND & G.expr (* conjunction ("and") *) | G.expr & G.OR & G.expr (* disjunction ("or") *) | G.expr & G.IMPLIES & G.expr (* implication ("implies") *) | G.expr & G.IFF & G.expr (* equivalence ("if and only if") *) | G.expr & G.NEQ & G.expr (* difference (negation of `<=>`) *) (* Promela syntax *) | L.EXCLAMATION & G.expr (* negation *) | G.expr & L.ET & G.expr (* conjunction *) | G.expr & L.PIPE & G.expr (* disjunction *) | G.expr & L.RARROW & G.expr (* implication *) | G.expr & L.LR_ARROW & G.expr (* equivalence *) (* other *) | G.expr & L.CIRCUMFLEX & G.expr (* xor (exclusive disjunction) *) | L.ITE & L.LPAREN & G.expr & L.COMMA & G.expr & L.COMMA & G.expr & L.RPAREN (* ternary conditional (if-then-else) *) | G.expr & L.EQ & G.expr | L.LPAREN & G.expr & L.RPAREN (* parentheses *) | L.NAME (* identifier (bit variable) *) | L.AT & L.INTEGER (* BDD node reference *) | L.FALSE | L.TRUE (* Boolean constants *) /\ G.names = comma1(L.NAME) /\ G.pairs = comma1(G.pair) /\ G.pair = L.NAME & L.SLASH & L.NAME dd_grammar == LET L == LeastGrammar(is_dd_lexer_grammar) is_parser_grammar(G) == is_dd_parser_grammar(L, G) IN LeastGrammar(is_parser_grammar) ============================================ ``` Comments are written using TLA+ syntax: - `(* this is a doubly-delimited comment *)` - `\* this is a trailing comment` Doubly-delimited comments can span multiple lines. The token precedence (lowest to highest) and associativity is: - `:` (left) - `<=>, <->` (left) - `=>, ->` (left) - `-` (left) - `#`, `^` (left) - `\/, |` (left) - `/\, &` (left) - `=` (left) - `~, !` - `-` unary minus, as in `-5` The meaning of a number of operators, assuming `a` and `b` take Boolean values: - `a => b` means `b \/ ~ a` - `a <=> b` means `(a /\ b) \/ (~ a /\ ~ b)` - `a # b` means `(a /\ ~ b) \/ (b /\ ~ a)` - `a - b` means `a /\ ~ b` (for BDDs only) - `ite(a, b, c)` means `(a /\ b) \/ (~ a /\ c)` Both and `setup.py`, and a developer may want to force a rebuild of the parser table. For this purpose, each module that contains a parser, also has a function `_rewrite_tables` that deletes and rewrites the tables. If the module is run as a script, then the `__main__` stanza calls this function to delete and the write the parser tables to the current directory. The parsers use [`astutils`](https://pypi.org/project/astutils/). ## Multi-valued decision diagrams (MDD) A representation for functions from integer variables to Boolean values. The primary motivation for implementing MDDs was to produce more readable string and graphical representations of BDDs. MDDs are implemented in pure Python. The interface is “low”, similar to `dd.bdd`, with reference counting managed by the user. A core of necessary methods have been implemented, named as the `BDD` methods with the same functionality in other `dd` modules. Complemented edges are used here too. BDDs are predicates over binary variables (two-valued). MDDs are predicates over integer variables (multi-valued). As expected, the data structure and algorithms for representing an MDD are a slight generalization of those for BDDs. For example, compare the body of `dd.mdd.MDD.find_or_add` with `dd.bdd.BDD.find_or_add`. The variables are defined by a `dict` argument to the constructor (in the future, dynamic variable addition may be implemented, by adding a method `MDD.add_var`) ```python import dd.mdd as _mdd dvars = dict( x=dict(level=0, len=4), y=dict(level=1, len=2)) mdd = _mdd.MDD(dvars) ``` So, variable `x` is an integer that can take values in `range(4)`. Currently, the main use of an MDD is for more comprehensive representation of a predicate stored in a BDD. This is achieved with the function `dd.mdd.bdd_to_mdd` that takes a `dd.bdd.BDD` and a mapping from MDD integers to BDD bits. Referencing of BDD nodes is necessary, because `bdd_to_mdd` invokes garbage collection on the BDD. ```python import dd.bdd as _bdd import dd.mdd as _mdd bits = dict(x=0, y0=1, y1=2) bdd = _bdd.BDD(bits) u = bdd.add_expr(r'x \/ (~ y0 /\ y1)') bdd.incref(u) # convert BDD to MDD ints = dict( x=dict(level=1, len=2, bitnames=['x']), y=dict(level=0, len=4, bitnames=['y0', 'y1'])) mdd, umap = _mdd.bdd_to_mdd(bdd, ints) # map node `u` from BDD to MDD v = umap[abs(u)] # complemented ? if u < 0: v = - v print(v) # -3 expr = mdd.to_expr(v) print(expr) # (! if (y in set([0, 1, 3])): (if (x = 0): 1, # elif (x = 1): 0), # elif (y = 2): 0) # plot MDD with graphviz mdd.dump('mdd.pdf') ``` Note that the `MDD` node `v` is complemented (-3 < 0), so the predicate in the negated value computed for node `y-3` in the next image. ![example_bdd](https://rawgithub.com/johnyf/binaries/main/dd/mdd.png) ## Installation of C extension modules ### Environment variables that activate C extensions By default, the package `dd` installs only its Python modules. You can select to install Cython extensions using environment variables: - `DD_FETCH=1`: download CUDD v3.0.0 sources from the internet, check the tarball's hash, unpack the tarball, and `make` CUDD. - `DD_CUDD=1`: build module `dd.cudd`, for CUDD BDDs - `DD_CUDD_ZDD=1`: build module `dd.cudd_zdd`, for CUDD ZDDs - `DD_SYLVAN=1`: build module `dd.sylvan`, for Sylvan BDDs - `DD_BUDDY=1`: build module `dd.buddy`, for BuDDy BDDs Example scripts are available that fetch and install the Cython bindings: - [`examples/install_dd_cudd.sh`]( https://github.com/tulip-control/dd/blob/main/examples/install_dd_cudd.sh) - [`examples/install_dd_sylvan.sh`]( https://github.com/tulip-control/dd/blob/main/examples/install_dd_sylvan.sh) - [`examples/install_dd_buddy.sh`]( https://github.com/tulip-control/dd/blob/main/examples/install_dd_buddy.sh) ### Alternative: Directly running `setup.py` Activating the Cython build by directly running `python setup.py` is an alternative to using environment variables (e.g., `export DD_CUDD=1` etc). The relevant command-line options of `setup.py` are: - `--fetch`: same effect as `DD_FETCH=1` - `--cudd`: same effect as `DD_CUDD=1` - `--cudd_zdd`: same effect as `DD_CUDD_ZDD=1` - `--sylvan`: same effect as `DD_SYLVAN=1` - `--buddy`: same effect as `DD_BUDDY=1` These options work for `python setup.py sdist` and `python setup.py install`, but directly running `python setup.py` is deprecated by `setuptools >= 58.3.0`. Example: ```shell pip download dd --no-deps tar xzf dd-*.tar.gz pushd dd-*/ # `pushd` means `cd` python setup.py install --fetch --cudd --cudd_zdd popd ``` [`pushd directory`]( https://en.wikipedia.org/wiki/Pushd_and_popd) is akin to `stack.append(directory)` in Python, and `popd` to `stack.pop()`. The path to an existing CUDD build directory can be passed as an argument, for example: ```shell python setup.py install \ --fetch \ --cudd="/home/user/cudd" ``` ### Using the package `build` The following also works for building source tarballs and wheels: ```sh pip install cython export DD_FETCH=1 DD_CUDD=1 python -m build --no-isolation ``` To build a source tarball: ```sh DD_CUDD=1 python -m build --sdist --no-isolation ``` ### Customizing the C compilation If you build and install CUDD, Sylvan, or BuDDy yourself, then ensure that: - the header files and libraries are present, and - the compiler is configured appropriately (include, linking, and library configuration), either by setting [environment variables]( https://en.wikipedia.org/wiki/Environment_variable) prior to calling `pip`, or by editing the file [`download.py`]( https://github.com/tulip-control/dd/blob/main/download.py). Currently, `download.py` expects to find Sylvan under `dd/sylvan` and built with [Autotools]( https://en.wikipedia.org/wiki/GNU_Build_System) (for an example, read `.github/workflows/setup_build_env.sh`). If the path differs in your environment, remember to update it. If you prefer defining installation directories, then follow [Cython's instructions]( https://cython.readthedocs.io/en/latest/src/tutorial/clibraries.html#compiling-and-linking) to define `CFLAGS` and `LDFLAGS` before installing. You need to have copied `CuddInt.h` to the installation's include location (CUDD omits it). For example, to use CUDD as installed by MacPorts (port [`libcudd`]( https://ports.macports.org/port/libcudd/)), use ```sh CFLAGS="-I/opt/local/include" LDFLAGS="-L/opt/local/lib" ``` ## Installing the development version For installing the development version of `dd` from the `git` repository, an alternative to cloning the repository and installing from the cloned repository is to [use `pip` for doing so]( https://pip.pypa.io/en/stable/cli/pip_install/#argument-handling): ```shell pip install https://github.com/tulip-control/dd/archive/main.tar.gz ``` or with [`pip` using `git`]( https://pip.pypa.io/en/stable/topics/vcs-support/#git) (this alternative requires that `git` be installed): ```shell pip install git+https://github.com/tulip-control/dd ``` A `git` URL can be passed also to [`pip download`]( https://pip.pypa.io/en/stable/cli/pip_download/#overview), for example: ```shell pip download --no-deps https://github.com/tulip-control/dd/archive/main.tar.gz ``` The extension `.zip` too can be used for the name of the [archive file]( https://en.wikipedia.org/wiki/Archive_file) in the URL. Analogously, with `pip` using `git`: ```shell pip download --no-deps git+https://github.com/tulip-control/dd ``` Note that the naming of paths *within* the archive file downloaded from GitHub in this way will differ, depending on whether `https://` or `git+https://` is used. ## Footnotes - The `Makefile` contains the rules `sdist` and `wheel` that create distributions for uploading to PyPI with `twine`. - Press `Ctrl + \` on Linux and Darwin to quit the Python process when CUDD computations take a long time. Read `stty -a` for your settings. - If you are interested in exploring other decision diagram packages, you can find [a list at `github.com/johnyf/tool_lists/`]( https://github.com/johnyf/tool_lists/blob/main/bdd.md). ## Copying This document is copyright 2015-2022 by California Institute of Technology. All rights reserved. Licensed under 3-clause BSD. ================================================ FILE: download.py ================================================ """Retrieve and build dependencies of C extensions.""" import argparse as _arg import collections.abc as _abc import ctypes import functools as _ft import hashlib import os import platform import shutil import subprocess import sys import tarfile import textwrap as _tw import typing as _ty import urllib.error import urllib.request try: import Cython.Build as _build pyx = '.pyx' except ImportError: print('`import cython` failed') pyx = '.c' import setuptools.extension as _extension EXTENSIONS: _ty.Final = [ 'cudd', 'cudd_zdd', 'buddy', 'sylvan'] # CUDD CUDD_VERSION: _ty.Final = '3.0.0' CUDD_TARBALL: _ty.Final = f'cudd-{CUDD_VERSION}.tar.gz' CUDD_URL: _ty.Final = ( 'https://sourceforge.net/projects/cudd-mirror/files/' f'cudd-{CUDD_VERSION}.tar.gz/download') CUDD_SHA256: _ty.Final = ( 'b8e966b4562c96a03e7fbea239729587' 'd7b395d53cadcc39a7203b49cf7eeb69') CC = 'gcc' FILE_PATH = os.path.dirname(os.path.realpath(__file__)) CUDD_PATH = os.path.join( FILE_PATH, f'cudd-{CUDD_VERSION}') CUDD_DIRS: _ty.Final = [ 'cudd', 'dddmp', 'epd', 'mtr', 'st', 'util'] CUDD_INCLUDE = ['.', *CUDD_DIRS] CUDD_LINK: _ty.Final = ['cudd/.libs', 'dddmp/.libs'] CUDD_LIB: _ty.Final = ['cudd', 'dddmp'] CUDD_CFLAGS = [ # '-arch x86_64', '-fPIC', '-std=c99', '-DBSD', '-DHAVE_IEEE_754', '-pthread', '-fwrapv', '-fno-strict-aliasing', '-Wall', '-W', '-O3'] sizeof_long = ctypes.sizeof(ctypes.c_long) sizeof_void_p = ctypes.sizeof(ctypes.c_void_p) CUDD_CFLAGS.extend([ f'-DSIZEOF_LONG={sizeof_long}', f'-DSIZEOF_VOID_P={sizeof_void_p}']) # add -fPIC XCFLAGS = ( 'XCFLAGS=-fPIC -mtune=native -DHAVE_IEEE_754 -DBSD ' f'-DSIZEOF_VOID_P={sizeof_void_p} ' f'-DSIZEOF_LONG={sizeof_long}') # Sylvan SYLVAN_PATH = os.path.join( FILE_PATH, 'sylvan') SYLVAN_INCLUDE = [ [SYLVAN_PATH, 'src'], [FILE_PATH, 'dd']] SYLVAN_LINK = [[SYLVAN_PATH, 'src/.libs']] def extensions( args: _arg.Namespace ) -> list[ _extension.Extension]: """Return C extensions, cythonize as needed. @param args: known args from `argparse.parse_known_args` """ directives = dict( language_level=3, embedsignature=True) cudd_cflags = list(CUDD_CFLAGS) sylvan_cflags = list() compile_time_env = dict() if platform.system() != 'Darwin': cudd_cflags.append('-mtune=native') # tell gcc to compile line tracing if args.linetrace: print('compile Cython extensions with line tracing') directives['linetrace'] = True cudd_cflags.append('-DCYTHON_TRACE=1') sylvan_cflags.append('-DCYTHON_TRACE=1') # directives['binding'] = True os.environ['CC'] = CC path = args.cudd if args.cudd else CUDD_PATH cudd_include = [(path, s) for s in CUDD_INCLUDE] cudd_link = [(path, s) for s in CUDD_LINK] try: _copy_cudd_license(args) _copy_extern_licenses(args) except FileNotFoundError: print('license files of build dependencies not found') c_extensions = dict( cudd=_extension.Extension( 'dd.cudd', sources=[f'dd/cudd{pyx}'], include_dirs=_join(cudd_include), library_dirs=_join(cudd_link), libraries=CUDD_LIB, extra_compile_args=cudd_cflags), cudd_zdd=_extension.Extension( 'dd.cudd_zdd', sources=[f'dd/cudd_zdd{pyx}'], include_dirs=_join(cudd_include), library_dirs=_join(cudd_link), libraries=CUDD_LIB, extra_compile_args=cudd_cflags), buddy=_extension.Extension( 'dd.buddy', sources=[f'dd/buddy{pyx}'], libraries=['bdd']), sylvan=_extension.Extension( 'dd.sylvan', sources=[f'dd/sylvan{pyx}'], include_dirs=_join(SYLVAN_INCLUDE), library_dirs=_join(SYLVAN_LINK), libraries=['sylvan'], extra_compile_args=sylvan_cflags)) for ext in EXTENSIONS: if getattr(args, ext) is None: c_extensions.pop(ext) if pyx == '.pyx': ext_modules = list() for k, v in c_extensions.items(): c = _build.cythonize( [v], compiler_directives=directives, compile_time_env=compile_time_env) ext_modules.append(c[0]) else: ext_modules = list(c_extensions.values()) return ext_modules def _copy_cudd_license( args: _arg.Namespace ) -> None: """Include CUDD's license in wheels.""" path = args.cudd if args.cudd else CUDD_PATH license = os.path.join(path, 'LICENSE') included = os.path.join('dd', 'CUDD_LICENSE') yes = ( args.bdist_wheel and getattr(args, 'cudd') is not None) if yes: shutil.copyfile(license, included) elif os.path.isfile(included): os.remove(included) def _copy_extern_licenses( args: _arg.Namespace ) -> None: """Include in wheels licenses related to building CUDD. To fetch the license files, invoke `make download_licenses`. """ licenses = [ 'GLIBC_COPYING.LIB', 'GLIBC_LICENSES', 'PYTHON_LICENSE'] path = os.path.join(FILE_PATH, 'extern') yes = ( args.bdist_wheel and getattr(args, 'cudd') is not None) for name in licenses: license = os.path.join(path, name) included = os.path.join('dd', name) if yes and os.path.isfile(license): shutil.copyfile(license, included) elif yes and not os.path.isfile(license): print( f'WARNING: No file: `{license}`, ' 'skipping file copy.') elif os.path.isfile(included): os.remove(included) def _join( paths: _abc.Iterable[ _abc.Iterable[str]] ) -> list[str]: """Return paths, after joining each. Flattens a list-of-lists to a list. """ return [os.path.join(*x) for x in paths] def fetch( url: str, sha256: str, filename: str ) -> None: """Download file from `url`, and check its hashes. @param sha256: SHA-256 hash value of file that will be downloaded """ if os.path.isfile(filename): print( f'File `{filename}` already present, ' 'checking hash.') _check_file_hash(filename, sha256) return print(f'Attempting to download file from URL: {url}') try: response = urllib.request.urlopen(url) if response is None: raise urllib.error.URLError( '`urllib.request.urlopen` returned `None` ' 'when attempting to open the URL: ' f'{url}') except urllib.error.URLError as url_error: raise RuntimeError(_tw.dedent(f''' An exception was raised when attempting to open the URL: {url} In case the error message from `urllib` is about SSL certificates, please confirm that your installation of Python has the required SSL certificates. How to ensure this can differ, depending on how Python is installed (building from source or using an installer). CPython's `--with-openssl` (of `configure`) is relevant when building CPython from source. When using an installer of CPython, a separate post-installation step may be needed, as described in CPython's documentation. Relevant information: For downloading CUDD, an alternative is to download by other means the file at the URL: {url} unpack it, and then run: ```python import download download.make_cudd() ``` Once CUDD compilation has completed, run: ``` export DD_CUDD=1 DD_CUDD_ZDD=1; pip install . ``` i.e., without the option `DD_FETCH`. ''')) from url_error with response, open(filename, 'wb') as f: f.write(response.read()) print( 'Completed downloading from URL ' '(may have resulted from redirection): ' f'{response.url}\n' 'Wrote the downloaded data to file: ' f'`{filename}`\n' 'Will now check the hash value (SHA-256) of ' f'the file: `{filename}`') _check_file_hash(filename, sha256) def _check_file_hash( filename: str, sha256: str ) -> None: """Assert `filename` has given hash.""" with open(filename, 'rb') as f: data = f.read() _assert_sha(data, sha256, 256, filename) print( 'Checked hash value (SHA-256) of ' f'file `{filename}`, and is as expected.') def _assert_sha( data: bytes, expected_sha_value: str, algo: _ty.Literal[ 256, 512], filename: str | None=None ) -> None: """Assert `data` hash is `expected_sha_value`. If the hash of `data`, as computed using the algorithm specified in `algo`, is not `expected_sha_value`, then raise an `AssertionError`. The hash value is computed using the functions: - `hashlib.sha256()` if `algo == 256` - `hashlib.sha512()` if `algo == 512` @param data: bytes, to compute the hash of them (as accepted by `hashlib.sha512()`) @param expected_sha_value: hash value (SHA-256 or SHA-512), must correspond to `algo` @param algo: hashing algorithm @param filename: name of file whose hash is being checked, optional argument, if present then it will be used in message of the `AssertionError` """ match algo: case 256: h = hashlib.sha256(data) case 512: h = hashlib.sha512(data) case _: raise ValueError( f'unknown algorithm: {algo = }') x = h.hexdigest() if x == expected_sha_value: return if filename is None: fs = '' else: fs = f'`{filename}` ' raise AssertionError( f'The computed SHA-{algo} hash value ' f'of the downloaded file {fs}does not match ' 'the expected hash value.' f'\nComputed SHA-{algo}: {x}' f'\nExpected SHA-{algo}: {expected_sha_value}') def untar( filename: str ) -> None: """Extract contents of tar file `filename`.""" print(f'++ unpack: {filename}') with tarfile.open(filename) as tar: tar.extractall() print('-- done unpacking.') def make_cudd( ) -> None: """Compile CUDD.""" path = CUDD_PATH cmd = ["./configure", "CFLAGS=-fPIC -std=c99"] subprocess.call(cmd, cwd=path) subprocess.call(['make', '-j4'], cwd=path) def fetch_cudd( ) -> None: """Retrieve, unpack, patch, and compile CUDD.""" filename = CUDD_TARBALL fetch(CUDD_URL, CUDD_SHA256, filename) untar(filename) make_cudd() def download_licenses( ) -> None: """Fetch licenses of dependencies. These licenses are included in the wheel of `dd`. The license files are placed in the directory `extern/`. """ license_dir = 'extern' if not os.path.isdir(license_dir): os.mkdir(license_dir) join = _ft.partial(os.path.join, license_dir) # download GLIBC licenses glibc_license_file = join('GLIBC_COPYING.LIB') glibc_license_url = ''' https://sourceware.org/git/ ?p=glibc.git;a=blob_plain;f=COPYING.LIB;hb=HEAD ''' _fetch_file(glibc_license_url, glibc_license_file) glibc_licenses_file = join('GLIBC_LICENSES') glibc_licenses_url = ''' https://sourceware.org/git/ ?p=glibc.git;a=blob_plain;f=LICENSES;hb=HEAD ''' _fetch_file(glibc_licenses_url, glibc_licenses_file) # download CPython license py_license_file = join('PYTHON_LICENSE') numbers = sys.version_info[:2] python_version = '.'.join(map(str, numbers)) py_license_url = f''' https://raw.githubusercontent.com/ python/cpython/{python_version}/LICENSE ''' _fetch_file(py_license_url, py_license_file) print('Downloaded license files.') def _fetch_file( url: str, filename: str ) -> None: """Dump `url` to `filename`. Removes blankspace from `url`. """ url = ''.join(url.split()) response = urllib.request.urlopen(url) if response is None: raise urllib.error.URLError( 'Error when attempting to open ' f'the URL: {url}') with response, open(filename, 'wb') as fd: fd.write(response.read()) if __name__ == '__main__': fetch_cudd() ================================================ FILE: examples/README.md ================================================ The examples are: - `variable_substitution.py`: rename variables that a BDD depends on - `boolean_satisfiability.py`: solving the [propositional satisfiability problem]( https://en.wikipedia.org/wiki/Boolean_satisfiability_problem) - `reachability.py`: compute the states reachable from some starting set of states. - `queens.py`: solve the [N-queens problem]( https://en.wikipedia.org/wiki/Eight_queens_puzzle). - `bdd_traversal.py`: breadth-first and depth-first iteration over nodes - `reordering.py`: activate dynamic variable reordering for the Python implementation, invoke reordering explicitly, and permute variables to a desired order. - `cudd_configure_reordering.py`: how to turn reordering off when using CUDD - `cudd_statistics.py`: read CUDD's activity in numbers. - `cudd_memory_limits.py`: bound how much memory CUDD is allowed to use. - `cudd_zdd.py`: how to use ZDDs with CUDD. - `json_example.py`: how to write BDDs to JSON files, and how to load BDDs from JSON files. The shell scripts show how to install the Cython modules of `dd`: - `install_dd_cudd.sh`: how to install the modules: - `dd.cudd` and - `dd.cudd_zdd` - `install_dd_sylvan.sh`: how to install the module `dd.sylvan` - `install_dd_buddy.sh`: how to install the module `dd.buddy` To install all the above modules, combine the steps contained in the above shell scripts, and define all the relevant environment variables, i.e., ```shell export \ DD_BUDDY=1 \ DD_FETCH=1 \ DD_CUDD=1 \ DD_CUDD_ZDD=1 \ DD_SYLVAN=1 pip install dd ``` ================================================ FILE: examples/_test_examples.py ================================================ """Run each example module.""" import os import subprocess as _sbp import sys def _main( ) -> None: """Run each example module under `.`.""" _, _, files = next(os.walk('.')) this_file = os.path.basename(__file__) files.remove(this_file) for filename in files: _, ext = os.path.splitext(filename) if ext != '.py': continue cmd = [sys.executable, filename] print(cmd) retcode = _sbp.call(cmd) if retcode == 0: continue raise RuntimeError(retcode) if __name__ == '__main__': _main() ================================================ FILE: examples/bdd_traversal.py ================================================ """Traversing binary decision diagrams. Read [1, Section 2.2]. Reference ========= [1] Steven M. LaValle Planning Algorithms Cambridge University Press, 2006 """ import collections as _cl import textwrap as _tw import dd.autoref as _bdd def traverse_breadth_first(u): """Return nodes encountered.""" queue = _cl.deque([u]) visited = set() while queue: g = queue.popleft() visited.add(int(g)) # is `g` a leaf ? if g.var is None: continue queue.extend([g.low, g.high]) return visited def traverse_depth_first(u): """Return nodes encountered.""" stack = [u] visited = set() while stack: g = stack.pop() visited.add(int(g)) # is `g` a leaf ? if g.var is None: continue stack.extend([g.low, g.high]) return visited def run_traversals(): bdd = _bdd.BDD() bdd.declare('x', 'y', 'z') u = bdd.add_expr( r' (x \/ ~ y) /\ z ') print('breadth-first traversal') visited_b = traverse_breadth_first(u) print(visited_b) print('depth-first traversal') visited_d = traverse_depth_first(u) print(visited_d) if visited_b == visited_d: return raise AssertionError(_tw.dedent(f''' Expected same set of nodes from traversals, but: {visited_b = } and: {visited_d = } ''')) if __name__ == '__main__': run_traversals() ================================================ FILE: examples/boolean_satisfiability.py ================================================ """Is a given Boolean formula satisfiable?""" import dd def example(): """Demonstrate usage.""" # a formula names = ['x', 'y'] formula = r'x /\ ~ y' sat = is_satisfiable(formula, names) _print_result(formula, sat) # another formula names = ['x'] formula = r'x /\ ~ x' sat = is_satisfiable(formula, names) _print_result(formula, sat) def is_satisfiable(formula, names): """Return `True` if `formula` is satisfiable. A formula is satisfiable by Boolean values, if there exist Boolean values such that, when those values are substituted for the variables that appear in the formula, the result is equivalent to `TRUE`. """ bdd = dd.BDD() bdd.declare(*names) u = bdd.add_expr(formula) return u != bdd.false def _print_result(formula, sat): """Inform at stdout.""" if sat: neg = '' else: neg = 'not ' print( f'The formula `{formula}` ' f'is {neg}satisfiable.') if __name__ == '__main__': example() ================================================ FILE: examples/cudd_configure_reordering.py ================================================ """How to configure reordering in CUDD via `dd.cudd`.""" import pprint import dd.cudd as _bdd bdd = _bdd.BDD() vrs = ['x', 'y', 'z'] bdd.declare(*vrs) # get the variable order levels = {var: bdd.level_of_var(var) for var in vrs} print(levels) # change the levels desired_levels = dict(x=2, y=0, z=1) _bdd.reorder(bdd, desired_levels) # confirm that variables are now where desired new_levels = {var: bdd.level_of_var(var) for var in vrs} print(new_levels) # dynamic reordering is initially turned on config = bdd.configure() pprint.pprint(config) # turn off dynamic reordering bdd.configure(reordering=False) # confirm dynamic reordering is now off config = bdd.configure() pprint.pprint(config) ================================================ FILE: examples/cudd_memory_limits.py ================================================ """How to place an upper bound on the memory CUDD consumes.""" import dd.cudd as _bdd def configure(): GiB = 2**30 b = _bdd.BDD() b.configure( # number of bytes max_memory=2 * GiB, # number of entries, not memory units! max_cache_hard=2**25) if __name__ == '__main__': configure() ================================================ FILE: examples/cudd_statistics.py ================================================ """How to print readable statistics.""" import pprint import dd.cudd def print_statistics(): b = dd.cudd.BDD() b.declare('x', 'y', 'z') u = b.add_expr(r'x /\ y /\ z') u = b.add_expr(r'x \/ y \/ ~ z') stats = b.statistics() pprint.pprint(format_dict(stats)) def format_dict(d): """Return `dict` with values readable by humans.""" return {k: format_number(v) for k, v in d.items()} def format_number(x): """Return readable string for `x`.""" if 0 < x < 1: return f'{x:1.2}' return f'{x:_}' if __name__ == '__main__': print_statistics() ================================================ FILE: examples/cudd_zdd.py ================================================ """How to use ZDDs with CUDD.""" import dd.cudd_zdd as _zdd def zdd_example(): zdd = _zdd.ZDD() zdd.declare('x', 'y', 'z') u = zdd.add_expr(r'(x /\ y) \/ z') let = dict(y=zdd.add_expr('~ x')) v = zdd.let(let, u) v_ = zdd.add_expr('z') assert v == v_, (v, v_) if __name__ == '__main__': zdd_example() ================================================ FILE: examples/good_vs_bad_variable_order.py ================================================ """How the variable order in a BDD affects the number of nodes. Reference ========= [1] Randal Bryant "On the complexity of VLSI implementations and graph representations of Boolean functions with application to integer multiplication" TOC, 1991 """ import dd.autoref as _bdd def comparing_two_variable_orders(): n = 6 # declare variables vrs = [f'x{i}' for i in range(n)] primed_vars = [prime(var) for var in vrs] bdd = _bdd.BDD() bdd.declare(*(vrs + primed_vars)) # equality constraints cause difficulties with BDD size expr = r' /\ '.join( f" {var} <=> {var}' " for var in vrs) u = bdd.add_expr(expr) bdd.collect_garbage() # an order that yields a small BDD for `expr` good_order = list() for var in vrs: good_order.extend([var, prime(var)]) # an order that yields a large BDD for `expr` bad_order = list(vrs) bad_order.extend(prime(var) for var in vrs) # plot _bdd.reorder(bdd, list_to_dict(good_order)) bdd.dump('good.pdf') _bdd.reorder(bdd, list_to_dict(bad_order)) bdd.dump('bad.pdf') def list_to_dict(c): return {var: level for level, var in enumerate(c)} def prime(s): return s + "'" if __name__ == '__main__': comparing_two_variable_orders() ================================================ FILE: examples/install_dd_buddy.sh ================================================ #!/usr/bin/env bash # # Install `dd`, including the module # `dd.buddy`, which is written in Cython. # # To run this script, enter in # a command-line environment: # # ./install_dd_buddy.sh set -v set -e # Fetch and build BuDDy BUDDY_INSTALL_PREFIX=`pwd` BUDDY_ARCHIVE=buddy-2.4.tar.gz BUDDY_URL=https://sourceforge.net/projects/buddy/\ files/buddy/BuDDy%202.4/buddy-2.4.tar.gz/download curl -L $BUDDY_URL -o $BUDDY_ARCHIVE tar -xzf $BUDDY_ARCHIVE pushd buddy-*/ ./configure \ --prefix=$BUDDY_INSTALL_PREFIX # as described in # the README file of BuDDy make make install # by default installs to: # `/usr/local/include/` and # `/usr/local/lib/` # # The installation location can # be changed with # `./configure --prefix=/where/to/install` export CFLAGS="-I$BUDDY_INSTALL_PREFIX/include" export LDFLAGS="-L$BUDDY_INSTALL_PREFIX/lib" export LD_LIBRARY_PATH=\ $BUDDY_INSTALL_PREFIX/lib:$LD_LIBRARY_PATH echo $CFLAGS echo $LDFLAGS echo $LD_LIBRARY_PATH popd # Fetch and install `dd` pip install cython export DD_BUDDY=1 pip install dd \ -vvv \ --use-pep517 \ --no-build-isolation # passes `-lbdd` to the C compiler # # fetch `dd` source pip download \ --no-deps dd \ --no-binary dd tar -xzf dd-*.tar.gz # confirm that `dd.buddy` did get installed pushd dd-*/tests/ python -c 'import dd.buddy' popd ================================================ FILE: examples/install_dd_cudd.sh ================================================ #!/usr/bin/env bash # # Install `dd`, including the modules # `dd.cudd` and `dd.cudd_zdd` # (which are written in Cython). # # To run this script, enter in # a command-line environment: # # ./install_dd_cudd.sh # # This script is unnecessary if you # want a pure-Python installation of `dd`. # If so, then `pip install dd`. # # This is script is unnecessary also # if a wheel file for your operating system # and CPython version is available on PyPI. # Wheel files (`*.whl`) can be found at: # # # If there *is* a wheel file on PyPI # that matches your operating system and # CPython version, then `pip install dd` # suffices. set -v set -e pip install dd # to first install # dependencies of `dd` pip uninstall -y dd pip download \ --no-deps dd \ --no-binary dd tar -xzf dd-*.tar.gz pushd dd-*/ export DD_FETCH=1 DD_CUDD=1 DD_CUDD_ZDD=1 pip install . \ -vvv \ --use-pep517 \ --no-build-isolation # confirm that `dd.cudd` did get installed pushd tests/ python -c 'import dd.cudd' popd popd ================================================ FILE: examples/install_dd_sylvan.sh ================================================ #!/usr/bin/env bash # # Install `dd`, including # the module `dd.sylvan`. # (which is written in Cython). # # To run this script, enter in # a command-line environment: # # ./install_dd_sylvan.sh set -v set -e # check for Sylvan build dependencies if ! command -v autoreconf &> /dev/null then echo "apt install autoconf" exit fi if ! command -v libtoolize &> /dev/null then echo "apt install libtool" exit fi # Fetch and install Sylvan SYLVAN_ARCHIVE=sylvan.tar.gz SYLVAN_URL=https://github.com/\ utwente-fmt/sylvan/tarball/v1.0.0 curl -L $SYLVAN_URL -o $SYLVAN_ARCHIVE # checksum echo "9877fe07a8cfe9889152e29624a4c5b283\ cb34672ec524ccb3edb313b3057fbf8ef45622a4\ 9796fae17aa24e0baea5ccfa18f1bc5923e3c552\ 45ab3e3c1927c8 sylvan.tar.gz" | shasum -a 512 -c - # unpack mkdir sylvan tar xzf sylvan.tar.gz -C sylvan --strip=1 pushd sylvan autoreconf -fi ./configure make # update the environment variable `LD_LIBRARY_PATH` export CFLAGS="-I`pwd`/src" export LDFLAGS="-L`pwd`/src/.libs" export LD_LIBRARY_PATH=`pwd`/src/.libs:$LD_LIBRARY_PATH echo $CFLAGS echo $LDFLAGS echo $LD_LIBRARY_PATH popd # Fetch and install `dd` export DD_SYLVAN=1 pip install dd \ -vvv \ --use-pep517 \ --no-build-isolation # fetch `dd` source pip download \ --no-deps dd \ --no-binary dd tar -xzf dd-*.tar.gz # confirm that `dd.sylvan` did get installed pushd dd-*/tests/ python -c 'import dd.sylvan' popd ================================================ FILE: examples/json_example.py ================================================ """How to write BDDs to JSON files, and load them.""" import dd.cudd as _bdd def json_example(): """Entry point.""" filename = 'storage.json' dump_bdd_as_json(filename) load_bdd_from_json(filename) def dump_bdd_as_json(filename): """Write a BDD to a JSON file.""" bdd = _bdd.BDD() bdd.declare('x', 'y', 'z') u = bdd.add_expr(r'(x /\ y) \/ ~ z') roots = dict(u=u) bdd.dump(filename, roots) print(f'Dumped BDD: {u}') def load_bdd_from_json(filename): """Load a BDD from a JSON file.""" bdd = _bdd.BDD() roots = bdd.load(filename) print(f'Loaded BDD: {roots}') if __name__ == '__main__': json_example() ================================================ FILE: examples/json_load.py ================================================ """Loading BDD from JSON using the `json` module. This example shows how to load from JSON, and convert to a `networkx` graph. BDDs can be loaded from JSON into BDD contexts using the methods: - `dd.autoref.BDD.load()` - `dd.cudd.BDD.load()` """ import json import textwrap as _tw import dd.autoref as _bdd import networkx as _nx def dump_load_example( ) -> None: """Loading to `networkx` graph, using `json`.""" filename = 'example_bdd.json' create_and_dump_bdd(filename) graph = load_and_map_to_nx(filename) # print `graph` print('The loaded graph is:') for u, v in graph.edges(): print(f'edge: {u} -> {v}') roots = graph.roots level_of_var = graph.level_of_var print(_tw.dedent(f''' with graph roots: {roots} and variable levels: {level_of_var} ''')) def create_and_dump_bdd( filename: str ) -> None: """Write BDD to JSON file.""" bdd = _bdd.BDD() bdd.declare('x', 'y', 'z') u = bdd.add_expr(r'x /\ (~ y \/ z)') roots = dict(u=u) bdd.dump( filename, roots=roots) class BDDGraph( _nx.DiGraph): """Storing also roots and variable levels.""" def __init__( self, *arg, **kw): super().__init__(*arg, **kw) self.roots: dict | None = None self.level_of_var: dict | None = None def load_and_map_to_nx( filename: str ) -> BDDGraph: """Return graph loaded from JSON.""" with open(filename, 'r') as fd: data = fd.read() data = json.loads(data) # map to nx graph = BDDGraph() for k, v in data.items(): print(k, v) if k in ('roots', 'level_of_var'): continue node = int(k) level, node_low, node_high = v graph.add_edge(node, node_low) graph.add_edge(node, node_high) graph.roots = data['roots'] graph.level_of_var = data['level_of_var'] return graph if __name__ == '__main__': dump_load_example() ================================================ FILE: examples/np.py ================================================ """How the variable order in a BDD affects the number of nodes. Reference ========= [1] Randal Bryant "On the complexity of VLSI implementations and graph representations of Boolean functions with application to integer multiplication" TOC, 1991 """ import dd.autoref as _bdd def comparing_two_variable_orders(): n = 6 # declare variables vrs = [ f'x{i}' for i in range(2 * n)] bdd = _bdd.BDD() bdd.declare(*vrs) # equality constraints cause difficulties with BDD size def eq(i): j = (i + n + 1) % (2 * n) return f' x{i} <=> x{j} ' expr_1 = r' /\ '.join(map( eq, range(n))) u = bdd.add_expr(expr_1) def eq(k): i = 2 * k j = 2 * k + 1 return f' x{i} <=> x{j} ' expr_2 = r' /\ '.join(map( eq, range(n))) v = bdd.add_expr(expr_2) bdd.collect_garbage() # an order that yields a small BDD for `expr` good_order = [ f'x{i - 1}' for i in [ 1, 7, 3, 9, 5, 11, 2, 8, 4, 10, 6, 12]] # an order that yields a large BDD for `expr` bad_order = list(vrs) # plot _bdd.reorder(bdd, list_to_dict(good_order)) bdd.dump('good.pdf') _bdd.reorder(bdd, list_to_dict(bad_order)) bdd.dump('bad.pdf') def list_to_dict(c): return { var: level for level, var in enumerate(c)} def prime(s): return f"{s}'" if __name__ == '__main__': comparing_two_variable_orders() ================================================ FILE: examples/queens.py ================================================ """N-Queens problem using one-hot encoding. Reference ========= [1] Henrik R. Andersen "An introduction to binary decision diagrams" Lecture notes for "Efficient Algorithms and Programs", 1999 The IT University of Copenhagen Section 6.1 """ import collections.abc as _abc import pickle import time import dd.bdd as _bdd def solve_queens(n): """Return set of models for the `n`-queens problem. @rtype: `int`, `BDD` """ vrs = [_var_str(i, j) for i in range(n) for j in range(n)] bdd = _bdd.BDD() bdd.declare(*vrs) s = queens_formula(n) u = bdd.add_expr(s) return u, bdd def queens_formula(n): """Return a non-trivial propositional formula for the problem.""" # i = row index # j = column index present = at_least_one_queen_per_row(n) rows = at_most_one_queen_per_line(True, n) cols = at_most_one_queen_per_line(False, n) slash = at_most_one_queen_per_diagonal(True, n) backslash = at_most_one_queen_per_diagonal(False, n) s = _conjoin([present, rows, cols, slash, backslash]) return s def at_least_one_queen_per_row(n): """Return formula as `str`.""" c = list() for i in range(n): xijs = [_var_str(i, j) for j in range(n)] s = _disjoin(xijs) c.append(s) return _conjoin(c) def at_most_one_queen_per_line(row, n): """Return formula as `str`. @param row: if `True`, then constrain rows, else columns. """ c = list() for i in range(n): if row: xijs = [_var_str(i, j) for j in range(n)] else: xijs = [_var_str(j, i) for j in range(n)] s = mutex(xijs) c.append(s) return _conjoin(c) def at_most_one_queen_per_diagonal(slash, n): """Return formula as `str`. @param slash: if `True`, then constrain anti-diagonals, else diagonals. """ c = list() if slash: a = -n b = n else: a = 0 b = 2 * n for k in range(a, b): if slash: ij = [(i, i + k) for i in range(n)] else: ij = [(i, k - i) for i in range(n)] ijs = [(i, j) for i, j in ij if 0 <= i < n and 0 <= j < n] if not ij: continue xijs = [_var_str(i, j) for i, j in ijs] s = mutex(xijs) c.append(s) return _conjoin(c) def mutex(v): """Return formula for at most one variable `True`. @param v: iterable of variables as `str` """ v = set(v) c = list() for x in v: rest = _disjoin(y for y in v if y != x) s = f'{x} => ~ ({rest})' c.append(s) return _conjoin(c) def _var_str(i, j): """Return variable for occupancy of cell at {row: i, column: j}.""" return f'x{i}{j}' def _conjoin( strings: _abc.Iterable ) -> str: """Return conjunction of `strings`.""" expr = _apply_infix( strings, operator=r' /\ ') if not expr: expr = 'FALSE' return expr def _disjoin( strings: _abc.Iterable ) -> str: """Return disjunction of `strings`.""" expr = _apply_infix( strings, operator=r' \/ ') if not expr: expr = 'TRUE' return expr def _apply_infix( strings: _abc.Iterable, operator: str ) -> str: """Apply infix `operator` to `strings`.""" nonempty = filter(None, strings) parenthesized = map(_parenthesize, nonempty) return operator.join(parenthesized) def _parenthesize(string) -> str: """Return `string` within parentheses.""" return f'({string})' def benchmark(n): """Run for `n` queens and print statistics.""" t0 = time.perf_counter() u, bdd = solve_queens(n) t1 = time.perf_counter() dt = t1 - t0 s = ( '------\n' f'queens: {n}\n' f'time: {dt} (sec)\n' f'node: {u}\n' f'total nodes: {len(bdd)}\n' '------\n') print(s) return dt def _example(): n_max = 9 fname = 'dd_times.p' times = dict() for n in range(n_max + 1): t = benchmark(n) times[n] = t with open(fname, 'wb') as f: pickle.dump(times, f) if __name__ == '__main__': _example() ================================================ FILE: examples/reachability.py ================================================ """Reachability computation over a graph. This example is discussed in the documentation [1]. Propositional variables are used. If you are interested in using integers, then take a look at the package `omega`. [1](https://github.com/tulip-control/dd/blob/ main/doc.md#example-reachability-analysis) """ import dd.autoref as _bdd # uncomment if you have compiled `dd.cudd` # import dd.cudd as _bdd def transition_system(bdd): """Return the transition relation of a graph.""" dvars = ["x0", "x0'", "x1", "x1'"] for var in dvars: bdd.add_var(var) s = r''' ((~ x0 /\ ~ x1) => ( (~ x0' /\ ~ x1') \/ (x0' /\ ~ x1') )) /\ ((x0 /\ ~ x1) => ~ (x0' /\ x1')) /\ ((~ x0 /\ x1) => ( (~ x0' /\ x1') \/ (x0' /\ ~ x1') )) /\ ~ (x0 /\ x1) ''' transitions = bdd.add_expr(s) return transitions def least_fixpoint(transitions, bdd): """Return ancestor nodes.""" # target is the set {2} target = bdd.add_expr(r'~ x0 /\ x1') # start from empty set q = bdd.false qold = None prime = {"x0": "x0'", "x1": "x1'"} qvars = {"x0'", "x1'"} # fixpoint reached ? while q != qold: qold = q next_q = bdd.let(prime, q) u = transitions & next_q # existential quantification over x0', x1' pred = bdd.exist(qvars, u) # alternative: pred = bdd.quantify(u, qvars, forall=False) q = q | pred | target return q def reachability_example(): bdd = _bdd.BDD() transitions = transition_system(bdd) q = least_fixpoint(transitions, bdd) s = q.to_expr() print(s) if __name__ == '__main__': reachability_example() ================================================ FILE: examples/reordering.py ================================================ """Activate dynamic reordering for the Python implementation `dd.autoref`.""" import logging import dd.autoref as _bdd def demo_dynamic_reordering(): """Activate dynamic reordering and add nodes until triggered.""" print( '\n' + (50 * '-') + '\ndemo of dynamic reordering\n' + (50 * '-')) show_logging() bdd = create_manager() # activate reordering # (for the Python implementation `dd.autoref` reordering # is disabled by default, whereas for `dd.cudd` reordering # is enabled by default) bdd.configure(reordering=True) print_manager_size(bdd) # nearly empty BDD manager print_var_levels(bdd) # add enough nodes to trigger reordering nodes = trigger_reordering(bdd) print_manager_size(bdd) print_var_levels(bdd) # variables have been reordered def show_logging(): """Display logging messages relevant to reordering. To log more details, increase the verbosity level to `logging.DEBUG`. """ logger = logging.getLogger('dd.bdd') logger.setLevel(logging.INFO) logger.addHandler(logging.StreamHandler()) def create_manager(): """Return a BDD manager with plenty of variables declared.""" bdd = _bdd.BDD() vrs = [f'x{i}' for i in range(100)] bdd.declare(*vrs) return bdd def trigger_reordering(bdd): """Add several nodes to the manager. Dynamic reordering is triggered when the total number of nodes that are in the manager reaches a certain threshold. We add nodes in order to reach that that threshold, and thus trigger reordering. To witness the reordering happen, look at the logging messages. """ nodes = list() for i in range(25): expr = ( r'(x{i1} /\ x{i2}) \/ (x{i3} /\ x{i4})' r' \/ (x{i5} /\ x{i6})').format( i1=i, i2=i + 6, i3=i + 7, i4=i + 8, i5=i + 9, i6=i + 10) u = bdd.add_expr(expr) nodes.append(u) return nodes def print_var_levels(bdd): """Print level of each variable.""" n = len(bdd.vars) levels = [ bdd.var_at_level(level) for level in range(n)] print( 'Variable order (starting at level 0):\n' f'{levels}') def demo_static_reordering(): """How to invoke reordering explicitly.""" print( '\n' + (50 * '-') + '\ndemo of static reordering\n' + (50 * '-')) bdd = _bdd.BDD() bdd.declare('z1', 'z2', 'z3', 'y1', 'y2', 'y3') expr = r'(z1 /\ y1) \/ (z2 /\ y2) \/ (z3 /\ y3)' u = bdd.add_expr(expr) print_manager_size(bdd) # invoke sifting _bdd.reorder(bdd) print_manager_size(bdd) def demo_specific_var_order(): """How to permute the variables to a desired order.""" print( '\n' + (50 * '-') + '\ndemo of user-defined variable permutation\n' + (50 * '-')) bdd = _bdd.BDD() bdd.declare('a', 'b', 'c') u = bdd.add_expr(r'(a \/ b) /\ ~ c') print_var_levels(bdd) # reorder desired_order = dict(a=2, b=0, c=1) _bdd.reorder(bdd, desired_order) # confirm print_var_levels(bdd) def print_manager_size(bdd): print(f'Nodes in manager: {len(bdd)}') if __name__ == '__main__': demo_dynamic_reordering() demo_static_reordering() demo_specific_var_order() ================================================ FILE: examples/transfer_bdd.py ================================================ """How to copy a BDD from one manager to another.""" import dd.autoref as _bdd def transfer(): """Copy a BDD from one manager to another.""" # create two BDD managers source = _bdd.BDD() target = _bdd.BDD() # declare the variables in both BDD managers vrs = ['a', 'b'] source.declare(*vrs) target.declare(*vrs) # create a BDD with root `u` u = source.add_expr(r'a /\ b') # copy the BDD `u` to the BDD manager `target` u_ = source.copy(u, target) def copy_variable_order(): """As in `transfer`, and copy variable order too.""" source = _bdd.BDD() target = _bdd.BDD() # declare variables in the source BDD manager source.declare('a', 'b') # create a BDD with root `u` u = source.add_expr(r'a /\ b') # copy the variables, and the variable order target.declare(*source.vars) target.reorder(source.var_levels) # copy the BDD `u` to the BDD manager `target` u_ = source.copy(u, target) if __name__ == '__main__': transfer() copy_variable_order() ================================================ FILE: examples/variable_substitution.py ================================================ """Renaming variables.""" import dd.autoref as _bdd # import dd.cudd as _bdd # uncomment to use CUDD def variable_substitution(): # instantiate a shared BDD manager bdd = _bdd.BDD() bdd.declare('x', 'y', 'u', 'v') # create the BDD for the disjunction of x and y u = bdd.add_expr(r'x \/ y') # Substitution of x' for x and y' for y. # In TLA+ we can write this as: # # LET # x == u # y == v # IN # x \/ y rename = dict(x='u', y='v') v = bdd.let(rename, u) # show the result s = bdd.to_expr(v) print(s) # another way to confirm that the result is as expected v_ = bdd.add_expr(r'u \/ v') assert v == v_ if __name__ == '__main__': variable_substitution() ================================================ FILE: examples/what_is_a_bdd.py ================================================ """How BDDs are implemented. This module describes the main characteristics of the data structure used in the module `dd.bdd`. The module `dd.autoref` is an interface to the implementation that is in the module `dd.bdd`. """ def bdd_implementation_example(): """Main details of `dd.bdd.BDD`. The module `dd.bdd.BDD` contains the Python implementation of binary decision diagrams. """ # the graph that represents # the BDDs stored in memory successors = { 1: (2, None, None), # The node `1` represents the value `TRUE`. # The node `-1` represents the value `FALSE`. # Node `1` is used also as node `-1`. # # The number of "-" symbols on the edges # along the path that reaches the node `1` # determines whether `1` will be regarded # as `-1` when reached. 2: (1, -1, 1), # node_id: # (level, # successor_if_false, # successor_if_true) 3: (0, -1, 2)} # Keys are positive integers. # Doing so reduces how much # memory needs to be used. var_to_level = dict( x=0, y=1) values_to_substitute = dict( x=False, y=True) bdd_reference = 3 # BDD that means `x /\ y`, # the conjunction of `x` and `y`. # invert dictionary level_to_var = { level: varname for varname, level in var_to_level.items()} # Exercise: change the implementation # of the function `let()`, so that # `values_to_substitute` can be # an assignment to only some of # the variables that occur in the # BDD given to `let()`. result = let( values_to_substitute, bdd_reference, successors, level_to_var) print(f'{result = }') def let( values: dict[str, bool], bdd_ref: int, successors: dict[ int, tuple[ int, int | None, int | None]], level_to_var: dict[ int, str] ) -> int: """Recursively substitute values for variables. Return a binary decision diagram that represents the result of this substitution. @param values: assignment of Boolean values to variable names @param bdd_ref: a node, key in successors @param successors: graph that stores nodes @return: BDD node, which is a key in `successors` """ # leaf ? if abs(bdd_ref) == 1: return bdd_ref # nonleaf node key = abs(bdd_ref) level, low, high = successors[key] variable_name = level_to_var[level] variable_value = values[variable_name] if variable_value: successor = high else: successor = low result = let( values, successor, successors, level_to_var) # copy sign if bdd_ref < 0: result = - result return result if __name__ == '__main__': bdd_implementation_example() ================================================ FILE: setup.py ================================================ """Installation script.""" import argparse as _arg import logging import os import sys import setuptools import download PACKAGE_NAME = 'dd' DESCRIPTION = ( 'Binary decision diagrams implemented in pure Python, ' 'as well as Cython wrappers of CUDD, Sylvan, and BuDDy.') LONG_DESCRIPTION = ( 'dd is a package for working with binary decision diagrams ' 'that includes both a pure Python implementation and ' 'Cython bindings to C libraries (CUDD, Sylvan, BuDDy). ' 'The Python and Cython modules implement the same API, ' 'so the same user code runs with both. ' 'All the standard operations on BDDs are available, ' 'including dynamic variable reordering using sifting, ' 'garbage collection, dump/load from files, plotting, ' 'and a parser of quantified Boolean expressions. ' 'More details can be found in the README at: ' 'https://github.com/tulip-control/dd') PACKAGE_URL = f'https://github.com/tulip-control/{PACKAGE_NAME}' PROJECT_URLS = { 'Bug Tracker': 'https://github.com/tulip-control/dd/issues', 'Documentation': 'https://github.com/tulip-control/dd/blob/main/doc.md', 'Source Code': 'https://github.com/tulip-control/dd'} VERSION_FILE = f'{PACKAGE_NAME}/_version.py' VERSION = '0.6.1' VERSION_FILE_TEXT = ( '# This file was generated from setup.py\n' "version = '{version}'\n") PYTHON_REQUIRES = '>=3.11' INSTALL_REQUIRES = [ 'astutils >= 0.0.5', 'networkx >= 2.4', 'ply >= 3.4, <= 3.10', 'setuptools >= 65.6.0'] TESTS_REQUIRE = [ 'pytest >= 4.6.11'] CLASSIFIERS = [ 'Development Status :: 2 - Pre-Alpha', 'Intended Audience :: Developers', 'Intended Audience :: Science/Research', 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Cython', 'Programming Language :: Python :: 3 :: Only', 'Topic :: Scientific/Engineering', 'Topic :: Software Development'] KEYWORDS = [ 'bdd', 'binary decision diagram', 'decision diagram', 'boolean', 'networkx', 'dot', 'graphviz'] def git_version( version: str ) -> str: """Return version with local version identifier.""" import git as _git repo = _git.Repo('.git') repo.git.status() # assert versions are increasing latest_tag = repo.git.describe( match='v[0-9]*', tags=True, abbrev=0) latest_version = _parse_version(latest_tag[1:]) given_version = _parse_version(version) if latest_version > given_version: raise AssertionError( (latest_tag, version)) sha = repo.head.commit.hexsha if repo.is_dirty(): return f'{version}.dev0+{sha}.dirty' # commit is clean # is it release of `version` ? try: tag = repo.git.describe( match='v[0-9]*', exact_match=True, tags=True, dirty=True) except _git.GitCommandError: return f'{version}.dev0+{sha}' if tag != f'v{version}': raise AssertionError((tag, version)) return version def _parse_version( version: str ) -> tuple[ int, int, int]: """Return numeric version.""" numerals = version.split('.') if len(numerals) != 3: raise ValueError(numerals) return tuple(map(int, numerals)) def parse_args( ) -> _arg.Namespace: """Return `args` irrelevant to `setuptools`.""" parser = _arg.ArgumentParser() parser.add_argument( '--fetch', action='store_true', help='download cudd from its website') parser.add_argument( '--linetrace', action='store_true', help='use line tracing for Cython extensions') for opt in download.EXTENSIONS: parser.add_argument( f'--{opt}', default=None, const='', type=str, nargs='?', help=f'build Cython extension {opt}') args, unknown = parser.parse_known_args() args.sdist = 'sdist' in unknown args.bdist_wheel = 'bdist_wheel' in unknown # avoid confusing `setuptools` sys.argv = [sys.argv[0], *unknown] return args def read_env_vars( ) -> dict: """Read relevant environment variables.""" keys = { k: '' for k in download.EXTENSIONS} keys['fetch'] = True env_vars = { k: v for k, v in keys.items() if f'DD_{k.upper()}' in os.environ} print('`setup.py` of `dd` read environment variables:') print(env_vars) return env_vars def run_setup( ) -> None: """Build parser, get version from `git`, install.""" env_vars = read_env_vars() args = parse_args() dargs = vars(args) for k, v in env_vars.items(): if dargs[k] in (None, False): dargs[k] = v if args.fetch: download.fetch_cudd() # build extensions ? ext_modules = download.extensions(args) # version try: version = git_version(VERSION) except AssertionError: raise except: print('No git info: Assume release.') version = VERSION s = VERSION_FILE_TEXT.format(version=version) with open(VERSION_FILE, 'w') as f: f.write(s) _build_parsers() setuptools.setup( name=PACKAGE_NAME, version=version, description=DESCRIPTION, long_description=LONG_DESCRIPTION, author='Caltech Control and Dynamical Systems', author_email='tulip@tulip-control.org', url=PACKAGE_URL, project_urls=PROJECT_URLS, license='BSD-3-Clause', license_files=['LICENSE'], python_requires=PYTHON_REQUIRES, install_requires=INSTALL_REQUIRES, packages=[PACKAGE_NAME], package_dir={PACKAGE_NAME: PACKAGE_NAME}, include_package_data=True, zip_safe=False, ext_modules=ext_modules, classifiers=CLASSIFIERS, keywords=KEYWORDS) def _build_parsers( ) -> None: """Cache each parser's state machine.""" if not _parser_requirements_installed(): return import dd.dddmp import dd._parser logging.getLogger('astutils').setLevel('ERROR') dd.dddmp._rewrite_tables(outputdir=PACKAGE_NAME) dd._parser._rewrite_tables(outputdir=PACKAGE_NAME) def _parser_requirements_installed( ) -> bool: """Return `True` if parser requirements found.""" try: import astutils import ply except ImportError: print( 'WARNING: `dd` could not cache parser tables ' '(ignore this if running only for metadata information).') return False return True if __name__ == '__main__': run_setup() ================================================ FILE: tests/.coveragerc ================================================ # config for `coverage.py` [run] plugins = Cython.Coverage ================================================ FILE: tests/README.md ================================================ Besides tests for the package `dd`, this directory contains the script `inspect_cython_signatures.py`, which checks the compliance of a Cython class to a Python specification (motivated by the unavailability of `ABC` in Cython). ================================================ FILE: tests/autoref_test.py ================================================ """Tests of the module `dd.autoref`.""" # This file is released in the public domain. # import logging import dd.autoref as _bdd import dd.bdd import dd._utils as _utils import pytest import common import common_bdd logging.getLogger('astutils').setLevel('ERROR') class Tests(common.Tests): def setup_method(self): self.DD = _bdd.BDD class BDDTests(common_bdd.Tests): def setup_method(self): self.DD = _bdd.BDD def test_str(): bdd = _bdd.BDD() s = str(bdd) s + 'must be a string' def test_find_or_add(): bdd = _bdd.BDD() bdd.declare('x', 'y') n = len(bdd) u = bdd.find_or_add('x', bdd.true, bdd.false) m = len(bdd) assert n < m, (n, m) u_ = bdd.find_or_add('x', bdd.true, bdd.false) assert u == u_, (u, u_) m_ = len(bdd) assert m == m_, (m, m_) def test_count(): bdd = _bdd.BDD() bdd.declare('x', 'y') u = bdd.add_expr('x') n = bdd.count(u) assert n == 1, n u = bdd.add_expr(r'x /\ y') n = bdd.count(u) assert n == 1, n u = bdd.add_expr(r'x \/ y') n = bdd.count(u) assert n == 3, n def test_dump_load(): vrs = ['x', 'y', 'z'] s = r'x \/ y \/ ~ z' fname = 'foo.p' # dump bdd = _bdd.BDD() bdd.declare(*vrs) u = bdd.add_expr(s) bdd.dump(fname, roots=[u]) # load other = _bdd.BDD() roots_other = other.load(fname) assert len(roots_other) == 1, roots_other v, = roots_other v_ = other.add_expr(s) assert v == v_, (v, v_) def test_dump_using_graphviz(): bdd = _bdd.BDD() bdd.declare('x', 'y') u = bdd.add_expr(r'x /\ y') bdd.dump('bdd.dot') bdd.dump('bdd.dot', filetype='dot') bdd.dump('bdd.pdf') bdd.dump('bdd.pdf', filetype='pdf') bdd.dump('bdd', filetype='pdf') bdd.dump('bdd.png') bdd.dump('bdd.png', filetype='png') bdd.dump('bdd.svg') bdd.dump('bdd.svg', filetype='svg') bdd.dump('bdd.ext', filetype='pdf') with pytest.raises(ValueError): bdd.dump('bdd.ext') def test_image(): bdd = _bdd.BDD() bdd.declare('x', 'y', 'z') action = bdd.add_expr('x => y') source = bdd.add_expr('x') qvars = {'x'} rename = dict(y='x') u = _bdd.image(action, source, rename, qvars) u_ = bdd.add_expr('x') assert u == u_ def test_preimage(): bdd = _bdd.BDD() bdd.declare('x', 'y', 'z') action = bdd.add_expr('x <=> y') target = bdd.add_expr('x') qvars = {'y'} rename = dict(x='y') u = _bdd.preimage(action, target, rename, qvars) u_ = bdd.add_expr('x') assert u == u_ def test_reorder_2(): bdd = _bdd.BDD() vrs = [ 'x', 'y', 'z', 'a', 'b', 'c', 'e', 'z1', 'z2', 'z3', 'y1', 'y2', 'y3'] bdd = _bdd.BDD() bdd.declare(*vrs) expr_1 = r'(~ z \/ (c /\ b)) /\ e /\ (a /\ (~ x \/ y))' # Figs. 6.24, 6.25 Baier 2008 expr_2 = r'(z1 /\ y1) \/ (z2 /\ y2) \/ (z3 /\ y3)' u = bdd.add_expr(expr_1) v = bdd.add_expr(expr_2) bdd.collect_garbage() n = len(bdd) assert n == 23, n bdd.reorder() n_ = len(bdd) assert n > n_, (n, n_) bdd.assert_consistent() def test_configure_dynamic_reordering(): bdd = _bdd.BDD() vrs = [ 'x', 'y', 'z', 'a', 'b', 'c', 'e', 'z1', 'z2', 'z3', 'y1', 'y2', 'y3'] expr_1 = r'(~ z \/ (c /\ b)) /\ e /\ (a /\ (~ x \/ y))' expr_2 = r'(z1 /\ y1) \/ (z2 /\ y2) \/ (z3 /\ y3)' # without dynamic reordering bdd = _bdd.BDD() bdd.declare(*vrs) u = bdd.add_expr(expr_1) v = bdd.add_expr(expr_2) bdd.collect_garbage() n = len(bdd) assert n == 23, n # with dynamic reordering del u, v, bdd dd.bdd.REORDER_STARTS = 7 bdd = _bdd.BDD() bdd.declare(*vrs) bdd.configure(reordering=True) u = bdd.add_expr(expr_1) v = bdd.add_expr(expr_2) bdd.collect_garbage() n = len(bdd) assert n < 23, n def test_collect_garbage(): bdd = _bdd.BDD() n = len(bdd) assert n == 1, n bdd.declare('x', 'y') u = bdd.add_expr(r'x \/ y') bdd.collect_garbage() n = len(bdd) assert n > 1, n del u bdd.collect_garbage() n = len(bdd) assert n == 1, n def test_copy_vars(): bdd = _bdd.BDD() other = _bdd.BDD() vrs = {'x', 'y', 'z'} bdd.declare(*vrs) _bdd.copy_vars(bdd, other) assert vrs.issubset(other.vars) def test_copy_bdd(): bdd = _bdd.BDD() other = _bdd.BDD() bdd.declare('x') other.declare('x') u = bdd.var('x') v = _bdd.copy_bdd(u, other) v_ = other.var('x') assert v == v_, other.to_expr(v) # involution u_ = _bdd.copy_bdd(v, bdd) assert u == u_, bdd.to_expr(u_) def test_func_len(): bdd = _bdd.BDD() bdd.declare('x', 'y') u = bdd.add_expr('x') n = len(u) assert n == 2, n u = bdd.add_expr(r'x /\ y') n = len(u) assert n == 3, n def test_dd_version(): import dd assert hasattr(dd, '__version__') version = dd.__version__ assert version is not None assert isinstance(version, str), version if __name__ == '__main__': test_str() ================================================ FILE: tests/bdd_test.py ================================================ """Tests of the module `dd.bdd`.""" # This file is released in the public domain. # import logging import os import dd.autoref import dd.bdd as _bdd import networkx as nx import networkx.algorithms.isomorphism as iso import pytest class BDD(_bdd.BDD): """Disables refcount check upon shutdown. This script tests the low-level manager, where reference counting is not automated. For simplicity, references are not cleared at the end of tests here. Automated reference counting is in `dd.autoref`. """ def __del__(self): pass def test_add_var(): b = BDD() # # automated level selection # first var j = b.add_var('x') assert len(b.vars) == 1, b.vars assert 'x' in b.vars, b.vars assert b.vars['x'] == 0, b.vars assert j == 0, j # second var j = b.add_var('y') assert len(b.vars) == 2, b.vars assert 'y' in b.vars, b.vars assert b.vars['y'] == 1, b.vars assert j == 1, j # third var j = b.add_var('z') assert len(b.vars) == 3, b.vars assert 'z' in b.vars, b.vars assert b.vars['z'] == 2, b.vars assert j == 2, j # # explicit level selection b = BDD() j = b.add_var('x', level=35) assert len(b.vars) == 1, b.vars assert 'x' in b.vars, b.vars assert b.vars['x'] == 35, b.vars assert j == 35, j j = b.add_var('y', level=5) assert len(b.vars) == 2, b.vars assert 'y' in b.vars, b.vars assert b.vars['y'] == 5, b.vars assert j == 5, j # attempt to add var at an existing level with pytest.raises(ValueError): b.add_var('z', level=35) with pytest.raises(ValueError): b.add_var('z', level=5) # # mixing automated and # explicit level selection b = BDD() b.add_var('x', level=2) b.add_var('y') assert len(b.vars) == 2, b.vars assert 'x' in b.vars, b.vars assert 'y' in b.vars, b.vars assert b.vars['x'] == 2, b.vars assert b.vars['y'] == 1, b.vars with pytest.raises(ValueError): b.add_var('z') b.add_var('z', level=0) def test_var(): b = BDD() with pytest.raises(ValueError): b.var('x') j = b.add_var('x') u = b.var('x') assert u > 0, u level, low, high = b.succ(u) assert level == j, (level, j) assert low == b.false, low assert high == b.true, high def test_assert_consistent(): g = two_vars_xy() g.assert_consistent() g = x_or_y() g.assert_consistent() g._succ[2] = (5, 1, 2) with pytest.raises(AssertionError): g.assert_consistent() g = x_or_y() g.roots.add(2) g._succ[4] = (0, 10, 1) with pytest.raises(AssertionError): g.assert_consistent() g = x_or_y() g.roots.add(2) g._succ[1] = (2, None, 1) with pytest.raises(AssertionError): g.assert_consistent() g = x_and_y() g.assert_consistent() def test_level_to_variable(): ordering = {'x': 0, 'y': 1} g = BDD(ordering) assert g.var_at_level(0) == 'x' assert g.var_at_level(1) == 'y' with pytest.raises(ValueError): g.var_at_level(10) def test_var_levels_attr(): bdd = BDD() bdd.declare('x', 'y') var_levels = bdd.var_levels assert len(var_levels) == 2, var_levels assert {'x', 'y'} == set(var_levels), var_levels assert {0, 1} == set(var_levels.values()), var_levels def test_descendants(): ordering = dict(x=0, y=1) b = BDD(ordering) u = b.add_expr(r'x /\ y') v = b.add_expr(r'x \/ y') roots = [u, v] nodes = b.descendants(roots) nodes_u = b.descendants([u]) nodes_v = b.descendants([v]) assert u in nodes_u, nodes_u assert v in nodes_v, nodes_v assert u in nodes, nodes assert v in nodes, nodes assert 1 in nodes_u, nodes_u assert 1 in nodes_v, nodes_v assert 1 in nodes, nodes assert len(nodes_u) == 3, nodes_u assert len(nodes_v) == 3, nodes_v assert nodes_u != nodes_v, (nodes_u, nodes_v) assert len(nodes) == 4, nodes assert nodes == nodes_u.union(nodes_v), ( nodes, b._succ) # no roots roots = [] nodes = b.descendants(roots) assert len(nodes) == 0, nodes # empty iterator roots = iter(tuple()) reachable = b.descendants(roots) assert reachable == set(), reachable # nonempty iterator roots = iter([u, v]) reachable = b.descendants(roots) assert u in reachable, reachable assert v in reachable, reachable assert 1 in reachable, reachable def test_is_essential(): g = two_vars_xy() assert g.is_essential(2, 'x') assert not g.is_essential(2, 'y') assert g.is_essential(3, 'y') assert not g.is_essential(3, 'x') g = x_and_y() assert g.is_essential(2, 'x') assert g.is_essential(3, 'y') assert g.is_essential(4, 'x') assert g.is_essential(4, 'y') assert not g.is_essential(3, 'x') assert not g.is_essential(-1, 'x') assert not g.is_essential(-1, 'y') assert not g.is_essential(1, 'x') assert not g.is_essential(1, 'y') # variable not in the ordering assert not g.is_essential(2, 'z') def test_support(): g = two_vars_xy() assert g.support(2) == {'x'} assert g.support(3) == {'y'} g = x_and_y() assert g.support(4) == {'x', 'y'} assert g.support(3) == {'y'} g = x_or_y() assert g.support(4) == {'x', 'y'} assert g.support(3) == {'y'} def test_count(): g = x_and_y() assert g.count(4) == 1 g = x_or_y() r = g.count(4) assert r == 3, r r = g.count(4, nvars=2) assert r == 3, r r = g.count(-4) assert r == 1, r r = g.count(-4, nvars=2) assert r == 1, r r = g.count(4, 3) assert r == 6, r r = g.count(-4, 3) assert r == 2, r with pytest.raises(Exception): g.count() r = g.count(4) assert r == 3, r g = _bdd.BDD() r = g.count(g.false) assert r == 0, r r = g.count(g.true) assert r == 1, r g.add_var('x') x = g.var('x') r = g.count(x) assert r == 1, r neg_x = g.add_expr('~x') r = g.count(neg_x) assert r == 1, r g.add_var('y') u = g.add_expr(r'x /\ y ') r = g.count(u) assert r == 1, r def test_pick_iter(): # x /\ y g = x_and_y() u = 4 bits = {'x', 'y'} s = [{'x': 1, 'y': 1}] compare_iter_to_list_of_sets(u, g, s, bits) # care_bits == support (default) bits = None compare_iter_to_list_of_sets(u, g, s, bits) # # x \/ y g = x_or_y() u = 4 # support bits = None s = [{'x': 1, 'y': 0}, {'x': 1, 'y': 1}, {'x': 0, 'y': 1}] compare_iter_to_list_of_sets(u, g, s, bits) # only what appears along traversal bits = set() s = [{'x': 1}, {'x': 0, 'y': 1}] compare_iter_to_list_of_sets(u, g, s, bits) # bits < support bits = {'x'} s = [{'x': 1}, {'x': 0, 'y': 1}] compare_iter_to_list_of_sets(u, g, s, bits) bits = {'y'} s = [{'x': 1, 'y': 0},{'x': 1, 'y': 1}, {'x': 0, 'y': 1}] compare_iter_to_list_of_sets(u, g, s, bits) # # x /\ ~ y g = x_and_not_y() u = -2 bits = {'x', 'y'} s = [{'x': 1, 'y': 0}] compare_iter_to_list_of_sets(u, g, s, bits) # gaps in order order = {'x': 0, 'y': 1, 'z': 2} bdd = BDD(order) u = bdd.add_expr(r'x /\ z') (m,) = bdd.pick_iter(u) assert m == {'x': 1, 'z': 1}, m def compare_iter_to_list_of_sets(u, g, s, care_bits): s = list(s) for d in g.pick_iter(u, care_bits): assert d in s, d s.remove(d) assert not s, s def test_enumerate_minterms(): # non-empty cube cube = dict(x=False) bits = ['x', 'y', 'z'] r = _bdd._enumerate_minterms(cube, bits) p = set_from_generator_of_dict(r) q = set() for y in (False, True): for z in (False, True): m = (('x', False), ('y', y), ('z', z)) q.add(m) assert p == q, (p, q) # empty cube cube = dict() bits = ['x', 'y', 'z'] r = _bdd._enumerate_minterms(cube, bits) p = set_from_generator_of_dict(r) q = set() for x in (False, True): for y in (False, True): for z in (False, True): m = (('x', x), ('y', y), ('z', z)) q.add(m) assert p == q, (p, q) # fewer bits than cube cube = dict(x=False, y=True) bits = set() r = _bdd._enumerate_minterms(cube, bits) p = set_from_generator_of_dict(r) q = {(('x', False), ('y', True))} assert p == q, (p, q) def set_from_generator_of_dict(gen): r = list(gen) p = {tuple(sorted(m.items(), key=lambda x: x[0])) for m in r} return p def test_isomorphism(): ordering = {'x': 0} g = BDD(ordering) g.roots.update([2, 3]) g._succ[2] = (0, -1, 1) g._succ[3] = (0, -1, 1) h = g.reduction() assert set(h) == {1, 2}, set(h) assert 0 not in h assert h._succ[1] == (1, None, None) assert h._succ[2] == (0, -1, 1) assert h.roots == {2} def test_elimination(): ordering = {'x': 0, 'y': 1} g = BDD(ordering) g.roots.add(2) # high == low, so node 2 is redundant g._succ[2] = (0, 3, 3) g._succ[3] = (1, -1, 1) h = g.reduction() assert set(h) == {1, 2} def test_reduce_combined(): """Fig.5 in 1986 Bryant TOC""" ordering = {'x': 0, 'y': 1, 'z': 2} g = BDD(ordering) g.roots.add(2) g._succ[2] = (0, 3, 4) g._succ[3] = (1, -1, 5) g._succ[4] = (1, 5, 6) g._succ[5] = (2, -1, 1) g._succ[6] = (2, -1, 1) h = g.reduction() assert 1 in h assert ordering == h.vars r = nx.MultiDiGraph() r.add_node(1, level=3) r.add_node(2, level=0) r.add_node(3, level=1) r.add_node(4, level=2) r.add_edge(2, 3, value=False, complement=False) r.add_edge(2, 4, value=True, complement=False) r.add_edge(3, 4, value=True, complement=False) r.add_edge(3, 1, value=False, complement=True) r.add_edge(4, 1, value=False, complement=True) r.add_edge(4, 1, value=True, complement=False) (u, ) = h.roots compare(u, h, r) def test_reduction_complemented_edges(): bdd = BDD() bdd.add_var('x', level=0) bdd.add_var('y', level=1) a, b = map(bdd.level_of_var, ['x', 'y']) assert a < b, (a, b) # complemented edge from internal node to # non-terminal node expr = r'~ x /\ y' _test_reduction_complemented_edges(expr, bdd) # complemented edge from external reference to # non-terminal node expr = r'x /\ ~ y' u = bdd.add_expr(expr) assert u < 0, u _test_reduction_complemented_edges(expr, bdd) def _test_reduction_complemented_edges(expr, bdd): u = bdd.add_expr(expr) bdd.roots.add(u) bdd_r = bdd.reduction() v, = bdd_r.roots v_ = bdd_r.add_expr(expr) assert v == v_, (v, v_) bdd_r.assert_consistent() bdd.roots.remove(u) def test_find_or_add(): ordering = {'x': 0, 'y': 1} g = BDD(ordering) # init n = len(g) m = g._min_free assert n == 1, n assert m == 2, m # elimination rule i = 0 v = -1 w = 1 n = len(g) u = g.find_or_add(i, v, v) n_ = len(g) assert n == n_, (n, n_) assert u == v, (u, v) assert len(g._pred) == 1, g._pred t = (2, None, None) assert t in g._pred, g._pred assert g._pred[t] == 1, g._pred # unchanged min_free v = 1 m = g._min_free g.find_or_add(i, v, v) m_ = g._min_free assert m_ == m, (m_, m) # add new node g = BDD(ordering) v = -1 w = 1 n = len(g) m = g._min_free assert n == 1, n u = g.find_or_add(i, v, w) n_ = len(g) m_ = g._min_free assert u != v, (u, v) assert n_ == n + 1, (n, n_) assert m_ == m + 1, (m, m_) assert g._succ[u] == (i, -1, 1) assert (i, v, w) in g._pred assert abs(u) in g._ref assert g._ref[abs(u)] == 0 # terminal node `v`: 2 refs + 1 ref by manager assert g._ref[abs(v)] == 3, g._ref # independent increase of reference counters v = u w = w refv = g._ref[abs(v)] refw = g._ref[w] u = g.find_or_add(i, v, w) refv_ = g._ref[abs(v)] refw_ = g._ref[w] assert refv + 1 == refv_, (refv, refv_) assert refw + 1 == refw_, (refw, refw_) # add existing n = len(g) m = g._min_free refv = g._ref[abs(v)] refw = g._ref[w] r = g.find_or_add(i, v, w) n_ = len(g) m_ = g._min_free refv_ = g._ref[abs(v)] refw_ = g._ref[w] assert n == n_, (n, n_) assert m == m_, (m, m_) assert u == r, u assert refv == refv_, (refv, refv_) assert refw == refw_, (refw, refw_) # only non-terminals can be added with pytest.raises(ValueError): g.find_or_add(2, -1, 1) # low and high must already exist with pytest.raises(ValueError): g.find_or_add(0, 3, 4) # canonicity of complemented edges # v < 0, w > 0 g = BDD(ordering) i = 0 v = -1 w = 1 u = g.find_or_add(i, v, w) assert u > 0, u # v > 0, w < 0 v = 1 w = -1 u = g.find_or_add(i, v, w) assert u < 0, u assert abs(u) in g._succ, u _, v, w = g._succ[abs(u)] assert v < 0, v assert w > 0, w # v < 0, w < 0 v = -1 w = -2 u = g.find_or_add(i, v, w) assert u < 0, u _, v, w = g._succ[abs(u)] assert v > 0, v assert w > 0, w def test_next_free_int(): g = BDD() # contiguous g._succ = {1, 2, 3} start = 1 n = g._next_free_int(start) _assert_smaller_are_nodes(start, g) assert n == 4, n start = 3 n = g._next_free_int(start) _assert_smaller_are_nodes(start, g) assert n == 4, n # with blanks g._succ = {1, 3} start = 1 n = g._next_free_int(start) _assert_smaller_are_nodes(start, g) assert n == 2, n n = g._next_free_int(start=3) assert n == 4, n # full g._succ = {1, 2, 3} g.max_nodes = 3 with pytest.raises(Exception): g._next_free_int(start=1) def _assert_smaller_are_nodes(start, bdd): for i in range(1, start + 1): assert i in bdd, i def test_collect_garbage(): # all nodes are garbage g = BDD({'x': 0, 'y': 1}) u = g.add_expr(r'x /\ y') n = len(g) assert n == 4, n uref = g._ref[abs(u)] assert uref == 0, uref _, v, w = g._succ[abs(u)] vref = g._ref[abs(v)] wref = g._ref[w] # terminal node `v`: 6 refs + 1 ref by manager assert vref == 6, vref assert wref == 1, wref g.collect_garbage() n = len(g) assert n == 1, n assert u not in g, g._succ assert w not in g, g._succ # some nodes not garbage # projection of x is garbage g = BDD({'x': 0, 'y': 1}) u = g.add_expr(r'x /\ y') n = len(g) assert n == 4, n g._ref[abs(u)] += 1 uref = g._ref[abs(u)] assert uref == 1, uref g.collect_garbage() n = len(g) assert n == 3, n def test_top_cofactor(): ordering = {'x': 0, 'y': 1} g = BDD(ordering) x = ordering['x'] y = ordering['y'] u = g.find_or_add(y, -1, 1) assert g._top_cofactor(u, x) == (u, u) assert g._top_cofactor(u, y) == (-1, 1) u = g.find_or_add(x, -1, 1) assert g._top_cofactor(u, x) == (-1, 1) assert g._top_cofactor(-u, x) == (1, -1) def test_ite(): ordering = {'x': 0, 'y': 1} g = BDD(ordering) # x ix = ordering['x'] x = g.find_or_add(ix, -1, 1) h = ref_var(ix) compare(x, g, h) # y iy = ordering['y'] y = g.find_or_add(iy, -1, 1) h = ref_var(iy) compare(y, g, h) # x and y u = g.ite(x, y, -1) h = ref_x_and_y() compare(u, g, h) # x or y u = g.ite(x, 1, y) h = ref_x_or_y() compare(u, g, h) # negation assert g.ite(x, -1, 1) == -x, g._succ assert g.ite(-x, -1, 1) == x, g._succ def test_add_expr(): ordering = {'x': 0, 'y': 1} g = BDD(ordering) # x ix = ordering['x'] u = g.add_expr('x') h = ref_var(ix) compare(u, g, h) # x and y u = g.add_expr(r'x /\ y') h = ref_x_and_y() compare(u, g, h) def test_expr_comments(): bdd = BDD() bdd.declare('x', 'y', 'z', 'w') expr = r''' \* trailing comment (x \/ ( (* doubly-delimited comment *) y /\ ~ z)) (* multiline comment *) /\ w ''' expr_ = r'(x \/ (y /\ ~ z)) /\ w' u = bdd.add_expr(expr) u_ = bdd.add_expr(expr_) assert u == u_, (u, u_) def test_compose(): ordering = {'x': 0, 'y': 1, 'z': 2} g = BDD(ordering) # x /\ (x \/ z) a = g.add_expr(r'x /\ y') b = g.add_expr(r'x \/ z') c = g.let({'y': b}, a) d = g.add_expr(r'x /\ (x \/ z)') assert c == d, (c, d) # (y \/ z) /\ x ordering = {'x': 0, 'y': 1, 'z': 2, 'w': 3} g = BDD(ordering) a = g.add_expr(r'(x /\ y) \/ z') b = g.add_expr(r'(y \/ z) /\ x') c = g.let({'z': b}, a) assert c == b, (c, b) # long expr ordering = {'x': 0, 'y': 1, 'z': 2, 'w': 3} g = BDD(ordering) a = g.add_expr(r'(x /\ y) \/ (~ z \/ (w /\ y /\ x))') b = g.add_expr(r'(y \/ ~ z) /\ x') c = g.let({'y': b}, a) d = g.add_expr( r'(x /\ ((y \/ ~ z) /\ x)) \/ ' r' (~ z \/ (w /\ ((y \/ ~ z) /\ x) /\ x))') assert c == d, (c, d) # complemented edges ordering = {'x': 0, 'y': 1} g = BDD(ordering) f = g.add_expr('x <=> y') var = 'y' new_level = 0 var_node = g.find_or_add(new_level, -1, 1) u = g.let({var: var_node}, f) assert u == 1, g.to_expr(u) def test_vector_compose(): bdd = _bdd.BDD() bdd.declare('w', 'z', 'x', 'y') u = bdd.add_expr( r'(x /\ y) \/ (z /\ y)') x = bdd.var('x') not_y = bdd.add_expr('~ y') defs = dict(w=bdd.false, y=not_y) v = bdd.let(defs, u) # cache test: # repeated expression # changes edge sign, and # is found in cache v_ = bdd.add_expr( r'(x /\ ~ y) \/ (z /\ ~ y)') assert v == v_, (v, v_) def test_cofactor(): ordering = {'x': 0, 'y': 1, 'z': 2} g = BDD(ordering) # u not in g with pytest.raises(ValueError): g.let({'x': False, 'y': True, 'z': False}, 5) # x /\ y e = g.add_expr(r'x /\ y') x = g.add_expr('x') assert g.let({'x': False}, x) == -1 assert g.let({'x': True}, x) == 1 assert g.let({'x': False}, -x) == 1 assert g.let({'x': True}, -x) == -1 y = g.add_expr('y') assert g.let({'x': True}, e) == y assert g.let({'x': False}, e) == -1 assert g.let({'y': True}, e) == x assert g.let({'y': False}, e) == -1 assert g.let({'x': False}, -e) == 1 assert g.let({'x': True}, -e) == -y assert g.let({'y': False}, -e) == 1 assert g.let({'y': True}, -e) == -x def test_swap(): # x, y g = BDD({'x': 0, 'y': 1}) x = g.add_expr('x') y = g.add_expr('y') g.incref(x) g.incref(y) n = len(g) assert n == 3, n nold, n = g.swap('x', 'y') assert n == 3, n assert nold == n, nold assert g.vars == {'y': 0, 'x': 1}, g.vars g.assert_consistent() # functions remain invariant x_ = g.add_expr('x') y_ = g.add_expr('y') assert x == x_, (x, x_, g._succ) assert y == y_, (y, y_, g._succ) # external reference counts remain unchanged assert g._ref[abs(x)] == 1 assert g._ref[abs(y)] == 1 # x /\ y g = BDD({'x': 0, 'y': 1}) u = g.add_expr(r'x /\ y') g.incref(u) nold, n = g.swap('x', 'y') assert nold == n, (nold, n) assert g.vars == {'y': 0, 'x': 1}, g.vars u_ = g.add_expr(r'x /\ y') assert u == u_, (u, u_) g.assert_consistent() # reference counts unchanged assert g._ref[abs(u)] == 1 # x /\ ~ y # tests handling of complement edges e = r'x /\ ~ y' g = x_and_not_y() u = g.add_expr(e) g.incref(u) g.collect_garbage() n = len(g) assert n == 3, n nold, n = g.swap('x', 'y') assert n == 3, n assert nold == n, nold assert g.vars == {'x': 1, 'y': 0} g.assert_consistent() u_ = g.add_expr(e) # function u must have remained unaffected assert u_ == u, (u, u_, g._succ) # invert swap of: # x /\ ~ y nold, n = g.swap('x', 'y') assert n == 3, n assert nold == n, nold assert g.vars == {'x': 0, 'y': 1} g.assert_consistent() u_ = g.add_expr(e) assert u_ == u, (u, u_, g._succ) # Figs. 6.24, 6.25 Baier 2008 g = BDD({'z1': 0, 'y1': 1, 'z2': 2, 'y2': 3, 'z3': 4, 'y3': 5}) u = g.add_expr(r'(z1 /\ y1) \/ (z2 /\ y2) \/ (z3 /\ y3)') g.incref(u) n = len(g) assert n == 16, n g.collect_garbage() n = len(g) assert n == 7, n # sift to inefficient order g.swap('y1', 'z2') # z1, z2, y1, y2, z3, y3 g.swap('y2', 'z3') # z1, z2, y1, z3, y2, y3 g.swap('y1', 'z3') # z1, z2, z3, y1, y2, y3 n = len(g) assert n == 15, n g.assert_consistent() new_ordering = { 'z1': 0, 'z2': 1, 'z3': 2, 'y1': 3, 'y2': 4, 'y3': 5} assert g.vars == new_ordering, g.vars u_ = g.add_expr(r'(z1 /\ y1) \/ (z2 /\ y2) \/ (z3 /\ y3)') assert u_ == u, (u, u_, g._succ) # g.dump('g.pdf') def test_sifting(): # Figs. 6.24, 6.25 Baier 2008 g = BDD({'z1': 0, 'z2': 1, 'z3': 2, 'y1': 3, 'y2': 4, 'y3': 5}) u = g.add_expr(r'(z1 /\ y1) \/ (z2 /\ y2) \/ (z3 /\ y3)') g.incref(u) g.collect_garbage() n = len(g) assert n == 15, n _bdd.reorder(g) n_ = len(g) assert n > n_, (n, n_) u_ = g.add_expr(r'(z1 /\ y1) \/ (z2 /\ y2) \/ (z3 /\ y3)') g.incref(u) g.collect_garbage() g.assert_consistent() assert u == u_, (u, u_) def test_request_reordering(): ctx = Dummy() # reordering off n = ctx._last_len assert n is None, n _bdd._request_reordering(ctx) # reordering on ctx._last_len = 1 ctx.length = 3 # >= 2 = 2 * _last_len # large growth with pytest.raises(_bdd._NeedsReordering): _bdd._request_reordering(ctx) ctx._last_len = 2 ctx.length = 3 # < 4 = 2 * _last_len # small growth _bdd._request_reordering(ctx) def test_reordering_context(): ctx = Dummy() # top context ctx.assert_(False) with _bdd._ReorderingContext(ctx): ctx.assert_(True) raise _bdd._NeedsReordering() ctx.assert_(False) # nested context ctx._reordering_context = True with pytest.raises(_bdd._NeedsReordering): with _bdd._ReorderingContext(ctx): ctx.assert_(True) raise _bdd._NeedsReordering() ctx.assert_(True) # other exception ctx._reordering_context = False with pytest.raises(AssertionError): with _bdd._ReorderingContext(ctx): ctx.assert_(True) raise AssertionError() ctx.assert_(False) ctx._reordering_context = True with pytest.raises(Exception): with _bdd._ReorderingContext(ctx): raise Exception() ctx.assert_(True) class Dummy: """To test state machine for nesting context.""" def __init__(self): self._reordering_context = False self._last_len = None self.length = 1 def __len__(self): return self.length def assert_(self, value): c = self._reordering_context assert c is value, c def test_dynamic_reordering(): b = TrackReorderings() [b.add_var(var) for var in ['x', 'y', 'z', 'a', 'b', 'c', 'e']] # add expr with reordering off assert not b.reordering_is_on() assert b.n_swaps == 0, b.n_swaps u = b.add_expr(r'x /\ y /\ z') assert b.n_swaps == 0, b.n_swaps b.incref(u) n = len(b) assert n == 7, n # add expr with reordering on b._last_len = 6 assert b.reordering_is_on() v = b.add_expr(r'a /\ b') assert b.reordering_is_on() assert b.n_swaps == 0, b.n_swaps b.incref(v) n = len(b) assert n == 10, n # add an expr that triggers reordering assert b.reordering_is_on() w = b.add_expr(r'z \/ (~ a /\ x /\ ~ y)') assert b.reordering_is_on() n_swaps = b.n_swaps assert n_swaps > 0, n_swaps b.incref(w) assert u in b, (w, b._succ) assert v in b, (v, b._succ) assert w in b, (w, b._succ) # add another expr that triggers reordering old_n_swaps = n_swaps assert b.reordering_is_on() r = b.add_expr(r'(~ z \/ (c /\ b)) /\ e /\ (a /\ (~x \/ y))') b.add_expr(r'(e \/ ~ a) /\ x /\ (b \/ ~ y)') n_swaps = b.n_swaps assert n_swaps > old_n_swaps, (n_swaps, old_n_swaps) assert b.reordering_is_on() class TrackReorderings(BDD): """To record invocations of reordering.""" def __init__(self, *arg, **kw): self.n_swaps = 0 super().__init__(*arg, **kw) def swap(self, *arg, **kw): self.n_swaps += 1 return super().swap(*arg, **kw) def reordering_is_on(self): d = self.configure() r = d['reordering'] return r is True def test_undeclare_vars(): bdd = BDD() bdd.declare('x', 'y', 'z', 'w') # empty arg `vrs` u = bdd.add_expr(r'x /\ y /\ w') rm_vars = bdd.undeclare_vars() rm_vars_ = {'z'} assert rm_vars == rm_vars_, (rm_vars, rm_vars_) bdd_vars_ = dict(x=0, y=1, w=2) assert bdd.vars == bdd_vars_, bdd.vars bdd.assert_consistent() # nonempty `vrs` with all empty levels bdd = BDD() bdd.declare('x', 'y', 'z', 'w') u = bdd.add_expr(r'y /\ w') rm_vars = bdd.undeclare_vars('x', 'z') rm_vars_ = {'x', 'z'} assert rm_vars == rm_vars_, (rm_vars, rm_vars_) bdd_vars_ = dict(y=0, w=1) assert bdd.vars == bdd_vars_, bdd.vars bdd.assert_consistent() # nonempty `vrs` without all empty levels bdd = BDD() bdd.declare('x', 'y', 'z', 'w') u = bdd.add_expr(r'y /\ w') rm_vars = bdd.undeclare_vars('z') rm_vars_ = {'z'} assert rm_vars == rm_vars_, (rm_vars, rm_vars_) bdd_vars_ = dict(x=0, y=1, w=2) assert bdd.vars == bdd_vars_, bdd.vars bdd.assert_consistent() # remove only unused variables bdd = BDD() bdd.declare('x', 'y', 'z', 'w') u = bdd.add_expr(r'y /\ w') with pytest.raises(ValueError): bdd.undeclare_vars('z', 'y') def test_del_repeated_calls(): bdd = _bdd.BDD() bdd.declare('x', 'y', 'z') u = bdd.add_expr(r'x /\ y') v = bdd.add_expr(r'y /\ ~ z') assert _references_exist(bdd._ref) bdd.__del__() assert not _references_exist(bdd._ref) assert bdd._ref == {1: 0}, bdd._ref assert set(bdd._succ) == {1}, bdd._succ bdd.__del__() assert bdd._ref == {1: 0}, bdd._ref assert set(bdd._succ) == {1}, bdd._succ bdd.__del__() assert bdd._ref == {1: 0}, bdd._ref assert set(bdd._succ) == {1}, bdd._succ def _references_exist(refs): return any( v != 0 for v in refs.values()) def test_dump_load(): prefix = 'test_dump_load' fname = f'{prefix}.p' dvars = dict(x=0, y=1) # dump b = BDD(dvars) e = r'x /\ ~ y' u_dumped = b.add_expr(e) b.dump(fname, [u_dumped]) # load b = BDD(dvars) b.add_expr(r'x \/ y') u_new = b.add_expr(e) u_loaded, = b.load(fname) assert u_loaded == u_new, ( u_dumped, u_loaded, u_new) b.assert_consistent() def test_dump_load_manager(): prefix = 'test_dump_load_manager' g = BDD({'x': 0, 'y': 1}) e = r'x /\ ~ y' u = g.add_expr(e) g.incref(u) fname = f'{prefix}.p' g._dump_manager(fname) h = g._load_manager(fname) g.assert_consistent() u_ = h.add_expr(e) assert u == u_, (u, u_) # h.dump(f'{prefix}.pdf') def test_dump_using_graphviz(): bdd = BDD() bdd.declare('x', 'y', 'z') u = bdd.add_expr(r'x /\ y') v = bdd.add_expr(r'y /\ ~ z') roots = [u, v] filename_noext = 'bdd' filetypes = ['pdf', 'png', 'svg', 'dot'] for filetype in filetypes: _dump_bdd_roots_as_filetype( roots, bdd, filename_noext, filetype) def _dump_bdd_roots_as_filetype( roots, bdd, filename_noext, filetype): fname = f'{filename_noext}.{filetype}' if os.path.isfile(fname): os.remove(fname) bdd.dump(fname, roots) assert os.path.isfile(fname) def test_quantify(): ordering = {'x': 0, 'y': 1, 'z': 2} g = BDD(ordering) # x /\ y e = g.add_expr(r'x /\ ~ y') x = g.add_expr('x') not_y = g.add_expr('~ y') assert g.quantify(e, {'x'}) == not_y assert g.quantify(e, {'x'}, forall=True) == -1 assert g.quantify(e, {'y'}) == x assert g.quantify(e, {'x'}, forall=True) == -1 # x \/ y \/ z e = g.add_expr(r'x \/ y \/ z') xy = g.add_expr(r'x \/ y') yz = g.add_expr(r'y \/ z') zx = g.add_expr(r'z \/ x') assert g.quantify(e, {'x'}) assert g.quantify(e, {'y'}) assert g.quantify(e, {'z'}) assert g.quantify(e, {'z'}, forall=True) == xy assert g.quantify(e, {'x'}, forall=True) == yz assert g.quantify(e, {'y'}, forall=True) == zx # complement edges u = -x v = g.quantify(u, {'y'}, forall=True) assert v == -x, g.to_expr(v) # multiple values: test recursion e = g.add_expr(r'x /\ y /\ z') x = g.add_expr('x') r = g.quantify(e, {'y', 'z'}) assert r == x, r def test_quantifier_syntax(): b = BDD() [b.add_var(var) for var in ['x', 'y']] # constants u = b.add_expr(r'\E x: TRUE') assert u == b.true, u u = b.add_expr(r'\E x, y: TRUE') assert u == b.true, u u = b.add_expr(r'\E x: FALSE') assert u == b.false, u u = b.add_expr(r'\A x: TRUE') assert u == b.true, u u = b.add_expr(r'\A x: FALSE') assert u == b.false, u u = b.add_expr(r'\A x, y: FALSE') assert u == b.false, u # variables u = b.add_expr(r'\E x: x') assert u == b.true, u u = b.add_expr(r'\A x: x') assert u == b.false, u u = b.add_expr(r'\E x, y: x') assert u == b.true, u u = b.add_expr(r'\E x, y: y') assert u == b.true, u u = b.add_expr(r'\A x: y') assert u == b.var('y'), u u = b.add_expr(r'\A x: ~ y') u_ = b.apply('not', b.var('y')) assert u == u_, (u, u_) def test_rename(): ordering = {'x': 0, 'xp': 1} g = BDD(ordering) x = g.add_expr('x') xp = g.add_expr('xp') dvars = {'x': 'xp'} xrenamed = g.let(dvars, x) assert xrenamed == xp, xrenamed ordering = {'x': 0, 'xp': 1, 'y': 2, 'yp': 3, 'z': 4, 'zp': 5} g = BDD(ordering) u = g.add_expr(r'x /\ y /\ ~ z') dvars = {'x': 'xp', 'y': 'yp', 'z': 'zp'} urenamed = g.let(dvars, u) up = g.add_expr(r'xp /\ yp /\ ~ zp') assert urenamed == up, urenamed # assertion violations # non-neighbors dvars = {'x': 'yp'} r = g.let(dvars, u) r_ = g.add_expr(r'yp /\ y /\ ~ z') assert r == r_, (r, r_) # u not in bdd dvars = {'x': 'xp'} with pytest.raises(ValueError): g.let(dvars, 1000) # y essential for u dvars = {'x': 'y'} v = g.let(dvars, u) v_ = g.add_expr(r'y /\ ~ z') assert v == v_, (v, v_) # old and new vars intersect dvars = {'x': 'x'} v = g.let(dvars, u) assert v == u, (v, u) def test_rename_syntax(): b = BDD() [b.add_var(var) for var in ['x', 'y', 'z', 'w']] # single substitution u = b.add_expr(r'\S y / x: TRUE') assert u == b.true, u u = b.add_expr(r'\S y / x: FALSE') assert u == b.false, u u = b.add_expr(r'\S y / x: x') u_ = b.add_expr('y') assert u == u_, (u, u_) u = b.add_expr(r'\S y / x: z') u_ = b.add_expr('z') assert u == u_, (u, u_) u = b.add_expr(r'\S y / x: x /\ z') u_ = b.add_expr(r'y /\ z') assert u == u_, (u, u_) # multiple substitution u = b.add_expr(r'\S y / x, w / z: x /\ z') u_ = b.add_expr(r'y /\ w') assert u == u_, (u, u_) u = b.add_expr(r'\S y / x, w / z: z \/ ~ x') u_ = b.add_expr(r'w \/ ~ y') assert u == u_, (u, u_) def test_image_rename_map_checks(): ordering = {'x': 0, 'xp': 1, 'y': 2, 'yp': 3, 'z': 4, 'zp': 5} bdd = BDD(ordering) # non-adjacent rename = {0: 2, 3: 4} qvars = set() r = _bdd.image(1, 1, rename, qvars, bdd) assert r == 1, r r = _bdd.preimage(1, 1, rename, qvars, bdd) assert r == 1, r # overlapping keys and values rename = {0: 1, 1: 2} with pytest.raises(AssertionError): _bdd.image(1, 1, rename, qvars, bdd) with pytest.raises(AssertionError): _bdd.preimage(1, 1, rename, qvars, bdd) # may be in support after quantification ? trans = bdd.add_expr('x => xp') source = bdd.add_expr(r'x /\ y') qvars = {0} rename = {1: 0, 3: 2} with pytest.raises(AssertionError): _bdd.image(trans, source, rename, qvars, bdd) # in support of `target` ? qvars = set() trans = bdd.add_expr('y') target = bdd.add_expr(r'x /\ y') rename = {0: 2} r = _bdd.preimage(trans, target, rename, qvars, bdd) assert r == bdd.var('y'), r def test_preimage(): # exists: x, y # forall: z ordering = {'x': 0, 'xp': 1, 'y': 2, 'yp': 3, 'z': 4, 'zp': 5} rename = {0: 1, 2: 3, 4: 5} g = BDD(ordering) f = g.add_expr('~ x') t = g.add_expr('x <=> ~ xp') qvars = {1, 3} p = _bdd.preimage(t, f, rename, qvars, g) x = g.add_expr('x') assert x == p, (x, p) # a cycle # (x /\ y) --> (~ x /\ y) --> # (~ x /\ ~ y) --> (x /\ ~ y) --> wrap around t = g.add_expr( r'((x /\ y) => (~ xp /\ yp)) /\ ' r'((~ x /\ y) => (~ xp /\ ~ yp)) /\ ' r'((~ x /\ ~ y) => (xp /\ ~ yp)) /\ ' r'((x /\ ~ y) => (xp /\ yp))') f = g.add_expr(r'x /\ y') p = _bdd.preimage(t, f, rename, qvars, g) assert p == g.add_expr(r'x /\ ~ y') f = g.add_expr(r'x /\ ~ y') p = _bdd.preimage(t, f, rename, qvars, g) assert p == g.add_expr(r'~ x /\ ~ y') # backward reachable set f = g.add_expr(r'x /\ y') oldf = None while oldf != f: p = _bdd.preimage(t, f, rename, qvars, g) oldf = f f = g.apply('or', p, oldf) assert f == 1 # go around once f = g.add_expr(r'x /\ y') start = f for i in range(4): f = _bdd.preimage(t, f, rename, qvars, g) end = f assert start == end # forall z exists x, y t = g.add_expr( r'(' r' ((x /\ y) => (zp /\ xp /\ ~ yp)) \/ ' r' ((x /\ y) => (~ zp /\ ~ xp /\ yp))' r') /\ ' r'(~ (x /\ y) => False)') f = g.add_expr(r'x /\ ~ y') ep = _bdd.preimage(t, f, rename, qvars, g) p = g.quantify(ep, {'zp'}, forall=True) assert p == -1 f = g.add_expr(r'(x /\ ~ y) \/ (~ x /\ y)') ep = _bdd.preimage(t, f, rename, qvars, g) p = g.quantify(ep, {'zp'}, forall=True) assert p == g.add_expr(r'x /\ y') def test_assert_valid_ordering(): ordering = {'x': 0, 'y': 1} _bdd._assert_valid_ordering(ordering) incorrect_ordering = {'x': 0, 'y': 2} with pytest.raises(AssertionError): _bdd._assert_valid_ordering(incorrect_ordering) def test_assert_refined_ordering(): ordering = {'x': 0, 'y': 1} new_ordering = {'z': 0, 'x': 1, 'w': 2, 'y': 3} _bdd._assert_isomorphic_orders(ordering, new_ordering, ordering) def test_to_graphviz_dot(): def fmt(x): return str(abs(x)) # with roots g = x_and_y() dot = _bdd._to_dot([4, 2], g) r = _graph_from_dot(dot) for u in g: assert fmt(u) in r, (u, r) for u, (_, v, w) in g._succ.items(): su = fmt(u) assert su in r, (su, r) if v is None or w is None: assert v is None, v assert w is None, w continue sv = fmt(v) sw = fmt(w) assert sv in r[su], (su, sv, r) assert sw in r[su], (su, sw, r) # no roots dot = _bdd._to_dot(None, g) r = _graph_from_dot(dot) # `r` has 3 hidden nodes, # used to layout variable levels assert len(r) == 8, r def _graph_from_dot(dot_graph): """Return `dict` of `set` for graph.""" g = dict() return _graph_from_dot_recurse(dot_graph, g) def _graph_from_dot_recurse(dot_graph, g): for h in dot_graph.subgraphs: _graph_from_dot_recurse(h, g) for u in dot_graph.nodes: g[u] = set() for u, v in dot_graph.edges: assert u in g, (u, g) g[u].add(v) return g def test_function_wrapper(): levels = dict(x=0, y=1, z=2) bdd = dd.autoref.BDD(levels) u = bdd.add_expr(r'x /\ y') assert u.bdd is bdd, (repr(u.bdd), repr(bdd)) assert abs(u.node) in bdd._bdd, (u.node, bdd._bdd._succ) # operators x = bdd.add_expr('x') z = bdd.add_expr('z') v = x.implies(z) w = u & ~ v w_ = bdd.add_expr(r'(x /\ y) /\ ~ ((~ x) \/ z)') assert w_ == w, (w_, w) r = ~ (u | v).equiv(w) r_ = bdd.add_expr( r'( (x /\ y) \/ ((~ x) \/ z) ) ^' r'( (x /\ y) /\ ~ ((~ x) \/ z) )') assert r_ == r, (r_, r) p = bdd.add_expr('y') q = p.equiv(x) q_ = bdd.add_expr('x <=> y') assert q_ == q, (q_, q) # to_expr s = q.to_expr() assert s == 'ite(x, y, (~ y))', s # equality p_ = bdd.add_expr('y') assert p_ == p, p_ # decref and collect garbage bdd.collect_garbage() n = len(bdd) assert n > 1, bdd._bdd._ref del p del q, q_ del r, r_ bdd.collect_garbage() m = len(bdd) assert m > 1, bdd._bdd._ref assert m < n, (m, n) del u del v del w, w_ del x del z bdd.collect_garbage() n = len(bdd) assert n == 2, bdd._bdd._ref del p_ bdd.collect_garbage() n = len(bdd) assert n == 1, bdd._bdd._ref # properties bdd = dd.autoref.BDD({'x': 0, 'y': 1, 'z': 2}) u = bdd.add_expr(r'x \/ ~ y') assert u.level == 0, u.level assert u.var == 'x', u.var y = bdd.add_expr('~ y') assert u.low == y, (u.low.node, y.node) assert u.high.node == 1, u.high.node assert u.ref == 1, u.ref def x_or_y(): g = two_vars_xy() u = 4 t = (0, 3, 1) assert_valid_succ_pred(u, t, g) g._succ[u] = t g._pred[t] = u g._ref[u] = 1 g._min_free = u + 1 g.assert_consistent() return g def x_and_y(): g = two_vars_xy() u = 4 t = (0, -1, 3) assert_valid_succ_pred(u, t, g) g._succ[u] = t g._pred[t] = u g._ref[u] = 1 g._min_free = u + 1 return g def two_vars_xy(): ordering = {'x': 0, 'y': 1} g = BDD(ordering) u = 2 t = (0, -1, 1) assert_valid_succ_pred(u, t, g) g._succ[u] = t g._pred[t] = u g._ref[u] = 1 u = 3 t = (1, -1, 1) assert_valid_succ_pred(u, t, g) g._succ[u] = t g._pred[t] = u g._ref[u] = 1 g._min_free = u + 1 return g def x_and_not_y(): # remember: # 2 = ~ (x /\ ~ y) # -2 = x /\ ~ y ordering = {'x': 0, 'y': 1} g = BDD(ordering) u = 3 v = -1 w = 1 t = (1, v, w) assert_valid_succ_pred(u, t, g) g._succ[u] = t g._pred[t] = u g._ref[abs(v)] += 1 g._ref[abs(w)] += 1 g._ref[abs(u)] = 0 u = 2 v = 1 w = 3 t = (0, v, w) assert_valid_succ_pred(u, t, g) g._succ[u] = t g._pred[t] = u g._ref[abs(v)] += 1 g._ref[abs(w)] += 1 g._ref[abs(u)] = 0 g._min_free = 4 return g def assert_valid_succ_pred(u, t, g): assert u > 1, u assert isinstance(t, tuple), t assert len(t) == 3, t assert t[0] >= 0, t assert u not in g._succ, g._succ assert t not in g._pred, g._pred def ref_var(i): h = nx.MultiDiGraph() h.add_node(1, level=2) h.add_node(2, level=i) h.add_edge(2, 1, value=False, complement=True) h.add_edge(2, 1, value=True, complement=False) return h def ref_x_and_y(): h = nx.MultiDiGraph() h.add_node(1, level=2) h.add_node(2, level=0) h.add_node(3, level=1) h.add_edge(2, 1, value=False, complement=True) h.add_edge(2, 3, value=True, complement=False) h.add_edge(3, 1, value=False, complement=True) h.add_edge(3, 1, value=True, complement=False) return h def ref_x_or_y(): h = nx.MultiDiGraph() h.add_node(1, level=2) h.add_node(2, level=0) h.add_node(3, level=1) h.add_edge(2, 3, value=False, complement=False) h.add_edge(2, 1, value=True, complement=False) h.add_edge(3, 1, value=False, complement=True) h.add_edge(3, 1, value=True, complement=False) return h def compare(u, bdd, h): g = _bdd.to_nx(bdd, [u]) post = nx.descendants(g, u) post.add(u) r = g.subgraph(post) gm = iso.MultiDiGraphMatcher( r, h, node_match=_nm, edge_match=_em) assert gm.is_isomorphic() d = gm.mapping assert d[1] == 1 def _nm(x, y): return x['level'] == y['level'] def _em(x, y): return ( bool(x[0]['value']) == bool(y[0]['value']) and bool(x[0]['complement']) == bool(y[0]['complement'])) if __name__ == '__main__': log = logging.getLogger('astutils') log.setLevel(logging.ERROR) log = logging.getLogger('dd.bdd') log.setLevel(logging.INFO) log.addHandler(logging.StreamHandler()) test_dynamic_reordering() ================================================ FILE: tests/common.py ================================================ """Common tests for `autoref`, `cudd`, `cudd_zdd`.""" # This file is released in the public domain. # import os import pytest class Tests: def setup_method(self): self.DD = None # `autoref.BDD` or `cudd.BDD` or # `cudd_zdd.ZDD` def test_true_false(self): bdd = self.DD() true = bdd.true false = bdd.false assert false.low is None assert false.high is None assert false != true assert false == ~ true assert false == false & true assert true == true | false def test_configure_reordering(self): zdd = self.DD() zdd.declare('x', 'y', 'z') u = zdd.add_expr(r'x \/ y') cfg = zdd.configure(reordering=False) cfg = zdd.configure() assert cfg['reordering'] == False cfg = zdd.configure(reordering=True) assert cfg['reordering'] == False cfg = zdd.configure() assert cfg['reordering'] == True def test_succ(self): bdd = self.DD() bdd.declare('x') u = bdd.var('x') level, low, high = bdd.succ(u) assert level == 0, level assert low == bdd.false, low # The next line applies to only BDDs # assert high == bdd.true, high def test_add_var(self): bdd = self.DD() bdd.add_var('x') bdd.add_var('y') assert set(bdd.vars) == {'x', 'y'}, bdd.vars x = bdd.var('x') y = bdd.var('y') assert x != y, (x, y) def test_var_cofactor(self): bdd = self.DD() bdd.add_var('x') x = bdd.var('x') values = dict(x=False) u = bdd.let(values, x) assert u == bdd.false, u values = dict(x=True) u = bdd.let(values, x) assert u == bdd.true, u def test_richcmp(self): bdd = self.DD() assert bdd == bdd other = self.DD() assert bdd != other def test_len(self): bdd = self.DD() u = bdd.true assert len(bdd) == 1, len(bdd) def test_contains(self): bdd = self.DD() true = bdd.true assert true in bdd bdd.add_var('x') x = bdd.var('x') assert x in bdd # undefined `__contains__` other_bdd = self.DD() other_true = other_bdd.true with pytest.raises(ValueError): other_true in bdd def test_var_levels(self): bdd = self.DD() # single variable bdd.declare('x') level = bdd.level_of_var('x') assert level == 0, level var = bdd.var_at_level(0) assert var == 'x', var # two variables bdd.declare('y') x_level = bdd.level_of_var('x') var = bdd.var_at_level(x_level) assert var == 'x', var y_level = bdd.level_of_var('y') var = bdd.var_at_level(y_level) assert var == 'y', var assert x_level != y_level, (x_level, y_level) assert x_level >= 0, x_level assert y_level >= 0, y_level def test_var_levels_attr(self): bdd = self.DD() bdd.declare('x', 'y') var_levels = bdd.var_levels assert len(var_levels) == 2, var_levels assert {'x', 'y'} == set(var_levels), var_levels assert {0, 1} == set(var_levels.values()), var_levels def test_levels(self): bdd = self.DD() bdd.add_var('x') bdd.add_var('y') bdd.add_var('z') ix = bdd.level_of_var('x') iy = bdd.level_of_var('y') iz = bdd.level_of_var('z') # before any reordering, levels are unchanged assert ix == 0, ix assert iy == 1, iy assert iz == 2, iz x = bdd.var_at_level(0) y = bdd.var_at_level(1) z = bdd.var_at_level(2) assert x == 'x', x assert y == 'y', y assert z == 'z', z def test_copy(self): bdd = self.DD() other = self.DD() bdd.declare('x') other.declare('x') u = bdd.add_expr('~ x') v = bdd.copy(u, other) v_ = other.add_expr('~ x') assert v == v_, (v, v_) # copy to same manager w = bdd.copy(u, bdd) assert u == w, (u, w) def test_compose(self): bdd = self.DD() for var in ['x', 'y', 'z']: bdd.add_var(var) u = bdd.add_expr(r'x /\ ~ y') # x |-> y sub = dict(x=bdd.var('y')) v = bdd.let(sub, u) v_ = bdd.false assert v == v_, len(v) # x |-> y, y |-> x sub = dict(x=bdd.var('y'), y=bdd.var('x')) v = bdd.let(sub, u) v_ = bdd.add_expr(r'y /\ ~ x') assert v == v_, v # x |-> z sub = dict(x=bdd.var('z')) v = bdd.let(sub, u) v_ = bdd.add_expr(r'z /\ ~ y') assert v == v_, v # x |-> z, y |-> x sub = dict(x=bdd.var('z'), y=bdd.var('x')) v = bdd.let(sub, u) v_ = bdd.add_expr(r'z /\ ~ x') assert v == v_, v # x |-> (y \/ z) sub = dict(x=bdd.add_expr(r'y \/ z')) v = bdd.let(sub, u) v_ = bdd.add_expr(r'(y \/ z) /\ ~ y') assert v == v_, v # LET x == ~ y IN ~ x u = bdd.add_expr('~ x') v = bdd.add_expr('~ y') let = dict(x=v) w = bdd.let(let, u) w_ = bdd.var('y') assert w == w_, len(w) # LET x == y IN x /\ y u = bdd.add_expr(r'x /\ y') v = bdd.add_expr('y') let = dict(x=v) w = bdd.let(let, u) w_ = bdd.var('y') assert w == w_, len(w) # LET x == ~ y IN x /\ y v = bdd.add_expr('~ y') let = dict(x=v) w = bdd.let(let, u) w_ = bdd.false assert w == w_, len(w) def test_cofactor(self): bdd = self.DD() for var in ['x', 'y']: bdd.add_var(var) x = bdd.var('x') y = bdd.var('y') # x /\ y u = bdd.apply('and', x, y) r = bdd.let(dict(x=False, y=False), u) assert r == bdd.false, r r = bdd.let(dict(x=True, y=False), u) assert r == bdd.false, r r = bdd.let(dict(x=False, y=True), u) assert r == bdd.false, r r = bdd.let(dict(x=True, y=True), u) assert r == bdd.true, r # x=False let = dict(x=False) r = bdd.let(let, u) r_ = bdd.false assert r == r_, len(r) # x=True let = dict(x=True) r = bdd.let(let, u) r_ = bdd.var('y') assert r == r_, len(r) # x /\ ~ y not_y = bdd.apply('not', y) u = bdd.apply('and', x, not_y) r = bdd.let(dict(x=False, y=False), u) assert r == bdd.false, r r = bdd.let(dict(x=True, y=False), u) assert r == bdd.true, r r = bdd.let(dict(x=False, y=True), u) assert r == bdd.false, r r = bdd.let(dict(x=True, y=True), u) assert r == bdd.false, r # y=False let = dict(y=False) r = bdd.let(let, u) r_ = bdd.add_expr('x') assert r == r_, len(r) # y=True let = dict(y=True) r = bdd.let(let, u) r_ = bdd.false assert r == r_, len(r) # ~ x \/ y not_x = bdd.apply('not', x) u = bdd.apply('or', not_x, y) r = bdd.let(dict(x=False, y=False), u) assert r == bdd.true, r r = bdd.let(dict(x=True, y=False), u) assert r == bdd.false, r r = bdd.let(dict(x=False, y=True), u) assert r == bdd.true, r r = bdd.let(dict(x=True, y=True), u) assert r == bdd.true, r def test_count(self): b = self.DD() # x b.declare('x') u = b.add_expr('x') with pytest.raises(ValueError): b.count(u, 0) n = b.count(u, 1) assert n == 1, n n = b.count(u, 2) assert n == 2, n n = b.count(u, 3) assert n == 4, n n = b.count(u) assert n == 1, n # x /\ y b.declare('y') u = b.add_expr(r'x /\ y') with pytest.raises(ValueError): b.count(u, 0) with pytest.raises(ValueError): b.count(u, 1) n = b.count(u, 2) assert n == 1, n n = b.count(u, 3) assert n == 2, n n = b.count(u, 5) assert n == 8, n n = b.count(u) assert n == 1, n # x \/ ~ y u = b.add_expr(r'x \/ ~ y') with pytest.raises(ValueError): b.count(u, 0) with pytest.raises(ValueError): b.count(u, 1) n = b.count(u, 2) assert n == 3, n n = b.count(u, 3) assert n == 6, n n = b.count(u, 4) assert n == 12, n n = b.count(u) assert n == 3, n def test_pick_iter(self): b = self.DD() b.add_var('x') b.add_var('y') # FALSE u = b.false m = list(b.pick_iter(u)) assert not m, m # TRUE, no care vars u = b.true m = list(b.pick_iter(u)) assert m == [{}], m # x u = b.add_expr('x') m = list(b.pick_iter(u)) m_ = [dict(x=True)] assert m == m_, (m, m_) # ~ x /\ y s = r'~ x /\ y' u = b.add_expr(s) g = b.pick_iter(u, care_vars=set()) m = list(g) m_ = [dict(x=False, y=True)] assert m == m_, (m, m_) u = b.add_expr(s) g = b.pick_iter(u) m = list(g) assert m == m_, (m, m_) # x /\ y u = b.add_expr(r'x /\ y') m = list(b.pick_iter(u)) m_ = [dict(x=True, y=True)] assert m == m_, m # x s = '~ y' u = b.add_expr(s) # partial g = b.pick_iter(u) m = list(g) m_ = [dict(y=False)] self.equal_list_contents(m, m_) # partial g = b.pick_iter(u, care_vars=['x', 'y']) m = list(g) m_ = [ dict(x=True, y=False), dict(x=False, y=False)] self.equal_list_contents(m, m_) # care bits x, y b.add_var('z') s = r'x \/ y' u = b.add_expr(s) g = b.pick_iter(u, care_vars=['x', 'y']) m = list(g) m_ = [ dict(x=True, y=False), dict(x=False, y=True), dict(x=True, y=True)] self.equal_list_contents(m, m_) def equal_list_contents(self, x, y): for u in x: assert u in y, (u, x, y) for u in y: assert u in x, (u, x, y) def test_apply(self): bdd = self.DD() for var in ['x', 'y', 'z']: bdd.add_var(var) x = bdd.var('x') y = bdd.var('y') z = bdd.var('z') # (x \/ ~ x) \equiv TRUE not_x = bdd.apply('not', x) true = bdd.apply('or', x, not_x) assert true == bdd.true, true # x /\ ~ x \equiv FALSE false = bdd.apply('and', x, not_x) assert false == bdd.false, false # x /\ y \equiv ~ (~ x \/ ~ y) u = bdd.apply('and', x, y) not_y = bdd.apply('not', y) v = bdd.apply('or', not_x, not_y) v = bdd.apply('not', v) assert u == v, (u, v) # xor u = bdd.apply('xor', x, y) r = bdd.let(dict(x=False, y=False), u) assert r == bdd.false, r r = bdd.let(dict(x=True, y=False), u) assert r == bdd.true, r r = bdd.let(dict(x=False, y=True), u) assert r == bdd.true, r r = bdd.let(dict(x=True, y=True), u) assert r == bdd.false, r # (z \/ ~ y) /\ x = (z /\ x) \/ (~ y /\ x) u = bdd.apply('or', z, not_y) u = bdd.apply('and', u, x) v = bdd.apply('and', z, x) w = bdd.apply('and', not_y, x) v = bdd.apply('or', v, w) assert u == v, (u, v) # symbols u = bdd.apply('and', x, y) v = bdd.apply('&', x, y) assert u == v, (u, v) u = bdd.apply('or', x, y) v = bdd.apply('|', x, y) assert u == v, (u, v) u = bdd.apply('not', x) v = bdd.apply('!', x) assert u == v, (u, v) u = bdd.apply('xor', x, y) v = bdd.apply('^', x, y) assert u == v, (u, v) # ternary u = bdd.apply('ite', x, y, ~ z) u_ = bdd.add_expr(r'(x /\ y) \/ (~ x /\ ~ z)') assert u == u_, (u, u_) def test_quantify(self): bdd = self.DD() for var in ['x', 'y', 'z']: bdd.add_var(var) x = bdd.var('x') # (\E x: x) \equiv TRUE r = bdd.quantify(x, ['x'], forall=False) assert r == bdd.true, r # (\A x: x) \equiv FALSE r = bdd.quantify(x, ['x'], forall=True) assert r == bdd.false, r # (\E y: x) \equiv x r = bdd.quantify(x, ['y'], forall=False) assert r == x, (r, x) # (\A y: x) \equiv x r = bdd.quantify(x, ['y'], forall=True) assert r == x, (r, x) # (\E x: x /\ y) \equiv y y = bdd.var('y') u = bdd.apply('and', x, y) r = bdd.quantify(u, ['x'], forall=False) assert r == y, (r, y) assert r != x, (r, x) # (\A x: x /\ y) \equiv FALSE r = bdd.quantify(u, ['x'], forall=True) assert r == bdd.false, r # (\A x: ~ x \/ y) \equiv y not_x = bdd.apply('not', x) u = bdd.apply('or', not_x, y) r = bdd.quantify(u, ['x'], forall=True) assert r == y, (r, y) # \E x: ((x /\ ~ y) \/ ~ z) u = bdd.add_expr(r'(x /\ ~ y) \/ ~ z') qvars = ['x'] r = bdd.exist(qvars, u) r_ = bdd.add_expr(r'\E x: ((x /\ ~ y) \/ ~ z)') assert r == r_, (r, r_) r_ = bdd.add_expr(r'(~ y) \/ ~ z') assert r == r_, (r, r_) # \E y: x /\ ~ y /\ ~ z u = bdd.add_expr(r'x /\ ~ y /\ ~ z') qvars = ['y'] r = bdd.exist(qvars, u) r_ = bdd.add_expr(r'x /\ ~ z') assert r == r_, len(r) def test_exist_forall(self): bdd = self.DD() for var in ['x', 'y']: bdd.add_var(var) x = bdd.var('x') # \E x: x = 1 r = bdd.exist(['x'], x) assert r == bdd.true, r # \A x: x = 0 r = bdd.forall(['x'], x) assert r == bdd.false, r # \E y: x = x r = bdd.exist(['y'], x) assert r == x, (r, x) # \A y: x = x r = bdd.forall(['y'], x) assert r == x, (r, x) # (\E x: x /\ y) \equiv y y = bdd.var('y') u = bdd.apply('and', x, y) r = bdd.exist(['x'], u) assert r == y, (r, y) assert r != x, (r, x) # (\A x: x /\ y) \equiv FALSE r = bdd.forall(['x'], u) assert r == bdd.false, r # (\A x: ~ x \/ y) \equiv y not_x = bdd.apply('not', x) u = bdd.apply('or', not_x, y) r = bdd.forall(['x'], u) assert r == y, (r, y) def test_cube(self): bdd = self.DD() for var in ['x', 'y', 'z']: bdd.add_var(var) # x x = bdd.var('x') c = bdd.cube(['x']) assert x == c, (x, c) # x /\ y y = bdd.var('y') u = bdd.apply('and', x, y) c = bdd.cube(['x', 'y']) assert u == c, (u, c) # x /\ ~ y not_y = bdd.apply('not', y) u = bdd.apply('and', x, not_y) d = dict(x=True, y=False) c = bdd.cube(d) assert u == c, (u, c) def test_add_expr(self): bdd = self.DD() for var in ['x', 'y']: bdd.add_var(var) # ((FALSE \/ TRUE) /\ x) \equiv x s = r'(True \/ FALSE) /\ x' u = bdd.add_expr(s) x = bdd.var('x') assert u == x, (u, x) # ((x \/ ~ y) /\ x) \equiv x s = r'(x \/ ~ y) /\ x' u = bdd.add_expr(s) assert u == x, (u, x) # x /\ y /\ z bdd.add_var('z') z = bdd.var('z') u = bdd.add_expr(r'x /\ y /\ z') u_ = bdd.cube(dict(x=True, y=True, z=True)) assert u == u_, (u, u_) # x /\ ~ y /\ z u = bdd.add_expr(r'x /\ ~ y /\ z') u_ = bdd.cube(dict(x=True, y=False, z=True)) assert u == u_, (u, u_) # (\E x: x /\ y) \equiv y y = bdd.var('y') u = bdd.add_expr(r'\E x: x /\ y') assert u == y, (str(u), str(y)) # (\A x: x \/ ~ x) \equiv TRUE u = bdd.add_expr(r'\A x: ~ x \/ x') assert u == bdd.true, u def test_to_expr(self): bdd = self.DD() bdd.declare('x', 'y') u = bdd.var('x') r = bdd.to_expr(u) r_ = 'x' assert r == r_, (r, r_) u = bdd.add_expr(r'x /\ y') r = bdd.to_expr(u) r_ = 'ite(x, y, FALSE)' assert r == r_, (r, r_) u = bdd.add_expr(r'x \/ y') r = bdd.to_expr(u) r_ = 'ite(x, TRUE, y)' assert r == r_, (r, r_) def test_support(self): zdd = self.DD() # declared at the start, for ZDDs to work zdd.declare('x', 'y', 'z') # FALSE u = zdd.false s = zdd.support(u) assert s == set(), s # TRUE u = zdd.true s = zdd.support(u) assert s == set(), s # x u = zdd.add_expr('x') s = zdd.support(u) assert s == {'x'}, s # ~ x u = zdd.add_expr('x') s = zdd.support(u) assert s == {'x'}, s # ~ y u = zdd.add_expr('~ y') s = zdd.support(u) assert s == {'y'}, s # x /\ y u = zdd.add_expr(r'x /\ y') s = zdd.support(u) assert s == {'x', 'y'}, s # x \/ y u = zdd.add_expr(r'x \/ y') s = zdd.support(u) assert s == {'x', 'y'}, s # x /\ ~ y u = zdd.add_expr(r'x /\ ~ y') s = zdd.support(u) assert s == {'x', 'y'}, s def test_rename(self): bdd = self.DD() bdd.declare('x', 'y', 'z', 'w') # LET x == y IN x x = bdd.var('x') supp = bdd.support(x) assert supp == set(['x']), supp d = dict(x='y') f = bdd.let(d, x) supp = bdd.support(f) assert supp == set(['y']), supp y = bdd.var('y') assert f == y, (f, y) # x, y -> z, w not_y = bdd.apply('not', y) u = bdd.apply('or', x, not_y) supp = bdd.support(u) assert supp == set(['x', 'y']), supp d = dict(x='z', y='w') f = bdd.let(d, u) supp = bdd.support(f) assert supp == set(['z', 'w']), supp z = bdd.var('z') w = bdd.var('w') not_w = bdd.apply('not', w) f_ = bdd.apply('or', z, not_w) assert f == f_, (f, f_) # ensure substitution, not swapping # # f == LET x == y # IN x /\ y u = bdd.apply('and', x, y) # replace x with y, but leave y as is d = dict(x='y') f = bdd.let(d, u) # THEOREM f <=> y assert f == y, (f, y) # f == LET x == y # IN x /\ ~ y u = bdd.apply('and', x, ~ y) d = dict(x='y') f = bdd.let(d, u) # THEOREM f <=> FALSE assert f == bdd.false, f # simultaneous substitution # # f == LET x == y1 (* x1, y1 correspond to *) # y == x1 (* x, y after substitution *) # IN x /\ ~ y u = bdd.apply('and', x, ~ y) # replace x with y, and simultaneously, y with x d = dict(x='y', y='x') f = bdd.let(d, u) f_ = bdd.apply('and', y, ~ x) # THEOREM f <=> (~ x /\ y) assert f == f_, (f, f_) del x, y, not_y, z, w, not_w, u, f, f_ # as method x = bdd.var('x') y_ = bdd.var('y') d = dict(x='y') y = bdd.let(d, x) assert y == y_, (y, y_) del x, y, y_ def test_ite(self): b = self.DD() for var in ['x', 'y', 'z']: b.add_var(var) x = b.var('x') u = b.ite(x, b.true, b.false) assert u == x, (u, x) u = b.ite(x, b.false, b.true) assert u == ~ x, (u, x) y = b.var('y') u = b.ite(x, y, b.false) u_ = b.add_expr(r'x /\ y') assert u == u_, (u, u_) def test_reorder_with_args(self): bdd = self.DD() dvars = ['x', 'y', 'z'] for var in dvars: bdd.add_var(var) self._confirm_var_order(dvars, bdd) order = dict(y=0, z=1, x=2) bdd.reorder(order) for var in order: level_ = order[var] level = bdd.level_of_var(var) assert level == level_, (var, level, level_) def test_reorder_without_args(self): bdd = self.DD() # Figs. 6.24, 6.25 Baier 2008 vrs = ['z1', 'z2', 'z3', 'y1', 'y2', 'y3'] bdd.declare(*vrs) self._confirm_var_order(vrs, bdd) expr = r'(z1 /\ y1) \/ (z2 /\ y2) \/ (z3 /\ y3)' u = bdd.add_expr(expr) n_before = u.dag_size bdd.reorder() n_after = u.dag_size assert n_after < n_before, (n_after, n_before) # optimal: n_after == 6 # # assert that each pair zi, yi is of # variables at adjacent levels # levels = {var: bdd.level_of_var(var) for var in vrs} # for i in range(1, 4): # a = levels[f'z{i}'] # b = levels[f'y{i}'] # assert abs(a - b) == 1, levels def _confirm_var_order(self, vrs, bdd): for i, var in enumerate(vrs): level = bdd.level_of_var(var) assert level == i, (var, level, i) def test_reorder_contains(self): bdd = self.DD() bdd.declare('x', 'y', 'z') u = bdd.add_expr(r'(x /\ y) \/ z') bdd.reorder() assert u in bdd def test_comparators(self): bdd = self.DD() # `None` assert not (bdd.false == None) assert not (bdd.true == None) assert bdd.false != None assert bdd.true != None # constant assert bdd.false < bdd.true assert bdd.false <= bdd.true assert bdd.false != bdd.true assert bdd.true >= bdd.false assert bdd.true > bdd.false assert bdd.true == bdd.true assert bdd.false == bdd.false # non-constant bdd.declare('x', 'y') u = bdd.add_expr('x') # compared to false assert u > bdd.false assert u >= bdd.false assert u != bdd.false assert bdd.false <= u assert bdd.false < u assert u == u # compared to true assert u < bdd.true assert u <= bdd.true assert u != bdd.true assert bdd.true >= u assert bdd.true > u # x /\ y x = bdd.var('x') y = bdd.var('y') assert (x & y) == ~ (~ x | ~ y) assert (x & y) != ~ (~ x | y) def test_function_support(self): bdd = self.DD() bdd.add_var('x') u = bdd.var('x') r = u.support assert r == {'x'}, r bdd.add_var('y') u = bdd.add_expr(r'y /\ x') r = u.support assert r == {'x', 'y'}, r def test_node_hash(self): bdd = self.DD() bdd.declare('z') u = bdd.add_expr('z') n = hash(u) m = hash(bdd.true) assert n != m, (n, m) def test_add_int(self): bdd = self.DD() bdd.declare('x', 'y') u = bdd.add_expr(r'x \/ ~ y') node_id = int(u) u_ = bdd._add_int(node_id) assert u == u_, (u, u_) id2 = int(u_) assert node_id == id2, (node_id, id2) # test string form node_str = str(u) s = f'@{node_id}' assert node_str == s, (node_str, s) def test_dump_using_graphviz( self): bdd = self.DD() bdd.declare('x', 'y') u = bdd.add_expr(r'x /\ y') for ext in ('dot', 'pdf', 'png', 'svg', 'ext'): filename = f'bdd.{ext}' if os.path.isfile(filename): os.remove(filename) # dot bdd.dump('bdd.dot', [u]) assert os.path.isfile('bdd.dot') os.remove('bdd.dot') bdd.dump( 'bdd.dot', [u], filetype='dot') assert os.path.isfile('bdd.dot') # pdf bdd.dump('bdd.pdf', [u]) assert os.path.isfile('bdd.pdf') os.remove('bdd.pdf') bdd.dump( 'bdd.pdf', [u], filetype='pdf') assert os.path.isfile('bdd.pdf') # no ext if os.path.isfile('bdd'): os.remove('bdd') bdd.dump( 'bdd', [u], filetype='pdf') assert os.path.isfile('bdd') # png bdd.dump( 'bdd.png', [u]) assert os.path.isfile('bdd.png') os.remove('bdd.png') bdd.dump( 'bdd.png', [u], filetype='png') assert os.path.isfile('bdd.png') # svg bdd.dump( 'bdd.svg', [u]) assert os.path.isfile('bdd.svg') os.remove('bdd.svg') bdd.dump( 'bdd.svg', [u], filetype='svg') assert os.path.isfile('bdd.svg') # ext bdd.dump( 'bdd.ext', [u], filetype='pdf') assert os.path.isfile('bdd.ext') with pytest.raises(ValueError): bdd.dump( 'bdd.ext', [u]) ================================================ FILE: tests/common_bdd.py ================================================ """Common tests for `autoref`, `cudd`.""" # This file is released in the public domain. # import os import pytest class Tests: def setup_method(self): self.DD = None # `autoref.BDD` or `cudd.BDD` def test_succ(self): bdd = self.DD() bdd.declare('x') u = bdd.var('x') level, low, high = bdd.succ(u) assert level == 0, level assert low == bdd.false, low assert high == bdd.true, high def test_find_or_add(self): b = self.DD() for var in ['x', 'y', 'z']: b.add_var(var) u = b.find_or_add('x', b.false, b.true) u_ = b.var('x') assert u == u_, b.to_expr(u) u = b.find_or_add('y', b.false, b.true) u_ = b.var('y') assert u == u_, b.to_expr(u) v = b.var('y') w = b.var('z') u = b.find_or_add('x', v, w) u_ = b.add_expr(r'(~ x /\ y) \/ (x /\ z)') assert b.apply('<=>', u, u_) == b.true assert u == u_, (b.to_expr(u), b.to_expr(u_)) def test_function(self): bdd = self.DD() bdd.add_var('x') # x x = bdd.var('x') # assert not x.negated low = x.low assert low == bdd.false, low high = x.high assert high == bdd.true, high assert x.var == 'x', x.var # ~ x not_x = ~x # assert not_x.negated low = not_x.low assert low == bdd.false, low high = not_x.high assert high == bdd.true, high assert not_x.var == 'x', not_x.var # constant nodes false = bdd.false assert false.var is None, false.var true = bdd.true assert true.var is None, true.var not_x_ = bdd.add_expr('~ x') assert not_x == not_x_, ( not_x, not_x_) # y bdd.add_var('y') y = bdd.var('y') # x & y x_and_y = x & y negated = x_and_y.negated assert not negated, negated var = x_and_y.var assert var == 'x', var low = x_and_y.low assert low == bdd.false, low.var y_ = x_and_y.high assert y == y_, y_.var x_and_y_ = bdd.add_expr(r'x /\ y') assert x_and_y == x_and_y_, ( x_and_y, x_and_y_) # x | y x_or_y = x | y negated = x_or_y.negated assert not negated, negated var = x_or_y.var assert var == 'x', var low = x_or_y.low assert low == y, low.var high = x_or_y.high assert high == bdd.true, high.var x_or_y_ = bdd.add_expr(r'x \/ y') assert x_or_y == x_or_y_, ( x_or_y, x_or_y_) # x ^ y x_xor_y = x ^ y negated = x_xor_y.negated assert negated, negated var = x_xor_y.var assert var == 'x', var high = x_xor_y.high assert high == y, high.var neg_y = x_xor_y.low negated = neg_y.negated assert negated, negated var = neg_y.var assert var == 'y', var low = neg_y.low assert low == bdd.false, low.var high = neg_y.high assert high == bdd.true, high.var x_xor_y_ = bdd.add_expr('x ^ y') assert x_xor_y == x_xor_y_, ( x_xor_y, x_xor_y_) def test_function_properties(self): bdd = self.DD() bdd.declare('x', 'y') order = dict(x=0, y=1) bdd.reorder(order) u = bdd.add_expr(r'x \/ y') y = bdd.add_expr('y') # Assigned first because in presence of a bug # different property calls could yield # different values. level = u.level assert level == 0, level var = u.var assert var == 'x', var low = u.low assert low == y, low high = u.high assert high == bdd.true, high ref = u.ref assert ref == 1, ref assert not u.negated support = u.support assert support == {'x', 'y'}, support # terminal u = bdd.false assert u.var is None, u.var assert u.low is None, u.low assert u.high is None, u.high def test_negated(self): bdd = self.DD() bdd.declare('x') u = bdd.add_expr('x') neg_u = bdd.add_expr('~ x') a = u.negated b = neg_u.negated assert a or b, (a, b) assert not (a and b), (a, b) def test_dump_pdf(self): bdd = self.DD() bdd.declare('x', 'y', 'z') u = bdd.add_expr(r'x /\ y') v = bdd.add_expr(r'y /\ ~ z') fname = 'bdd.pdf' roots = [u, v] self.rm_file(fname) bdd.dump(fname, roots) assert os.path.isfile(fname) def test_dump_load_json(self): bdd = self.DD() bdd.declare('x', 'y', 'z') u = bdd.add_expr(r'(z /\ x /\ y) \/ x \/ ~ y') fname = 'foo.json' bdd.dump(fname, [u]) u_, = bdd.load(fname) assert u == u_, len(u_) # test `ValueError` with pytest.raises(ValueError): bdd.dump(fname, None) with pytest.raises(ValueError): bdd.dump(fname, list()) with pytest.raises(ValueError): bdd.dump(fname, dict()) def rm_file(self, fname): if os.path.isfile(fname): os.remove(fname) ================================================ FILE: tests/common_cudd.py ================================================ """Common tests for `cudd`, `cudd_zdd`.""" # This file is released in the public domain. # import pytest class Tests: def setup_method(self): self.DD = None # `cudd.BDD` or `cudd_zdd.ZDD` self.MODULE = None # `cudd` or `cudd_zdd` def test_add_var(self): bdd = self.DD() bdd.add_var('x') bdd.add_var('y') jx = bdd._index_of_var['x'] jy = bdd._index_of_var['y'] assert jx == 0, jx assert jy == 1, jy x = bdd._var_with_index[0] y = bdd._var_with_index[1] assert x == 'x', x assert y == 'y', y assert bdd.vars == {'x', 'y'}, bdd.vars x = bdd.var('x') y = bdd.var('y') assert x != y, (x, y) def test_len(self): bdd = self.DD() assert len(bdd) == 0, len(bdd) u = bdd.true assert len(bdd) == 1, len(bdd) del u assert len(bdd) == 0, len(bdd) def test_levels(self): bdd = self.DD() bdd.add_var('x', index=0) bdd.add_var('y', index=2) bdd.add_var('z', index=10) ix = bdd.level_of_var('x') iy = bdd.level_of_var('y') iz = bdd.level_of_var('z') # before any reordering, levels match var indices assert ix == 0, ix assert iy == 2, iy assert iz == 10, iz x = bdd.var_at_level(0) y = bdd.var_at_level(2) z = bdd.var_at_level(10) assert x == 'x', x assert y == 'y', y assert z == 'z', z def test_var_at_level_exceptions(self): bdd = self.DD() # no variables with pytest.raises(ValueError): bdd.var_at_level(-1) with pytest.raises(ValueError): bdd.var_at_level(0) with pytest.raises(ValueError): bdd.var_at_level(1) with pytest.raises(ValueError): # no var at level CUDD_CONST_INDEX bdd.var_at_level(bdd.false.level) with pytest.raises(OverflowError): bdd.var_at_level(bdd.false.level + 1) # 1 declared variable bdd.declare('x') level = bdd.level_of_var('x') assert level == 0, level var = bdd.var_at_level(0) assert var == 'x', var with pytest.raises(ValueError): bdd.var_at_level(-1) with pytest.raises(ValueError): bdd.var_at_level(1) with pytest.raises(ValueError): # no var at level CUDD_CONST_INDEX bdd.var_at_level(bdd.false.level) with pytest.raises(OverflowError): bdd.var_at_level(bdd.false.level + 1) bdd._var_with_index = dict() with pytest.raises(ValueError): bdd.var_at_level(0) def test_incref_decref_locally_inconsistent(self): # "Locally inconsistent" here means that # from the viewpoint of some `Function` instance, # the calls to `incref` and `decref` would result # in an incorrect reference count. # # In this example overall the calls to `incref` # and `decref` result in an incorrect reference count. bdd = self.DD() bdd.declare('x', 'y') u = bdd.add_expr(r'x /\ y') # ref cnt = 1 v = bdd.add_expr(r'x /\ y') # ref cnt = 2 bdd.incref(u) # ref cnt = 3 bdd.decref(v) # ref cnt = 2 del u, v # ref cnt = 1 # this assertion implies that `DD.__dealloc__` # would raise an exception (that would be # ignored) assert len(bdd) > 0, len(bdd) u = bdd.add_expr(r'x /\ y') # ref cnt = 2 u._ref = 2 bdd.decref(u) # ref cnt = 1 def test_decref_incref_locally_inconsistent(self): # "Locally inconsistent" here means the # same as described in the previous method. # # The difference with the previous method # is that here `decref` is called before # `incref`, not after. # # In this example overall the calls to `incref` # and `decref` result in an incorrect reference count. bdd = self.DD() bdd.declare('x', 'y') u = bdd.add_expr(r'x /\ y') # ref cnt = 1 v = bdd.add_expr(r'x /\ y') # ref cnt = 2 bdd.decref(v) # ref cnt = 1 bdd.incref(u) # ref cnt = 2 del u, v # ref cnt = 1 # this assertion implies that `DD.__dealloc__` # would raise an exception (that would be # ignored) assert len(bdd) > 0, len(bdd) u = bdd.add_expr(r'x /\ y') # ref cnt = 2 u._ref = 2 bdd.decref(u) # ref cnt = 1 def test_double_incref_decref_locally_inconsistent(self): # "Locally inconsistent" here means the # same as described in a method above. # # The main difference with the previous method # is that here `decref` is called twice. # # Overall, the calls to `incref` and `decref` # would have resulted in a correct reference count # with an earlier implementation. # # In any case, this pattern of calls to # `incref` and `decref` now raises # a `RuntimeError`. bdd = self.DD() bdd.declare('x', 'y') u = bdd.add_expr(r'x /\ y') # ref cnt = 1 v = bdd.add_expr(r'x /\ y') # ref cnt = 2 bdd.incref(u) # ref cnt = 3 bdd.incref(u) # ref cnt = 4 bdd.decref(v) # ref cnt = 3 with pytest.raises(RuntimeError): bdd.decref(v) del u, v # ref cnt = 2 assert len(bdd) > 0, len(bdd) u = bdd.add_expr(r'x /\ y') # ref cnt = 3 u._ref = 3 bdd.decref(u) # ref cnt = 2 bdd.decref(u) # ref cnt = 1 def test_decref_and_dealloc(self): bdd = self.DD() bdd.declare('x', 'y') u = bdd.add_expr(r'x /\ ~ y') assert u.ref == 1, u.ref s = int(u) bdd.decref(u, recursive=True) del u # calls method `Function.__dealloc__` # the execution of `decref` and then # `__dealloc__` should result in # reference count 0, # not a negative value v = bdd._add_int(s) assert v.ref == 1, v.ref bdd.decref(v) del v # the following check passes when # `u.ref = 0 - 1`, # because the reference count is an # unsigned integer, so subtracting 1 # results in a saturated positive value, # which is ignored by the function # `Cudd_CheckZeroRef` (which checks # `node->ref != 0 && node->ref != DD_MAXREF`) assert len(bdd) == 0, len(bdd) def test_decref(self): bdd = self.DD() # Turn off garbage collection to prevent the # memory for the CUDD BDD/ZDD node below from # being deallocated when the reference count # of the node reaches 0. # If that happened, then further access to # the attribute `u.ref` would have been unsafe. bdd.configure( reordering=False, garbage_collection=False) bdd.declare('x', 'y') u = bdd.add_expr(r'x /\ ~ y') assert u.ref == 1, u.ref assert u._ref == 1, u._ref bdd.decref(u, recursive=True) # CAUTION: `u.node is NULL` hereafter assert u._ref == 0, u._ref # Ensure that `decref` decrements # only positive reference counts. # No need for `recursive=True`, # because this call should have # no effect at all. with pytest.raises(RuntimeError): bdd.decref(u) assert u._ref == 0, u._ref assert len(bdd) == 0, len(bdd) def test_decref_ref_lower_bound(self): bdd = self.DD() bdd.declare('x', 'y') u = bdd.add_expr(r'x /\ ~ y') assert u._ref == 1, u._ref assert u.ref == 1, u.ref # `recursive=True` is necessary here # because after `u._ref` becomes `0`, # we cannot any more dereference # the successor nodes of `u.node`. # # The reason is that memory for the # BDD/ZDD node pointed to by `u.node` # may be deallocated after its # reference count becomes 0. bdd.decref(u, recursive=True) # `u` should not be used after # the reference count of the BDD/ZDD node # pointed to by `u` becomes 0. # This avoids accessing deallocated memory. assert u._ref == 0, u._ref # Ensure that the method `decref` decrements # only positive reference counts. # `recursive=True` is irrelevant here, # because this call should have # no effect at all. # # Again, `u` is not used in any way # that could access deallocated memory. with pytest.raises(RuntimeError): bdd.decref(u) assert u._ref == 0, u._ref # check also with `recursive=True` with pytest.raises(RuntimeError): bdd.decref(u, recursive=True) # check also `incref` with pytest.raises(RuntimeError): bdd.incref(u) assert len(bdd) == 0, len(bdd) def test_dealloc_wrong_ref_lower_bound(self): bdd = self.DD() bdd.declare('x', 'y') u = bdd.add_expr(r'x /\ ~ y') # make an erroneous external modification assert u.ref == 1, u.ref u._ref = -1 # erroneous value with pytest.raises(AssertionError): self.MODULE._test_call_dealloc(u) assert u.ref == 1, u.ref assert u._ref == -1, u._ref u._ref = 1 # restore del u assert len(bdd) == 0, len(bdd) def test_dealloc_multiple_calls(self): bdd = self.DD() bdd.declare('x', 'y') u = bdd.add_expr(r'x /\ ~ y') assert u.ref == 1, u.ref assert u._ref == 1, u._ref self.MODULE._test_call_dealloc(u) self.MODULE._test_call_dealloc(u) assert u._ref == 0, u._ref assert len(bdd) == 0, len(bdd) ================================================ FILE: tests/copy_test.py ================================================ """Tests of the module `dd._copy`.""" # This file is released in the public domain. # import dd.autoref as _autoref import dd.cudd as _cudd import dd._copy as _copy def test_involution(): _test_involution(_autoref) _test_involution(_cudd) def _test_involution(mod): bdd_1, bdd_2 = _setup(mod) u = bdd_1.add_expr(r'x /\ ~ y') v = _copy.copy_bdd(u, bdd_2) u_ = _copy.copy_bdd(v, bdd_1) assert u == u_, (u, u_) def test_bdd_mapping(): _test_bdd_mapping(_autoref) _test_bdd_mapping(_cudd) def _test_bdd_mapping(mod): bdd_1, bdd_2 = _setup(mod) u = bdd_1.add_expr(r'x /\ ~ y') cache = dict() u_ = _copy.copy_bdd(u, bdd_2, cache) d = {bdd_1._add_int(k): v for k, v in cache.items()} _check_bdd_mapping(d, bdd_1, bdd_2) def _check_bdd_mapping(umap, old_bdd, new_bdd): """Raise `AssertionError` if `umap` is inconsistent. Asserts that each variable is declared in both managers, and at the same level. """ # add terminal to map umap[old_bdd.true] = new_bdd.true for u, v in umap.items(): assert u in old_bdd, u assert v in new_bdd, v assert u.var == v.var assert u.level == v.level assert u.negated == v.negated # terminal ? if u.var is None: continue # non-terminal low = _map_node(u.low, umap) high = _map_node(u.high, umap) assert low == v.low assert high == v.high def _map_node(u, umap): """Map node, accounting for complement.""" z = _copy._flip(u, u) r = umap[z] return _copy._flip(r, u) def _setup(mod): bdd_1 = mod.BDD() bdd_2 = mod.BDD() bdd_1.declare('x', 'y') bdd_2.declare('x', 'y') return bdd_1, bdd_2 def test_dump_load_same_order(): _test_dump_load_same_order(_autoref) _test_dump_load_same_order(_cudd) def _test_dump_load_same_order(mod): b = mod.BDD() b.declare('x', 'y', 'z') expr = r'x /\ ~ y' u = b.add_expr(expr) # dump fname = 'hoho.json' nodes = [u] _copy.dump_json(nodes, fname) # load target = mod.BDD() roots = _copy.load_json( fname, target, load_order=True) # assert v, = roots v_ = target.add_expr(expr) assert v == v_, (v, v_) # copy to source BDD manager u_ = target.copy(v, b) assert u == u_, (u, u_) def test_dump_load_different_order(): _test_dump_load_different_order(_autoref) _test_dump_load_different_order(_cudd) def _test_dump_load_different_order(mod): source = mod.BDD() source.declare('x', 'y') expr = ' x <=> y ' u = source.add_expr(expr) # dump fname = 'hoho.json' nodes = [u] _copy.dump_json(nodes, fname) # load target = mod.BDD() target.declare('y', 'x') roots = _copy.load_json( fname, target, load_order=False) # assert v, = roots v_ = target.add_expr(expr) assert v == v_, (v, v_) ================================================ FILE: tests/cudd_test.py ================================================ """Tests of the module `dd.cudd`.""" # This file is released in the public domain. # import logging import dd.cudd as _cudd import pytest import common import common_bdd import common_cudd logging.getLogger('astutils').setLevel('ERROR') class Tests(common.Tests): def setup_method(self): self.DD = _cudd.BDD class BDDTests(common_bdd.Tests): def setup_method(self): self.DD = _cudd.BDD class CuddTests(common_cudd.Tests): def setup_method(self): self.DD = _cudd.BDD self.MODULE = _cudd def test_str(): bdd = _cudd.BDD() with pytest.warns(UserWarning): s = str(bdd) s + 'must be a string' def test_insert_var(): bdd = _cudd.BDD() level = 0 j = bdd.insert_var('x', level) assert j == 0, j # initially indices = levels x = bdd.var_at_level(level) assert x == 'x', x level = 101 bdd.insert_var('y', level) y = bdd.var_at_level(level) assert y == 'y', y def test_refs(): _cudd._test_incref() _cudd._test_decref() def test_len(): bdd = _cudd.BDD() assert len(bdd) == 0, len(bdd) u = bdd.true assert len(bdd) == 1, len(bdd) del u assert len(bdd) == 0, len(bdd) u = bdd.true v = bdd.false assert len(bdd) == 1, len(bdd) bdd.add_var('x') x = bdd.var('x') assert len(bdd) == 2, len(bdd) not_x = ~x # len(bdd) is the number of referenced nodes # a node is used both for the positive and # negative literals of its variable assert len(bdd) == 2, len(bdd) del x assert len(bdd) == 2, len(bdd) del not_x assert len(bdd) == 1, len(bdd) del u, v assert len(bdd) == 0, len(bdd) def test_cube_array(): _cudd._test_dict_to_cube_array() _cudd._test_cube_array_to_dict() def test_dump_load_dddmp(): bdd = _cudd.BDD() for var in ['x', 'y', 'z', 'w']: bdd.add_var(var) u = bdd.add_expr(r'(x /\ ~ w) \/ z') fname = 'bdd.dddmp' bdd.dump(fname, [u], filetype='dddmp') u_, = bdd.load(fname) assert u == u_ def test_load_sample0(): bdd = _cudd.BDD() names = ['a', 'b', 'c'] for var in names: bdd.add_var(var) fname = 'sample0.dddmp' u, = bdd.load(fname) n = len(u) assert n == 5, n s = r'~ ( (a /\ (b \/ c)) \/ (~ a /\ (b \/ ~ c)) )' u_ = bdd.add_expr(s) assert u == u_, (u, u_) def test_and_exists(): bdd = _cudd.BDD() for var in ['x', 'y']: bdd.add_var(var) # (\E x: x /\ y) \equiv y x = bdd.add_expr('x') y = bdd.add_expr('y') qvars = ['x'] r = _cudd.and_exists(x, y, qvars) assert r == y, (r, y) # (\E x: x /\ ~ x) \equiv FALSE not_x = bdd.apply('not', x) r = _cudd.and_exists(x, not_x, qvars) assert r == bdd.false def test_or_forall(): bdd = _cudd.BDD() for var in ['x', 'y']: bdd.add_var(var) # (\A x, y: x \/ ~ y) \equiv FALSE x = bdd.var('x') not_y = bdd.add_expr('~ y') qvars = ['x', 'y'] r = _cudd.or_forall(x, not_y, qvars) assert r == bdd.false, r def test_swap(): bdd = _cudd.BDD() bdd.declare('x', 'y') x = bdd.var('x') y = bdd.var('y') # swap x and y # # This swap returns the same result as `bdd.let` # with the same arguments, because `d` contains # both `'x'` and `'y'` as keys. # # This result is obtained when the arguments are # not checked for overlapping key-value pairs. u = bdd.apply('and', x, ~ y) d = dict(x='y', y='x') with pytest.raises(ValueError): f = bdd._swap(u, d) # f_ = bdd.apply('and', ~ x, y) # assert f == f_, (f, f_) # # swap x and y # ensure swapping, not simultaneous substitution # # This swap returns a different result than # `bdd.let` when given the same arguments, # because `d` contains only `'x'` as key, # so `let` does not result in simultaneous # substitution. u = bdd.apply('and', x, ~ y) d = dict(x='y') # swap x with y f = bdd._swap(u, d) f_ = bdd.apply('and', y, ~ x) # compare to the corresponding test of `bdd.let` assert f == f_, (f, f_) # # each variable should in at most one # key-value pair of `d` # # 1) keys and values are disjoint sets bdd.declare('z') z = bdd.var('z') u = bdd.apply('and', x, ~ y) d = dict(x='y', y='z') with pytest.raises(ValueError): f = bdd._swap(u, d) # The following result is obtained if the # assertions are removed from `BDD._swap`. # f_ = bdd.apply('and', y, ~ z) # assert f == f_, bdd.to_expr(f) # # 2) each value appears once among values u = bdd.apply('and', x, ~ y) d = dict(x='y', z='y') with pytest.raises(ValueError): f = bdd._swap(u, d) # The following result is obtained if the # assertions are removed from `BDD._swap`. # f_ = bdd.apply('and', y, ~ z) # assert f == f_, bdd.to_expr(f) def test_copy_bdd_same_indices(): # each va has same index in each `BDD` bdd = _cudd.BDD() other = _cudd.BDD() assert bdd != other dvars = ['x', 'y', 'z'] for var in dvars: bdd.add_var(var) other.add_var(var) s = r'(x /\ y) \/ ~ z' u0 = bdd.add_expr(s) u1 = _cudd.copy_bdd(u0, other) u2 = _cudd.copy_bdd(u1, bdd) # involution assert u0 == u2, (u0, u2) # confirm w = other.add_expr(s) assert w == u1, (w, u1) # different nodes u3 = _cudd.copy_bdd(other.true, bdd) assert u3 != u2, (u3, u2) def test_copy_bdd_different_indices(): # each var has different index in each `BDD` bdd = _cudd.BDD() other = _cudd.BDD() assert bdd != other dvars = ['x', 'y', 'z'] for var in dvars: bdd.add_var(var) for var in reversed(dvars): other.add_var(var) s = r'(x \/ ~ y) /\ ~ z' u0 = bdd.add_expr(s) u1 = _cudd.copy_bdd(u0, other) u2 = _cudd.copy_bdd(u1, bdd) # involution assert u0 == u2, (u0, u2) # confirm w = other.add_expr(s) assert w == u1, (w, u1) # different nodes u3 = _cudd.copy_bdd(other.true, bdd) assert u3 != u2, (u3, u2) def test_copy_bdd_different_order(): bdd = _cudd.BDD() other = _cudd.BDD() assert bdd != other dvars = ['x', 'y', 'z', 'w'] for index, var in enumerate(dvars): bdd.add_var(var, index=index) other.add_var(var, index=index) # reorder order = dict(w=0, x=1, y=2, z=3) other.reorder(order) # confirm resultant order for var in order: level_ = order[var] level = other.level_of_var(var) assert level == level_, (var, level, level_) # same indices for var in dvars: i = bdd._index_of_var[var] j = other._index_of_var[var] assert i == j, (i, j) # but different levels for var in dvars: i = bdd.level_of_var(var) j = other.level_of_var(var) assert i != j, (i, j) # copy s = r'(x \/ ~ y) /\ w /\ (z \/ ~ w)' u0 = bdd.add_expr(s) u1 = _cudd.copy_bdd(u0, other) u2 = _cudd.copy_bdd(u1, bdd) assert u0 == u2, (u0, u2) u3 = _cudd.copy_bdd(other.false, bdd) assert u3 != u2, (u3, u2) # verify w = other.add_expr(s) assert w == u1, (w, u1) def test_count_nodes(): bdd = _cudd.BDD() [bdd.add_var(var) for var in ['x', 'y', 'z']] u = bdd.add_expr(r'x /\ y') v = bdd.add_expr(r'x /\ z') assert len(u) == 3, len(u) assert len(v) == 3, len(v) bdd.reorder(dict(x=0, y=1, z=2)) n = _cudd.count_nodes([u, v]) assert n == 5, n bdd.reorder(dict(z=0, y=1, x=2)) n = _cudd.count_nodes([u, v]) assert n == 4, n def test_function(): bdd = _cudd.BDD() bdd.add_var('x') # x x = bdd.var('x') assert not x.negated # ~ x not_x = ~x assert not_x.negated if __name__ == '__main__': test_function() ================================================ FILE: tests/cudd_zdd_test.py ================================================ """Tests of the module `dd.cudd_zdd`.""" # This file is released in the public domain. # import inspect import os import subprocess import sys import dd.cudd as _cudd import dd.cudd_zdd as _cudd_zdd import dd._copy as _copy import pytest import common import common_cudd class Tests(common.Tests): def setup_method(self): self.DD = _cudd_zdd.ZDD class CuddTests(common_cudd.Tests): def setup_method(self): self.DD = _cudd_zdd.ZDD self.MODULE = _cudd_zdd def test_str(): bdd = _cudd_zdd.ZDD() with pytest.warns(UserWarning): s = str(bdd) s + 'must be a string' def test_false(): zdd = _cudd_zdd.ZDD() u = zdd.false assert len(u) == 0, len(u) def test_true(): zdd = _cudd_zdd.ZDD() zdd.declare('x', 'y', 'z', 'w') u = zdd.true assert u.low is not None assert u.high is not None assert len(u) == 4, len(u) def test_true_node(): zdd = _cudd_zdd.ZDD() zdd.declare('x', 'y') u = zdd.true_node assert u.low is None assert u.high is None assert len(u) == 0, len(u) def test_index_at_level(): zdd = _cudd_zdd.ZDD() zdd.add_var('x', 1) level = zdd.level_of_var('x') assert level == 1, ( level, zdd.index_of_var, zdd.vars) level_to_index = { -20: None, -1: None, 0: 0, 1: 1, 2: None, 3: None, 100: None} for level, index_ in level_to_index.items(): index = zdd._index_at_level(level) assert index == index_, ( level, index, index_, zdd.index_of_var, zdd.vars) # no `dd.cudd_zdd.ZDD` variable declared at level 0 # CUDD indices range from 0 to 1 with pytest.raises(ValueError): zdd.level_of_var(0) # no CUDD variable at level 2 with pytest.raises(ValueError): zdd.level_of_var(2) def test_var_level_gaps(): zdd = _cudd_zdd.ZDD() zdd.add_var('x', 2) n_vars = len(zdd.vars) assert n_vars == 1, n_vars max_var_level = _max_var_level(zdd) assert max_var_level == 2, max_var_level def _max_var_level(zdd): """Return the maximum level in `zdd`. The indices of variables in CUDD can span more levels than the variables declared in `zdd`. This happens when declaring variables with noncontiguous levels, using `ZDD.add_var()`. Nonetheless, `ZDD.add_var()` ensures that there exists a variable in `ZDD.vars` whose level equals the maximum level over CUDD indices. """ if not zdd.vars: return None return max( zdd.level_of_var(var) for var in zdd.vars) def test_gt_var_levels(): zdd = _cudd_zdd.ZDD() zdd.add_var('x', 1) level_to_value = { 0: False, 1: False, 2: True, 3: True, 100: True} for level, value_ in level_to_value.items(): value = zdd._gt_var_levels(level) assert value == value_, ( level, value, value_, zdd.index_of_var, zdd.vars) with pytest.raises(ValueError): zdd._gt_var_levels(-1) def test_number_of_cudd_vars_without_gaps(): zdd = _cudd_zdd.ZDD() # no variables _assert_n_vars_max_level(0, 0, None, zdd) # 1 declared variable # 1 variable index in CUDD zdd.declare('x') _assert_n_vars_max_level(1, 1, 0, zdd) # 2 declared variables # 2 variable indices in CUDD zdd.declare('y') _assert_n_vars_max_level(2, 2, 1, zdd) def test_number_of_cudd_vars_with_gaps(): zdd = _cudd_zdd.ZDD() # no variables _assert_n_vars_max_level(0, 0, None, zdd) # 1 declared variable # 2 variable indices in CUDD zdd.add_var('x', 1) _assert_n_vars_max_level(2, 1, 1, zdd) # 2 declared variables # 15 variable indices in CUDD zdd.add_var('y', 14) _assert_n_vars_max_level(15, 2, 14, zdd) def _assert_n_vars_max_level( n_cudd_vars: int, n_zdd_vars: int, max_var_level: int, zdd): n_cudd_vars_ = zdd._number_of_cudd_vars() assert n_cudd_vars_ == n_cudd_vars, ( n_cudd_vars_, n_cudd_vars) n_zdd_vars_ = len(zdd.vars) assert n_zdd_vars_ == n_zdd_vars, ( zdd.vars, n_zdd_vars) max_var_level_ = _max_var_level(zdd) assert max_var_level_ == max_var_level, ( max_var_level_, max_var_level) def test_var(): zdd = _cudd_zdd.ZDD() zdd.declare('x', 'y', 'z') x = zdd.var('x') x_ = zdd._var_cudd('x') assert x == x_, len(x) y = zdd.var('y') y_ = zdd._var_cudd('y') assert y == y_, len(y) z = zdd.var('z') z_ = zdd._var_cudd('z') assert z == z_, len(z) def test_support_cudd(): # support implemented by CUDD zdd = _cudd_zdd.ZDD() zdd.declare('x', 'y') zdd._add_bdd_var(0) zdd._add_bdd_var(1) u = zdd.add_expr('~ x') s = zdd._support_cudd(u) assert s == {'y'}, s # `{'x'}` is expected def test_cudd_cofactor(): zdd = _cudd_zdd.ZDD() zdd.declare('x', 'y') u = zdd.add_expr(r'x /\ ~ y') r = zdd._cofactor_cudd(u, 'y', False) r_ = zdd.add_expr(r'x /\ ~ y') assert r == r_, len(r) u = zdd.add_expr(r'x /\ y') r = zdd._cofactor_cudd(u, 'x', True) r_ = zdd.add_expr(r'~ x /\ y') # no node at x assert r == r_ def test_find_or_add(): bdd = _cudd_zdd.ZDD() bdd.declare('x', 'y', 'z') v = bdd.add_expr(r'~ x /\ y /\ ~ z') w = bdd.add_expr(r'~ x /\ ~ y /\ z') u = bdd.find_or_add('x', v, w) assert u.low == v, len(u) assert u.high == w, len(u) assert u.var == 'x', u.var assert u.level == 0, u.level def test_count(): zdd = _cudd_zdd.ZDD() zdd.declare('x', 'y') # FALSE u = zdd.false n = zdd.count(u, 2) assert n == 0, n # TRUE u = zdd.true n = zdd.count(u, 1) assert n == 2, n n = zdd.count(u, 2) assert n == 4, n def test_bdd_to_zdd_copy(): zdd = _cudd_zdd.ZDD() zdd.declare('x', 'y', 'z') bdd = _cudd.BDD() bdd.declare('x', 'y', 'z') u = bdd.add_expr('x') v = bdd.copy(u, zdd) x = zdd.var('x') assert v == x, len(v) print_size(v, 'v') # copy `y` u = bdd.var('y') y = bdd.copy(u, zdd) y_ = zdd.var('y') assert y == y_, (y, y_) def test_len(): zdd = _cudd_zdd.ZDD() zdd.declare('x', 'y', 'z') # x x = zdd.var('x') assert len(x) == 3, len(x) # y y = zdd.var('y') assert len(y) == 3, len(y) # x /\ y /\ ~ z u = x & y & ~ zdd.var('z') assert len(u) == 2, len(u) # ~ x u = zdd.add_expr('~ x') assert len(u) == 2, len(u) def test_ith_var_without_gaps(): zdd = _cudd_zdd.ZDD() zdd.declare('x', 'y', 'z') u = _cudd_zdd._ith_var('x', zdd) # check ZDD for variable x assert u.var == 'x', u.var assert u.level == 0, u.level assert u.low == zdd.false, ( u, u.low, zdd.false) v = u.high assert v.var == 'y', v.var assert v.level == 1, v.level assert v.low == v.high, ( v, v.low, v.high) w = v.low assert w.var == 'z' assert w.level == 2, w.level assert w.low == w.high, ( w, w.low, w.high) assert w.low == zdd.true_node, ( w, w.low, zdd.true_node) # check ZDD for variable y u = _cudd_zdd._ith_var('y', zdd) assert u.var == 'x', u.var assert u.level == 0, u.level assert u.low == u.high, ( u, u.low, u.high) v = u.low assert v.var == 'y', v.var assert v.level == 1, v.level assert v.low == zdd.false, ( v, v.low, zdd.false) w = v.high assert w.var == 'z', w.var assert w.level == 2, w.level assert w.low == w.high, ( w, w.low, w.high) assert w.low == zdd.true_node, ( w, w.low, zdd.true_node) # check ZDD for variable z u = _cudd_zdd._ith_var('z', zdd) assert u.var == 'x', u.var assert u.level == 0, u.level assert u.low == u.high, ( u, u.low, u.high) v = u.low assert v.var == 'y', v.var assert v.level == 1, v.level assert v.low == v.high, ( v, v.low, v.high) w = v.low assert w.var == 'z', w.var assert w.level == 2, w.level assert w.low == zdd.false, ( w, w.low, zdd.false) assert w.high == zdd.true_node, ( w, w.high, zdd.true_node) def test_ith_var_with_gaps(): zdd = _cudd_zdd.ZDD() zdd.add_var('x', 1) with pytest.raises(AssertionError): # because 1 declared variable, # but 2 CUDD variable indices _cudd_zdd._ith_var('x', zdd) zdd.vars.update(dict(y=0, z=3)) with pytest.raises(AssertionError): _cudd_zdd._ith_var('x', zdd) def test_disjunction(): zdd = _cudd_zdd.ZDD() zdd.declare('w', 'x', 'y') # x \/ TRUE v = zdd.add_expr('x') w = zdd.true u = zdd._disjoin_root(v, w) assert u == w, len(u) # x \/ FALSE w = zdd.false u = zdd._disjoin_root(v, w) assert u == v, len(u) # x \/ y v = zdd.add_expr('x') w = zdd.add_expr('y') u = zdd._disjoin_root(v, w) u_ = zdd.add_expr(r'x \/ y') assert u == u_, len(u) # (~ w /\ x) \/ y v = zdd.add_expr(r'~ w /\ x') w = zdd.add_expr('y') u = zdd._disjoin_root(v, w) u_ = zdd.add_expr(r'(~ w /\ x) \/ y') assert u == u_, len(u) def test_conjunction(): zdd = _cudd_zdd.ZDD() zdd.declare('x', 'y', 'z') v = zdd.var('x') w = zdd.var('y') u = zdd._conjoin_root(v, w) u_ = zdd.add_expr(r'x /\ y') assert u == u_, len(u) u = zdd._conjoin_root(v, ~ w) u_ = zdd.add_expr(r'x /\ ~ y') assert u == u_, len(u) def test_methods_disjoin_conjoin_gaps_opt(): run_python_with_optimization( test_methods_disjoin_conjoin_gaps) def test_methods_disjoin_conjoin_gaps(): import dd.cudd_zdd as _zdd import pytest zdd = _zdd.ZDD() zdd.add_var('x', 20) u = zdd.find_or_add( 'x', zdd.false, zdd.true_node) level = 1 with pytest.raises(ValueError): _zdd._call_method_disjoin( zdd, level, u, ~ u, cache=dict()) with pytest.raises(ValueError): _zdd._call_method_conjoin( zdd, level, ~ u, u, cache=dict()) def test_method_disjoin(): zdd = _cudd_zdd.ZDD() zdd.declare('x') v = zdd.var('x') level = 1 with pytest.raises(ValueError): _cudd_zdd._call_method_disjoin( zdd, level, v, v, dict()) def test_methods_disjoin_conjoin_with_opt(): run_python_with_optimization( test_methods_disjoin_conjoin) def test_methods_disjoin_conjoin(): import dd.cudd_zdd as _zdd import pytest zdd = _zdd.ZDD() zdd.declare('x') v = zdd.var('x') true = zdd.true_node level = 1 with pytest.raises(ValueError): _zdd._call_method_disjoin( zdd, level, v, true, dict()) with pytest.raises(ValueError): _zdd._call_method_conjoin( zdd, level, v, true, dict()) with pytest.raises(ValueError): _zdd._call_method_conjoin( zdd, level, v, ~ v, dict()) def run_python_with_optimization( function): """Run `function` with `python -O`. Start new `python` process because Python's optimization level cannot be changed at runtime. """ name = function.__name__ function_src = inspect.getsource(function) assertion_src = inspect.getsource(_assert) src = f'{function_src}\n{assertion_src}\n{name}()' assert sys.executable, sys.executable cmd = [ sys.executable, '-O', '-c', src] proc = subprocess.run( cmd, capture_output=True, text=True) if proc.returncode == 0: return raise AssertionError( f'The function `{name}`, when run with ' f'`{cmd[:-1]}`, resulted in exiting with ' f'return code {proc.returncode}.\n' f'The `stdout` was:\n{proc.stdout}\n' f'The `stderr` was:\n{proc.stderr}') def _assert(test): if test: return raise AssertionError(test) def test_c_disjunction(): zdd = _cudd_zdd.ZDD() zdd.declare('w', 'x', 'y') v = zdd.add_expr(r'~ w /\ x') w = zdd.add_expr('y') u = _cudd_zdd._c_disjoin(v, w) u_ = zdd.add_expr(r'(~ w /\ x) \/ y') assert u == u_, len(u) def test_c_conjunction(): zdd = _cudd_zdd.ZDD() zdd.declare('x', 'y', 'z') x = zdd.var('x') y = zdd.var('y') u = _cudd_zdd._c_conjoin(x, y) u_ = zdd.add_expr(r'x /\ y') assert u == u_, len(u) def test_c_disjoin_conjoin(): zdd = _cudd_zdd.ZDD() zdd.declare('x') u = zdd.var('x') true = zdd.true_node level = 1 with pytest.raises(AssertionError): _cudd_zdd._call_disjoin(level, u, true) with pytest.raises(AssertionError): _cudd_zdd._call_conjoin(level, u, true) with pytest.raises(AssertionError): _cudd_zdd._call_conjoin(level, true, u) def test_c_disjoin_conjoin_leaf_check(): zdd = _cudd_zdd.ZDD() leaf_level = zdd.false.level u = zdd.true_node r = _cudd_zdd._call_disjoin( leaf_level, u, u) assert r == u, (r.level, u.level) zdd.declare('x') v = zdd.var('x') with pytest.raises(AssertionError): _cudd_zdd._call_disjoin( leaf_level, v, v) r = _cudd_zdd._call_conjoin( leaf_level, u, u) assert r == u, (r.level, u.level) with pytest.raises(AssertionError): _cudd_zdd._call_conjoin( leaf_level, v, v) def test_c_exist(): zdd = _cudd_zdd.ZDD() zdd.declare('x', 'y', 'z') # \E x: (x /\ ~ y) \/ ~ z u = zdd.add_expr(r'(x /\ ~ y) \/ ~ z') qvars = ['x'] r = _cudd_zdd._c_exist(qvars, u) r_ = zdd.exist(qvars, u) assert r == r_, len(r) # \E x: x u = zdd.add_expr('x') qvars = ['x'] r = _cudd_zdd._c_exist(qvars, u) r_ = zdd.exist(qvars, u) assert r == r_, len(r) def test_dump(): zdd = _cudd_zdd.ZDD() zdd.declare('x', 'y', 'w') u = zdd.add_expr('~ w') fname = 'not_w.pdf' if os.path.isfile(fname): os.remove(fname) assert not os.path.isfile(fname) zdd.dump(fname, [u]) assert os.path.isfile(fname) def test_dict_to_zdd(): zdd = _cudd_zdd.ZDD() zdd.declare('x', 'y', 'z') qvars = {'x', 'z'} u = _cudd_zdd._dict_to_zdd(qvars, zdd) assert len(u) == 2, len(u) assert u.var == 'x', u.var assert u.low == u.high v = u.low assert v.var == 'z', v.var assert v.low == v.high assert v.low == zdd.true_node def print_size(u, msg): n = len(u) print(f'Dag size of {msg}: {n}') if __name__ == '__main__': Tests().test_support() # test_compose() ================================================ FILE: tests/dddmp_test.py ================================================ """Tests of the module `dd.dddmp`.""" import logging import os import dd.dddmp as _dddmp import pytest logger = logging.getLogger( 'dd.dddmp.parser_logger') logger.setLevel(logging.ERROR) def test_lexer(): lexer = _dddmp.Lexer() s = '.ghs?5' lexer.lexer.input(s) tok = lexer.lexer.token() assert tok.value == '.ghs' with pytest.raises(Exception): lexer.lexer.token() def test_parser(): parser = _dddmp.Parser() with pytest.raises(Exception): parser.parser.parse( input='.mode C', lexer=parser.lexer.lexer) def test_sample0(): fname = 'sample0.dddmp' parser = _dddmp.Parser() bdd, n_vars, ordering, roots = parser.parse(fname) assert set(bdd) == set(range(1, 6)), sorted(bdd) bdd_ = { 1: (None, None), 2: (-1, 1), # -1 is a complemented edge 3: (2, 1), 4: (-2, 1), # -2 is a complemented edge 5: (4, 3)} for u, (level, v, w) in bdd.items(): v_, w_ = bdd_[u] assert v == v_, (u, v, w, v_, w_) assert w == w_, (u, v, w, v_, w_) # other attributes assert n_vars == 50, n_vars assert ordering == { 'a': 1, 'b': 2, 'c': 3}, ordering assert roots == {-5} # debugging # h.dump('bdd.pdf', roots, filetype='pdf') def test_sample1(): fname = 'sample1.dddmp' parser = _dddmp.Parser() parser.build(debug=True) bdd, n_vars, ordering, roots = parser.parse(fname) assert len(bdd) == 16, len(bdd) assert n_vars == 10, n_vars assert roots == {6, -13, -16}, roots def test_sample2(): # x /\ y # where x, y have indices 0, 1 fname = 'sample2.dddmp' bdd = _dddmp.load(fname) n = len(bdd) assert n == 3, n n_vars = len(bdd.vars) assert n_vars == 2, n_vars assert bdd.roots == {3}, bdd.roots root = 3 i, v, w = bdd.succ(root) assert i == 0, i assert v == -1, v i, v, w = bdd.succ(w) assert i == 1, i assert v == -1, v assert w == 1, w # overwrite indices with strings bdd.vars = dict(x=0, y=1) u = bdd.add_expr(r'x /\ y') assert u == root, u def test_sample3(): # x /\ y # where x, y are at levels 1, 0 # nodes are labeled with var names fname = 'sample3.dddmp' bdd = _dddmp.load(fname) n = len(bdd) assert n == 3, n n_vars = len(bdd.vars) assert n_vars == 2, n_vars assert bdd.roots == {3}, bdd.roots root = 3 u = bdd.add_expr(r'x /\ y') assert root == u, u def test_load_dddmp(): # small sample fname = 'sample0.dddmp' bdd = _dddmp.load(fname) n = len(bdd) n_vars = len(bdd.vars) assert n == 5, n assert n_vars == 3, n_vars assert bdd.roots == {-5}, bdd.roots root = -5 u = bdd.add_expr( r'~ ( (a /\ (b \/ c)) \/ ' r'(~ a /\ (b \/ ~ c)) )') assert u == root, (u, root) # larger sample fname = 'sample1.dddmp' bdd = _dddmp.load(fname) n = len(bdd) n_vars = len(bdd.vars) assert n == 16, n assert n_vars == 10, n_vars assert bdd.roots == {6, -13, -16} varnames = { 'G0', 'G1', 'G2', 'G3', 'G5', 'G6', 'G7', 'TMP1', 'TMP2', 'TMP3'} bddvars = set(bdd.vars) assert bddvars == varnames, bddvars bdd.assert_consistent() def test_rewrite_tables(): prefix = '_dddmp_parser_state_machine' for ext in ('.py', '.pyc'): fname = f'{prefix}{ext}' if os.path.isfile(fname): os.remove(fname) _dddmp._rewrite_tables() assert os.path.isfile(f'{prefix}.py') def to_nx(bdd, n_vars, ordering, roots): """Convert parsing result to `networkx` graph. Convert result of the method `dd.dddmp.Parser.parse()` to an instance of the class `networkx.MultiDiGraph`. The arguments `bdd`, `n_vars`, `ordering`, `roots` are those values that are returned from the method `dd.dddmp.Parser.parse`. """ import networkx as nx level2var = { ordering[k]: k for k in ordering} level2var[n_vars + 1] = 'T' h = nx.MultiDiGraph() h.roots = roots for u in bdd: i, v, w = bdd[u] assert u >= 0, u label = level2var[i] h.add_node(u, label=label) # terminal ? if v is None or w is None: assert v is None assert w is None continue complemented = '-1' if v < 0 else ' ' h.add_edge( u, abs(v), label=complemented, style='dashed') assert w >= 0, w # "then" edge cannot be complemented h.add_edge(u, w) return h def test_dump_with_cudd_load_with_dddmp(): import dd.cudd fname = 'foo.dddmp' # dump bdd = dd.cudd.BDD() bdd.declare('y', 'x') u = bdd.add_expr(r'x /\ y') bdd.dump(fname, [u]) # load bdd = _dddmp.load(fname) print(bdd.roots) u, = bdd.roots u_ = bdd.add_expr(r'x /\ y') assert u == u_, (u, u_) expr = bdd.to_expr(u) print(expr) if __name__ == '__main__': test_load_dddmp() ================================================ FILE: tests/inspect_cython_signatures.py ================================================ """Compare the signatures of methods in a Cython `cdef` class to ABC. A `cdef` class cannot inherit from an ABC (or a Python class that serves the same purpose). `inspect.signature` works when the compiler directive `binding` is enabled, but returns keyword arguments as if they are positional. This script reports any mismatch of argument names (ignoring which ones are keyword arguments) between methods of the classes: - `dd._abc.BDD` (the specification) - `dd.cudd.BDD` (the implementation) Methods present in `_abc.BDD` but absent from `cudd.BDD` are reported too. Use the script to ensure the API is implemented, also for other Cython modules, for example `dd.sylvan`. MEMO: Remember to enable `binding` when using this script. """ # Copyright 2017 by California Institute of Technology # All rights reserved. Licensed under BSD-3. # import inspect import logging import warnings import dd._abc as _abc import dd.cudd as _cudd log = logging.getLogger(__name__) def inspect_signatures(spec, imp): """Print mismatches of method names or argument names. @param spec: the specification @param imp: the implementation """ print(f'Specification class: {type(spec)}') print(f'Implementation class: {type(imp)}') print('Checking whether all spec methods are implemented:\n') spec_dir = dir(spec) imp_dir = dir(imp) for method_name in spec_dir: if is_hidden(method_name): continue method = getattr(spec, method_name) if not callable(method): continue log.info(f'"{method_name}" is callable') if method_name not in imp_dir: print(f'MISSING implementation for "{method_name}"\n') continue assert method_name in spec_dir, method_name assert method_name in imp_dir, method_name spec_method = getattr(spec, method_name) imp_method = getattr(imp, method_name) spec_sig = get_signature(spec_method) imp_sig = get_signature(imp_method) if spec_sig is None or imp_sig is None: continue spec_args = spec_sig.parameters.keys() imp_args = imp_sig.parameters.keys() if spec_args != imp_args: print( f'MISMATCH: method "{method_name}"\n' f' spec args: {spec_args}\n' f' imp args: {imp_args}\n') print('\nExtra methods:\n') for method_name in imp_dir: if is_hidden(method_name): continue method = getattr(imp, method_name) if not callable(method): continue if method_name not in spec_dir: print(method_name) def is_hidden(method_name): """Return `True` if not an interface method.""" return method_name.startswith('_') def get_signature(func): """Wrapper of `inspect.signature` with Cython reminder.""" try: sig = inspect.signature(func) except ValueError: warnings.warn( 'Compile `dd.cudd` with the compiler directive `binding`' f' for the function "{func}"') sig = None return sig def _main(): """Check that `dd.cudd.BDD` implements `dd._abc`.""" # BDD manager a = _abc.BDD() b = _cudd.BDD() inspect_signatures(a, b) # BDD nodes print(30 * '-' + '\n') u = _abc.Operator # cannot instantiate `dd.cudd.Function` # without a `DdNode` pointer b.declare('x') v = b.add_expr('x') inspect_signatures(u, v) if __name__ == '__main__': _main() ================================================ FILE: tests/iterative_recursive_flattener.py ================================================ """Mapping trees to BDDs by iteration, and by recursion. The iterative traversal avoids exceeding: - CPython's call-stack bound, and - the underlying C call-stack bound. """ # This file is released in the public domain. # import typing as _ty _QUANTIFIERS: _ty.Final = { r'\A', r'\E'} _LEAFS: _ty.Final = { 'bool', 'num', 'var'} _BOOLEANS: _ty.Final = { 'false', 'true'} def _recurse_syntax_tree( tree, bdd): r"""Add abstract syntax `tree` to `self`. ```tla ASSUME /\ hasattr(tree_node, 'operator') /\ hasattr(tree_node, 'operands') /\ \/ ~ is_leaf(tree_node) \/ /\ hasattr(tree_node, value) /\ \/ tree_node.value \in {"FALSE", "TRUE"} (* for leaf nodes that represent Boolean constants *) \/ tree_node.value \in STRING \ { "FALSE", "TRUE"} (* for leaf nodes that represent identifiers of BDD variables, or numeric literals *) ``` """ match tree.type: case 'operator': if (tree.operator in _QUANTIFIERS and len(tree.operands) == 2): qvars, expr = tree.operands qvars = {x.value for x in qvars} forall = (tree.operator == r'\A') u = _recurse_syntax_tree(expr, bdd) return bdd.quantify( u, qvars, forall=forall) elif tree.operator == r'\S': expr, rename = tree.operands rename = { k.value: v.value for k, v in rename} u = _recurse_syntax_tree(expr, bdd) return bdd.rename(u, rename) else: operands = [ _recurse_syntax_tree(x, bdd) for x in tree.operands] return bdd.apply( tree.operator, *operands) case 'bool': value = tree.value.lower() if value not in _BOOLEANS: raise ValueError(tree.value) return getattr(bdd, value) case 'var': return bdd.var(tree.value) case 'num': i = int(tree.value) return bdd._add_int(i) raise ValueError( f'unknown node type: {tree.type = }') def _reduce_syntax_tree( tree, bdd): """Convert syntax tree to decision diagram. This function is implemented iteratively in Python, in order to avoid recursion limits of Python's implementation. """ stack = [ list(), [tree]] while len(stack) > 1: _reduce_step(stack, bdd) if len(stack) == 1 and len(stack[0]): res, = stack[0] return res raise AssertionError(stack) def _reduce_step( stack: list, bdd ) -> None: """Step in iteration of tree reduction.""" tree, *operands = stack[-1] match tree.type: case 'operator': if tree.operator in _QUANTIFIERS: _reduce_quantifier( tree, operands, stack, bdd) elif tree.operator == r'\S': _reduce_substitution( tree, operands, stack, bdd) else: _reduce_operator( tree, operands, stack, bdd) case 'bool': stack.pop() value = tree.value.lower() if value not in _BOOLEANS: raise ValueError(tree.value) u = getattr(bdd, value) stack[-1].append(u) case 'var': stack.pop() value = bdd.var(tree.value) stack[-1].append(value) case 'num': stack.pop() number = int(tree.value) value = bdd._add_int(number) stack[-1].append(value) case _: raise ValueError( f'unknown node type: {tree.type}') def _reduce_quantifier( tree, operands, stack: list, bdd ) -> None: """Reduce quantifier tree.""" if not operands: _, successor = tree.operands stack.append([successor]) return u, = operands qvars, _ = tree.operands qvars = { name.value for name in qvars} forall = (tree.operator == r'\A') res = bdd.quantify( u, qvars, forall=forall) stack.pop() stack[-1].append(res) def _reduce_substitution( tree, operands, stack: list, bdd ) -> None: """Reduce `LET`.""" if not operands: successor, _ = tree.operands stack.append([successor]) return u, = operands _, renaming = tree.operands renaming = { k.value: v.value for k, v in renaming} res = bdd.rename(u, renaming) stack.pop() stack[-1].append(res) def _reduce_operator( tree, operands, stack: list, bdd ) -> None: """Reduce operator application.""" n_operands = len(operands) n_successors = len(tree.operands) if 0 < n_operands == n_successors: res = bdd.apply( tree.operator, *operands) stack.pop() stack[-1].append(res) elif 0 <= n_operands < n_successors: successor = tree.operands[n_operands] stack.append([successor]) else: raise AssertionError( tree, operands) ================================================ FILE: tests/mdd_test.py ================================================ """Tests of the module `dd.mdd`.""" import logging import os import dd.bdd import dd.mdd logger = logging.getLogger(__name__) def test_ite(): dvars = dict( x=dict(level=0, len=2), y=dict(level=1, len=2)) mdd = dd.mdd.MDD(dvars) u = mdd.find_or_add(0, -1, 1) v = mdd.find_or_add(1, -1, 1) g = mdd.find_or_add(0, -1, 1) r_ = mdd.find_or_add(0, v, 1) r = mdd.ite(g, u, v) assert r == r_, (r, r_) def test_find_or_add(): dvars = dict(x=dict(level=0, len=4), y=dict(level=1, len=2)) m = dd.mdd.MDD(dvars) u = m.find_or_add(0, 1, -1, 1, 1) # m.dump('hehe.pdf') print(m.to_expr(u)) def test_bdd_to_mdd(): ordering = {'x': 0, 'y': 1} bdd = dd.bdd.BDD(ordering) u = bdd.add_expr(r'x /\ ~ y') bdd.incref(u) # BDD -> MDD dvars = dict( x=dict(level=1, len=2, bitnames=['x']), y=dict(level=0, len=2, bitnames=['y'])) mdd, umap = dd.mdd.bdd_to_mdd(bdd, dvars) v = umap[abs(u)] if u < 0: v = -v print(v) bdd.decref(u) def test_mdd_dump_to_pdf(): dvars = dict( x=dict(level=0, len=2), y=dict(level=1, len=2)) mdd = dd.mdd.MDD(dvars) v = mdd.find_or_add(1, -1, 1) u = mdd.find_or_add(0, -1, -v) # x /\ ~ y assert u < 0, u filename = 'mdd.pdf' if os.path.isfile(filename): os.remove(filename) mdd.dump('mdd.pdf') assert os.path.isfile(filename) if __name__ == '__main__': test_bdd_to_mdd() ================================================ FILE: tests/parser_test.py ================================================ """Tests of module `dd._parser`.""" # This file is released in the public domain. # import collections.abc as _abc import itertools as _itr import logging import math import sys import typing as _ty import dd.autoref as _bdd import dd._parser import pytest import iterative_recursive_flattener as _flattener _log = logging.getLogger(__name__) def _make_parser_test_expressions( ) -> _abc.Iterable[str]: """Yield test formulas.""" expressions = [ '~ FALSE', '~ TRUE', '! FALSE', '! TRUE', '@15', '@-24', r'FALSE /\ TRUE', r'TRUE /\ FALSE', r'FALSE \/ TRUE', r'TRUE \/ FALSE', r'TRUE \/ TRUE \/ FALSE', 'TRUE # FALSE', 'TRUE && TRUE', 'FALSE || TRUE', 'TRUE & FALSE', 'FALSE | TRUE', 'TRUE ^ FALSE', r'\E x, y, z: TRUE ^ x => y', r'\A y: y \/ x', r'(TRUE) => (FALSE /\ TRUE)', 'TRUE <=> TRUE', '~ (FALSE <=> FALSE)', 'ite(FALSE, FALSE, TRUE)', " var_name' => x' ", ] def rm_blanks(expr): return expr.replace('\x20', '') return _itr.chain( expressions, map(rm_blanks, expressions)) BDD_TRANSLATION_TEST_EXPRESSIONS: _ty.Final = [ '~ a', r'a /\ b', r'a \/ b', 'a => b', 'a <=> b', 'a # b', 'a ^ b', r'\E a: a => b', r'\A a: \E b: a \/ ~ b', r'! a /\ ~ b', 'a || b', 'a | b | c', 'a && b && c', 'a & b', 'b -> a', 'c -> a -> b', 'b <-> a', r'(a \/ b) & c', ] PARSER_TEST_EXPRESSIONS: _ty.Final = list( _make_parser_test_expressions()) def test_all_parsers_same_results(): parser = dd._parser.Parser() bdd = _bdd.BDD() bdd.declare('a', 'b', 'c') for expr in BDD_TRANSLATION_TEST_EXPRESSIONS: u1 = dd._parser.add_expr(expr, bdd) # translator that directly # creates BDDs from expression strings tree = parser.parse(expr) # parser creates a syntax tree u2 = _flattener._recurse_syntax_tree(tree, bdd) # recursive translation of # syntax tree to BDD u3 = _flattener._reduce_syntax_tree(tree, bdd) # iterative translation of # syntax tree to BDD assert u1 == u2, ( u1, u2, bdd.to_expr(u1), bdd.to_expr(u2)) assert u2 == u3, (u2, u3) def test_translator_vs_recursion_limit(): bdd = _bdd.BDD() bdd.declare('a', 'b', 'c') parser = dd._parser.Parser() # expression < recursion limit expr = r'a /\ b \/ c' dd._parser.add_expr(expr, bdd) # expression > recursion limit expr = make_expr_gt_recursion_limit() dd._parser.add_expr(expr, bdd) def test_log_syntax_tree_to_bdd(): bdd = _bdd.BDD() bdd.declare('a') expr = syntax_tree_of_shape('log') # parse to syntax tree parser = dd._parser.Parser() tree = parser.parse(expr) # flatten to BDD u1 = dd._parser.add_expr(expr, bdd) u2 = _flattener._recurse_syntax_tree(tree, bdd) u3 = _flattener._reduce_syntax_tree(tree, bdd) assert u1 == u2, (u1, u2) assert u2 == u3, (u2, u3) def test_linear_syntax_tree_to_bdd(): bdd = _bdd.BDD() bdd.declare('a') expr = syntax_tree_of_shape('linear') # parse to syntax tree parser = dd._parser.Parser() tree = parser.parse(expr) # flatten to BDD u1 = dd._parser.add_expr(expr, bdd) u2 = _flattener._reduce_syntax_tree(tree, bdd) assert u1 == u2, (u1, u2) with pytest.raises(RecursionError): _flattener._recurse_syntax_tree(tree, bdd) def make_expr_gt_recursion_limit( ) -> str: """Return expression with many operators. The returned expression contains more operator applications than Python's current recursion limit. """ recursion_limit = sys.getrecursionlimit() n_operators = 2 * recursion_limit tail = n_operators * r' /\ a ' return f' a{tail} ' def syntax_tree_of_shape( shape: _ty.Literal[ 'log', 'linear'] ) -> str: """Return expression.""" match shape: case 'log': delimit = True case 'linear': delimit = False case _: raise ValueError(shape) recursion_limit = sys.getrecursionlimit() log2 = math.log2(recursion_limit) depth = round(2 + log2) expr = 'a' for _ in range(depth): expr = _delimit( rf'{expr} /\ {expr}', '(', ')', delimit) return expr def _delimit( expr: str, start: str, end: str, delimit: bool ) -> str: """Return `expr` delimited.""" if not delimit: return expr return f'{start} {expr} {end}' def test_lexing(): lexer = dd._parser.Lexer() expr = 'variable' tokens = tokenize(expr, lexer) assert len(tokens) == 1, tokens token, = tokens assert token.type == 'NAME', token.type assert token.value == 'variable', token.value expr = " primed' " tokens = tokenize(expr, lexer) assert len(tokens) == 1, tokens token, = tokens assert token.type == 'NAME', token.type assert token.value == "primed'", token.value expr = '~ a' tokens = tokenize(expr, lexer) assert len(tokens) == 2, tokens tilde, name = tokens assert tilde.type == 'NOT', tilde.type assert tilde.value == '!', tilde.value assert name.type == 'NAME', name.type assert name.value == 'a', name.value expr = '! a' tokens = tokenize(expr, lexer) assert len(tokens) == 2, tokens not_, name = tokens assert not_.type == 'NOT', not_.type assert not_.value == '!', not_.value assert name.type == 'NAME', name.type assert name.value == 'a', name.value infixal = [ (r'a /\ b', 'AND', '&'), ('a && b', 'AND', '&'), ('a & b', 'AND', '&'), (r'a \/ b', 'OR', '|'), ('a || b', 'OR', '|'), ('a | b', 'OR', '|'), ('a => b', 'IMPLIES', '=>'), ('a -> b', 'IMPLIES', '=>'), ('a <=> b', 'EQUIV', '<->'), ('a <-> b', 'EQUIV', '<->'), ('a # b', 'XOR', '#'), ('a ^ b', 'XOR', '^'), ('a = b', 'EQUALS', '='), ] for expr, op_type, op_value in infixal: tokens = tokenize(expr, lexer) assert len(tokens) == 3, tokens a, op, b = tokens _assert_names_operator( op, a, b, op_type, op_value) expr = '@4' tokens = tokenize(expr, lexer) assert len(tokens) == 2, tokens at, four = tokens assert at.type == 'AT', at.type assert at.value == '@', at.value assert four.type == 'NUMBER', four.type assert four.value == '4', four.value expr = '@-1' tokens = tokenize(expr, lexer) assert len(tokens) == 3, tokens at, minus, one = tokens assert at.type == 'AT', at.type assert at.value == '@', at.value assert minus.type == 'MINUS', minus.type assert minus.value == '-', minus.value assert one.type == 'NUMBER', one.type assert one.value == '1', one.value expr = r'\A x1, x2, x3: (x1 => (x2 <=> x3))' tokens = tokenize(expr, lexer) assert len(tokens) == 16, tokens (forall, x1_1, comma_1, x2_1, comma_2, x3_1, colon, lparen_1, x1_2, implies, lparen_2, x2_2, equiv, x3_2, rparen_1, rparen_2) = tokens assert forall.type == 'FORALL', forall.type assert forall.value == r'\A', forall.value assert x1_1.type == 'NAME', x1_1.type assert x1_1.value == 'x1', x1_1.value assert x1_2.type == 'NAME', x1_2.type assert x1_2.value == 'x1', x1_2.value assert x2_1.type == 'NAME', x2_1.type assert x2_1.value == 'x2', x2_1.value assert x2_2.type == 'NAME', x2_2.type assert x2_2.value == 'x2', x2_2.value assert x3_1.type == 'NAME', x3_1.type assert x3_1.value == 'x3', x3_1.value assert x3_2.type == 'NAME', x3_2.type assert x3_2.value == 'x3', x3_2.value assert colon.type == 'COLON', colon.type assert colon.value == ':', colon.value assert lparen_1.type == 'LPAREN', lparen_1.type assert lparen_1.value == '(', lparen_1.value assert lparen_2.type == 'LPAREN', lparen_1.type assert lparen_2.value == '(', lparen_2.value assert rparen_1.type == 'RPAREN', rparen_1.type assert rparen_1.value == ')', rparen_1.value assert rparen_2.type == 'RPAREN', rparen_2.type assert rparen_2.value == ')', rparen_2.value assert implies.type == 'IMPLIES', implies.type assert implies.value == '=>', implies.value assert equiv.type == 'EQUIV', equiv.type assert equiv.value == '<->', equiv.value assert comma_1.type == 'COMMA', comma_1.type assert comma_1.value == ',', comma_1.value assert comma_2.type == 'COMMA', comma_2.type assert comma_2.value == ',', comma_2.value expr = r'\E x, y: x /\ y' tokens = tokenize(expr, lexer) assert len(tokens) == 8, tokens (exists, x_1, comma, y_1, colon, x_2, and_, y_2) = tokens assert exists.type == 'EXISTS', exists.type assert exists.value == r'\E', exists.value assert x_1.type == 'NAME', x_1.type assert x_1.value == 'x', x_1.value assert x_2.type == 'NAME', x_2.type assert x_2.value == 'x', x_2.value assert y_1.type == 'NAME', y_1.type assert y_1.value == 'y', y_1.value assert y_2.type == 'NAME', y_2.type assert y_2.value == 'y', y_2.value assert and_.type == 'AND', and_.type assert and_.value == '&', and_.value assert colon.type == 'COLON', colon.type assert colon.value == ':', colon.value assert comma.type == 'COMMA', comma.type assert comma.value == ',', comma.value def _assert_names_operator( op, a, b, op_type, op_value): assert a.type == 'NAME', a.type assert a.value == 'a', a.value assert b.type == 'NAME', b.type assert b.value == 'b', b.value assert op.type == op_type, ( op.type, op_type) assert op.value == op_value, ( op.value, op_value) def tokenize( string: str, lexer ) -> list: r"""Return tokens representing `string`. ```tla ASSUME /\ hasattr(lexer, 'lexer') /\ hasattr(lexer.lexer, 'input') /\ is_iterable(lexer.lexer) ``` """ lexer.lexer.input(string) return list(lexer.lexer) def test_parsing(): # nullary operators parser = dd._parser.Parser() expr = 'FALSE' tree = parser.parse(expr) _assert_false_node(tree) expr = 'TRUE' tree = parser.parse(expr) _assert_true_node(tree) expr = '@1' tree = parser.parse(expr) assert tree.type == 'num', tree.type assert tree.value == '1', tree.value expr = '@20' tree = parser.parse(expr) assert tree.type == 'num', tree.type assert tree.value == '20', tree.value expr = 'operator_name' tree = parser.parse(expr) assert tree.type == 'var', tree.type assert (tree.value == 'operator_name' ), tree.value expr = "operator_name'" tree = parser.parse(expr) assert tree.type == 'var', tree.type assert (tree.value == "operator_name'" ), tree.value expr = '_OPerATorN_amE' tree = parser.parse(expr) assert (tree.type == 'var' ), tree.type assert tree.value == '_OPerATorN_amE' # unary operators expr = '~ FALSE' tree = parser.parse(expr) assert tree.type == 'operator', tree.type assert tree.operator == '!', tree.operator assert (len(tree.operands) == 1 ), tree.operands tree, = tree.operands _assert_false_node(tree) expr = '! TRUE' tree = parser.parse(expr) assert tree.type == 'operator', tree.type assert tree.operator == '!', tree.operator assert (len(tree.operands) == 1 ), tree.operands tree, = tree.operands _assert_true_node(tree) expr = '@1' tree = parser.parse(expr) assert tree.type == 'num', tree.type assert tree.value == '1', tree.value expr = '@-5' tree = parser.parse(expr) assert tree.type == 'num', tree.type assert tree.value == '-5', tree.value # binary operators binary_operators = { '/\\': '&', r'\/': '|', '=>': '=>', '<=>': '<->', '&&': '&', '||': '|', '->': '=>', '<->': '<->', '&': '&', '|': '|', '^': '^', '#': '#', '=': '=', } pairs = binary_operators.items() for operator, token_type in pairs: _check_binary_operator( operator, token_type, parser) expr = 'TRUE <=> TRUE' tree = parser.parse(expr) _assert_binary_operator_tree(tree, '<->') true_1, true_2 = tree.operands _assert_true_node(true_1) _assert_true_node(true_2) expr = '(TRUE)' tree = parser.parse(expr) _assert_true_node(tree) expr = '(~ TRUE)' tree = parser.parse(expr) assert tree.type == 'operator', tree.type assert tree.operator == '!', tree.operator assert (len(tree.operands) == 1 ), tree.operands true, = tree.operands _assert_true_node(true) expr = r'(TRUE /\ FALSE)' tree = parser.parse(expr) _assert_binary_operator_tree(tree, '&') true, false = tree.operands _assert_false_true_nodes(false, true) # ternary operators expr = 'ite(TRUE, FALSE, TRUE)' tree = parser.parse(expr) assert tree.type == 'operator', tree.type assert tree.operator == 'ite', tree.operator assert (len(tree.operands) == 3 ), tree.operands true_1, false, true_2 = tree.operands _assert_true_node(true_1) _assert_true_node(true_2) _assert_false_node(false) # quantification expr = r'\A x, y, z: (x /\ y) => z' tree = parser.parse(expr) assert tree.type == 'operator', tree.type assert tree.operator == r'\A', tree.operator assert (len(tree.operands) == 2 ), tree.operands names, predicate = tree.operands assert len(names) == 3, names x, y, z = names assert x.type == 'var', x.type assert x.value == 'x', x.value assert y.type == 'var', y.type assert y.value == 'y', y.value assert z.type == 'var', z.type assert z.value == 'z', z.value _assert_binary_operator_tree( predicate, '=>') expr_1, expr_2 = predicate.operands _assert_binary_operator_tree( expr_1, '&') x, y = expr_1.operands assert x.type == 'var', x.type assert x.value == 'x', x.value assert y.type == 'var', y.type assert y.value == 'y', y.value assert expr_2.type == 'var', expr_2.type assert expr_2.value == 'z', expr_2.value names, predicate = tree.operands expr = r'\E u: (u = x) \/ (x # u)' tree = parser.parse(expr) assert tree.type == 'operator', tree.type assert (tree.operator == r'\E' ), tree.operator assert (len(tree.operands) == 2 ), tree.operands names, predicate = tree.operands assert len(names) == 1, names name, = names assert name.type == 'var', name.type assert name.value == 'u', name.value _assert_binary_operator_tree( predicate, '|') expr_1, expr_2 = predicate.operands _assert_binary_operator_tree(expr_1, '=') u, x = expr_1.operands assert u.type == 'var', u.type assert u.value == 'u', u.value assert x.type == 'var', x.type assert x.value == 'x', x.value _assert_binary_operator_tree(expr_2, '#') x, u = expr_2.operands assert x.type == 'var', x.type assert x.value == 'x', x.value assert u.type == 'var', u.type assert u.value == 'u', u.value def _check_binary_operator( operator: str, token_type: str, parser ) -> None: expr = f'FALSE {operator} TRUE' _log.debug(expr) tree = parser.parse(expr) _assert_binary_operator_tree( tree, token_type) false, true = tree.operands _assert_false_true_nodes(false, true) def _assert_binary_operator_tree( tree, operator: str ) -> None: assert tree.type == 'operator', tree.type assert tree.operator == operator, ( tree.operator, operator) assert (len(tree.operands) == 2 ), tree.operands def _assert_false_true_nodes( false, true): _assert_false_node(false) _assert_true_node(true) def _assert_false_node( false): assert false.type == 'bool', false.type assert false.value == 'FALSE', false.value def _assert_true_node( true): assert true.type == 'bool', true.type assert true.value == 'TRUE', true.value def test_add_expr(): bdd = BDD() for expr in PARSER_TEST_EXPRESSIONS: _log.debug(expr) dd._parser.add_expr(expr, bdd) class BDD: """Scaffold for testing.""" def __init__( self): self.false = 1 self.true = 1 def _add_int( self, number): _log.debug(f'{number = }') return 1 def var( self, name): _log.debug(f'{name =}') return 1 def apply( self, operator, *operands): _log.debug(f''' {operator = } {operands = } ''') return 1 def quantify( self, u, qvars, forall=None): _log.debug(f''' {u = } {qvars = } {forall = } ''') return 1 def rename( self, u, renaming): _log.debug(f''' {u = } {renaming = } ''') return 1 def test_recursive_traversal_vs_recursion_limit(): bdd = BDD() parser = dd._parser.Parser() # expression < recursion limit expr = r'a /\ b \/ c' tree = parser.parse(expr) _flattener._recurse_syntax_tree(tree, bdd) # expression > recursion limit expr = make_expr_gt_recursion_limit() tree = parser.parse(expr) with pytest.raises(RecursionError): _flattener._recurse_syntax_tree(tree, bdd) def test_iterative_traversal_vs_recursion_limit(): bdd = BDD() parser = dd._parser.Parser() # expression < recursion limit expr = r'a \/ ~ b /\ c' tree = parser.parse(expr) _flattener._reduce_syntax_tree(tree, bdd) # expression > recursion limit expr = make_expr_gt_recursion_limit() tree = parser.parse(expr) _flattener._reduce_syntax_tree(tree, bdd) if __name__ == '__main__': test_all_parsers_same_results() ================================================ FILE: tests/pytest.ini ================================================ # configuration file for package `pytest` [pytest] filterwarnings = error python_files = *_test.py python_classes = *Tests python_functions = test_* ================================================ FILE: tests/regressions_test.py ================================================ import dd.cudd as _cudd def test_reordering_setting_restore(): # Original report at https://github.com/tulip-control/dd/issues/40 b = _cudd.BDD() b.configure(reordering=False) b.add_var('x') b.add_var('y') # x /\ y s = r'~ x /\ y' u = b.add_expr(s) assert not b.configure()['reordering'] g = b.pick_iter(u) m = list(g) m_ = [dict(x=False, y=True)] assert m == m_, (m, m_) assert not b.configure()['reordering'] if __name__ == '__main__': test_reordering_setting_restore() ================================================ FILE: tests/sylvan_test.py ================================================ """Tests of the module `dd.sylvan`.""" import logging import dd.sylvan as _sylvan logging.getLogger('astutils').setLevel('ERROR') def test_len(): b = _sylvan.BDD() # constant assert len(b) == 0, len(b) u = b.false assert len(b) == 0, len(b) del u assert len(b) == 0, len(b) # var node b.add_var('x') u = b.var('x') assert len(b) == 1, len(b) del u assert len(b) == 0, len(b) def test_true_false(): b = _sylvan.BDD() false = b.false true = b.true assert false != true assert false == ~ true assert false == false & true assert true == true | false del true, false def test_add_var(): bdd = _sylvan.BDD() bdd.add_var('x') bdd.add_var('y') jx = bdd._index_of_var['x'] jy = bdd._index_of_var['y'] assert jx == 0, jx assert jy == 1, jy x = bdd._var_with_index[0] y = bdd._var_with_index[1] assert x == 'x', x assert y == 'y', y assert bdd.vars == {'x', 'y'}, bdd.vars x = bdd.var('x') y = bdd.var('y') assert x != y, (x, y) del x, y def test_insert_var(): bdd = _sylvan.BDD() level = 0 j = bdd.add_var('x', index=level) assert j == 0, j # initially indices = levels x = bdd.var_at_level(level) assert x == 'x', x level = 101 bdd.add_var('y', index=level) y = bdd.var_at_level(level) assert y == 'y', y def test_add_expr(): bdd = _sylvan.BDD() for var in ['x', 'y']: bdd.add_var(var) # ((0 \/ 1) /\ x) \equiv x s = r'(TRUE \/ FALSE) /\ x' u = bdd.add_expr(s) x = bdd.var('x') assert u == x, (u, x) # ((x \/ ~ y) /\ x) \equiv x s = r'(x \/ ~ y) /\ x' u = bdd.add_expr(s) assert u == x, (u, x) # x /\ y /\ z bdd.add_var('z') z = bdd.var('z') u = bdd.add_expr(r'x /\ y /\ z') u_ = bdd.cube(dict(x=True, y=True, z=True)) assert u == u_, (u, u_) # x /\ ~ y /\ z u = bdd.add_expr(r'x /\ ~ y /\ z') u_ = bdd.cube(dict(x=True, y=False, z=True)) assert u == u_, (u, u_) # (\E x: x /\ y) \equiv y y = bdd.var('y') u = bdd.add_expr(r'\E x: x /\ y') assert u == y, (str(u), str(y)) # (\A x: x \/ ~ x) \equiv TRUE u = bdd.add_expr(r'\A x: ~ x \/ x') assert u == bdd.true, u del x, y, z, u, u_ def test_support(): bdd = _sylvan.BDD() bdd.add_var('x') bdd.add_var('y') u = bdd.var('x') supp = bdd.support(u) assert supp == {'x'}, supp u = bdd.var('y') supp = bdd.support(u) assert supp == {'y'}, supp u = bdd.add_expr(r'x /\ y') supp = bdd.support(u) assert supp == {'x', 'y'}, supp del u def test_compose(): bdd = _sylvan.BDD() bdd.add_var('x') bdd.add_var('y') x = bdd.var('x') y = bdd.var('y') var_sub = dict(x=y) y_ = bdd.let(var_sub, x) assert y == y_, bdd.to_expr(y_) del x, y, y_, var_sub def test_cofactor(): bdd = _sylvan.BDD() bdd.add_var('x') x = bdd.var('x') # u = bdd.let(dict(x=True), x) # assert u == bdd.true, u # u = bdd.let(dict(x=False), x) # u = bdd.true # u_ = bdd.false # assert u == ~u_ assert x == bdd.add_expr('x') u = bdd.let(dict(x=bdd.false), x) u_ = bdd.false assert u == u_, (u, u_) del x, u, u_ def test_rename(): bdd = _sylvan.BDD() # single variable bdd.add_var('x') bdd.add_var('y') x = bdd.var('x') y = bdd.var('y') rename = dict(x='y') y_ = bdd.let(rename, x) assert y == y_, bdd.to_expr(y_) # multiple variables bdd.add_var('z') bdd.add_var('w') s = r'(x /\ ~ y) \/ w' u = bdd.add_expr(s) rename = dict(x='w', y='z', w='y') v = bdd.let(rename, u) s = r'(w /\ ~ z) \/ y' v_ = bdd.add_expr(s) assert v == v_, bdd.to_expr(v) del x, y, y_, u, v, v_ def test_count(): bdd = _sylvan.BDD() n = bdd.count(bdd.false) assert n == 0, n n = bdd.count(bdd.true) assert n == 1, n bdd.declare('x') x = bdd.var('x') n = bdd.count(x) assert n == 1, n bdd.declare('y') u = bdd.add_expr('~ y') n = bdd.count(u) assert n == 1, n u = bdd.add_expr(r'x /\ y') n = bdd.count(u) assert n == 1, n u = bdd.add_expr(r'~ x \/ y') n = bdd.count(u) assert n == 3, n bdd.declare('z') u = bdd.add_expr(r'x /\ (~y \/ z)') n = bdd.count(u) assert n == 3, n u = bdd.add_expr(r'x \/ y \/ ~z') n = bdd.count(u) assert n == 7, n # The function `test_pick_iter` is copied # from `common.Tests.test_pick_iter`. def test_pick_iter(): b = _sylvan.BDD() b.add_var('x') b.add_var('y') # FALSE u = b.false m = list(b.pick_iter(u)) assert not m, m # TRUE, no care vars u = b.true m = list(b.pick_iter(u)) assert m == [{}], m # x u = b.add_expr('x') m = list(b.pick_iter(u)) m_ = [dict(x=True)] assert m == m_, (m, m_) # ~ x /\ y s = r'~ x /\ y' u = b.add_expr(s) g = b.pick_iter(u, care_vars=set()) m = list(g) m_ = [dict(x=False, y=True)] assert m == m_, (m, m_) u = b.add_expr(s) g = b.pick_iter(u) m = list(g) assert m == m_, (m, m_) # x /\ y u = b.add_expr(r'x /\ y') m = list(b.pick_iter(u)) m_ = [dict(x=True, y=True)] assert m == m_, m # x s = '~ y' u = b.add_expr(s) # partial g = b.pick_iter(u) m = list(g) m_ = [dict(y=False)] equal_list_contents(m, m_) # partial g = b.pick_iter(u, care_vars=['x', 'y']) m = list(g) m_ = [ dict(x=True, y=False), dict(x=False, y=False)] equal_list_contents(m, m_) # care bits x, y b.add_var('z') s = r'x \/ y' u = b.add_expr(s) g = b.pick_iter(u, care_vars=['x', 'y']) m = list(g) m_ = [ dict(x=True, y=False), dict(x=False, y=True), dict(x=True, y=True)] equal_list_contents(m, m_) # The function `equal_list_contents` is copied # from `common.Tests.equal_list_contents`. def equal_list_contents(x, y): for u in x: assert u in y, (u, x, y) for u in y: assert u in x, (u, x, y) def test_py_operators(): bdd = _sylvan.BDD() bdd.declare('x', 'y') x = bdd.var('x') y = bdd.var('y') u = ~ x u_ = bdd.add_expr('~ x') assert u == u_, (u, u_) u = x & y u_ = bdd.add_expr(r'x /\ y') assert u == u_, (u, u_) u = x | y u_ = bdd.add_expr(r'x \/ y') assert u == u_, (u, u_) u = x ^ y u_ = bdd.add_expr('x # y') assert u == u_, (u, u_) if __name__ == '__main__': test_pick_iter()