[
  {
    "path": ".gitattributes",
    "content": "*.py eol=lf\n*.pyx eol=lf\n*.pxd eol=lf\n*.c eol=lf\n\n*.txt eol=lf\n*.md eol=lf\n*.yml eol=lf\n\n* filter=trimWhitespace\n"
  },
  {
    "path": ".github/workflows/main.yml",
    "content": "---\n# configuration for GitHub Actions\nname: dd tests\non:\n    push:\n    pull_request:\n    schedule:\n        - cron: '37 5 5 * *'\njobs:\n    build:\n        name: Build\n        runs-on: ubuntu-24.04\n        strategy:\n            matrix:\n                python-version: [\n                    '3.11',\n                    '3.12',\n                    '3.13',\n                    '3.14',\n                    ]\n        steps:\n            - uses: actions/checkout@v5\n            - name: Set up Python ${{ matrix.python-version }}\n              uses: actions/setup-python@v6\n              with:\n                  python-version: ${{ matrix.python-version }}\n            - name: Prepare installation environment\n              run: |\n                ./.github/workflows/setup_build_env.sh\n            - name: Install `dd`\n              run: |\n                set -o posix\n                echo 'Exported environment variables:'\n                export -p\n                export \\\n                    DD_FETCH=1 \\\n                    DD_CUDD=1 \\\n                    DD_CUDD_ZDD=1 \\\n                    DD_SYLVAN=1\n                pip install . \\\n                    --verbose \\\n                    --use-pep517 \\\n                    --no-build-isolation\n            - name: Install test dependencies\n              run: |\n                pip install pytest\n            - name: Run `dd` tests\n              run: |\n                set -o posix\n                echo 'Exported environment variables:'\n                export -p\n                # run tests\n                make test\n            - name: Run `dd` examples\n              run: |\n                pushd examples/\n                python _test_examples.py\n                popd\n"
  },
  {
    "path": ".github/workflows/setup_build_env.sh",
    "content": "#!/usr/bin/env bash\n\n\n# Prepare environment for building `dd`.\n\n\nset -x\nset -e\nsudo apt install \\\n    graphviz\ndot -V\npip install --upgrade \\\n    pip \\\n    setuptools \\\n    wheel\n# note that installing from `requirements.txt`\n# would also install packages that\n# may be absent from where `dd` will be installed\npip install cython\n#\n# install `sylvan`\n# download\ncurl -L \\\nhttps://github.com/utwente-fmt/sylvan/tarball/v1.0.0 \\\n-o sylvan.tar.gz\n# checksum\necho \"9877fe07a8cfe9889152e29624a4c5b283\\\ncb34672ec524ccb3edb313b3057fbf8ef45622a4\\\n9796fae17aa24e0baea5ccfa18f1bc5923e3c552\\\n45ab3e3c1927c8  sylvan.tar.gz\" | shasum -a 512 -c -\n# unpack\nmkdir sylvan\ntar xzf sylvan.tar.gz -C sylvan --strip=1\npushd sylvan\nautoreconf -fi\n./configure\nmake\nexport LD_LIBRARY_PATH=`pwd`/src/.libs:$LD_LIBRARY_PATH\necho $LD_LIBRARY_PATH\nif [[ -z \"${DEPLOY_ENV}\" ]]; then\n    # store values to use in later steps for\n    # environment variables\n    echo \"LD_LIBRARY_PATH=$LD_LIBRARY_PATH\" \\\n    >> $GITHUB_ENV\nfi\npopd\n"
  },
  {
    "path": ".gitignore",
    "content": ".coverage\n.cache\n.mypy_cache/\n.pytype/\n.pytest_cache/\ntodo.txt\ndd/_version.py\ncython_debug/*\ncudd*/*\nextern/\ndd/CUDD_LICENSE\ndd/GLIBC_COPYING.LIB\ndd/GLIBC_LICENSES\ndd/PYTHON_LICENSE\nbuddy-*/\nsylvan/\n.DS_Store\n._.DS_Store\n*.out\n*_state_machine.py\n*.c\n__pycache__\n*.pyc\n*.whl\n*.pdf\n*.png\n*.svg\n*.txt\n*.p\n*.json\n*.dddmp\n*.so\n*.html\n*.tar.gz\ndist/*\nbuild/*\n*.egg-info\n*.swp\n*.tags\n*.xml\n"
  },
  {
    "path": "AUTHORS",
    "content": "People that have authored commits to `dd`.\n\nIoannis Filippidis\nSofie Haesaert\nScott C. Livingston\nMario Wenzel\n\n"
  },
  {
    "path": "CHANGES.md",
    "content": "# dd changelog\n\n\n## 0.6.1\n\n- DEP: rm module `dd._compat`\n\n\n## 0.6.0\n\n- REL: require Python >= 3.11\n- REL: require `cython >= 3.0.0`\n- REL: require `astutils >= 0.0.5`\n- DEP: deprecate hidden module `dd._compat`\n\nAPI:\n\n- use TLA+ syntax for comments:\n  - `(* this is a doubly-delimited comment *)`\n  - `\\* this is a trailing comment`\n- use symbol `#` as operator that means\n  the logical negation of `<=>`.\n  The symbol `#` no longer signifies comments.\n- return `list` of loaded roots of BDDs,\n  when loading BDDs from Pickle files in:\n  - `dd.autoref.BDD.load()`\n  - `dd.bdd.BDD.load()`\n- in `dd.cudd`, `dd.cudd_zdd`\n  check available memory only in systems where\n  both `SC_PAGE_SIZE` and `SC_PHYS_PAGES` are\n  defined (read `sysconf(3)`).\n- raise `ValueError` from:\n  - `dd.autoref.BDD.level_of_var()`\n  - `dd.bdd.BDD.level_of_var()`\n  whenever unknown values are given as arguments\n- rename:\n  - method `dd.buddy.BDD.level()` to `dd.buddy.BDD.level_of_var()`\n  - method `dd.buddy.BDD.at_level()` to `dd.buddy.BDD.var_at_level()`\n  as the naming convention in other `dd` modules\n- raise `ValueError` from:\n  - `dd.buddy.BDD.level_of_var()`\n  - `dd.buddy.BDD.var_at_level()`\n  whenever unknown values are given as arguments,\n  as done in other `dd` modules too\n- several `assert` statements replaced by `raise`,\n  with more specific exceptions, e.g.,\n  `ValueError`, `TypeError`, `RuntimeError`\n- strings returned by methods:\n  - `dd.cudd.Function.__repr__()`\n  - `dd.cudd_zdd.Function.__repr__()`\n  changed to follow specification of `object.__repr__()`\n  (delimited by `<` and `>`).\n  Now also includes the object `id` as `hex` number.\n\n\n## 0.5.7\n\n- require `pytest >= 4.6.11`, instead of `nose`, for Python 3.10 compatibility\n- support for dumping and loading BDDs to and from JSON files\n  now requires Python 3\n- test using GitHub Actions\n\nAPI:\n\n- return memory size in bytes from methods `dd.cudd.BDD.statistics` and\n  `dd.cudd_zdd.ZDD.statistics`\n  (the value of key `'mem'` in the returned `dict`)\n- print used memory in bytes in the methods `dd.cudd.BDD.__str__` and\n  `dd.cudd_zdd.ZDD.__str__`\n- remove the now obsolete constants `dd.cudd.GB`, `dd.cudd_zdd.GB`\n- remove the unused constant `dd.sylvan.GB`\n- method `dd.cudd_zdd.ZDD.dump`:\n  - support PNG and SVG formats, in addition to PDF\n  - allow more than one references to ZDD nodes in the argument `roots`\n- add method `apply` to the class `dd.mdd.MDD`\n- several `assert` statements replaced by `raise` with\n  exceptions more specific than `AssertionError`\n- set `dd.cudd.Function.node` and `dd.cudd_zdd.Function.node`\n  to `NULL` when the (local) reference count becomes zero\n\n\n## 0.5.6\n\n- distribute `manylinux2014_x86_64` wheel via PyPI\n\nAPI:\n\n- require `cython >= 0.29.15`\n- add module `dd.cudd_zdd`\n- allow empty support variable names in DDDMP files in function `dd.dddmp.load`\n- methods `dump` and `load` of the classes\n  `dd.cudd.BDD` and `dd.autoref.BDD`:\n  - add JSON to file types\n  - load by file extension\n- change return type of method `dd.cudd.BDD.load`\n  to `list` of `dd.cudd.Function`\n- multiple roots supported in `dd.cudd.BDD.dump` for\n  file types other than DDDMP\n- method `count` of the classes\n  `dd.cudd.BDD` and `dd.cudd_zdd.ZDD`:\n  - make optional the argument `nvars`\n- `dd.autoref.BDD.load`:\n  require file extension `.p` for pickle files\n\n\n## 0.5.5\n\nAPI:\n\n- require `networkx <= 2.2` on Python 2\n- class `dd.bdd.BDD`:\n  - remove argument `debug` from method `_next_free_int`\n  - add method `undeclare_variables`\n- plot nodes for external BDD references in function `dd.bdd.to_pydot`,\n  which is used by the methods `BDD.dump` of the modules\n  `dd.cudd`, `dd.autoref`, and `dd.bdd`\n- function `dd._copy.load_json`:\n  rename argument from `keep_order` to `load_order`\n- add unused keyword arguments to method `autoref.BDD.decref`\n\n\n## 0.5.4\n\n- enable `KeyboardInterrupt` on POSIX systems for `cudd`\n  when `cysignals >= 1.7.0` is present at installation\n\nAPI:\n\n- change signature of method `cudd.BDD.dump`\n- add GraphViz as an option of `cudd.BDD.dump`\n- allow copying between managers with different variable orders\n- allow simultaneous substitution in `bdd.BDD.let`\n- add property `BDD.var_levels`\n- add method `BDD.reorder` to `cudd` and `autoref`\n- add method `cudd.BDD.group` for grouping variables\n- add `autoref.BDD` methods `incref` and `decref`\n- change signatures of `cudd.BDD` methods `incref` and `decref`\n- change default to `recursive=False` in method `cudd.BDD.decref`\n- add property `Function.dag_size`\n- add module `dd._copy`\n- rm function `dd.bdd.copy_vars`, use method `BDD.declare` instead,\n  and separately copy variable order, if needed.\n  This function has moved to `_copy.copy_vars`.\n- rm method `bdd.BDD.evaluate`, use method `dd.BDD.let`\n\n\n## 0.5.3\n\n- distribute `manylinux1_x86_64` wheel via PyPI\n\nAPI:\n\n- update to `networkx >= 2.0` (works with `< 2.0` too)\n- class `BDD` in modules `autoref`, `bdd`, `cudd`, `sylvan`:\n  - remove deprecated methods (where present):\n    `compose`, `cofactor`, `rename`, `evaluate`,\n    `sat_iter`, `sat_len`\n\n\n## 0.5.2\n\nAPI:\n\n- require `networkx < 2.0.0`\n- add module `dd._abc` that defines API implemented by other modules.\n- add method `declare` to `BDD` classes\n- add methods `implies` and `equiv` to class `cudd.Function`\n- change BDD node reference syntax to \"@ INTEGER\"\n- change `Function.__str__` to include `@` in modules `cudd` and `autoref`\n- deprecate `BDD` methods `compose`, `cofactor`, `rename`, `evaluate`,\n  instead use `BDD.let`\n- class `BDD` in modules `autoref`, `bdd`, `cudd`, `sylvan`:\n  - methods `pick`, `pick_iter`:\n    rename argument from `care_bits` to `care_vars`\n- class `BDD` in modules `autoref`, `bdd`:\n  - method `count`:\n    rename argument from `n` to `nvars`\n- class `BDD` in modules `bdd`, `cudd`:\n  - allow swapping variables in method `rename`,\n    accept only variable names, not levels\n- rm argument `bdd` from functions:\n  - `image`, `preimage` in module `autoref`\n  - `and_exists`, `or_forall`, `dump` in module `cudd`\n  - `and_exists`, `or_forall` in module `sylvan`\n- rm argument `roots` from method `autoref.BDD.collect_garbage`\n- rm argument `source` from function:\n  `copy_bdd` in modules `autoref`, `cudd`\n- rm function `cudd.rename`, use method `cudd.BDD.let`\n- rm function `autoref.rename`, use method `autoref.BDD.let`\n- rm method `autoref.Function.__xor__`\n- add TLA constants \"TRUE\" and \"FALSE\" to syntax,\n  use these in method `BDD.to_expr`\n\n\n## 0.5.1\n\nAPI:\n\n- classes `cudd.BDD`, `autoref.BDD`, `bdd.BDD`:\n  - add method `let`, which will replace `compose`, `cofactor`, `rename`\n  - add method `pick`\n  - add method `pick_iter`, deprecate `sat_iter`\n  - add method `count`, deprecate `sat_len`\n  - allow copying node to same manager, but log warning\n- class `sylvan.BDD`:\n  - add method `let`\n- classes `cudd.Function`, `autoref.Function`:\n  - implement all comparison methods (`__le__`, `__lt__`)\n\n\n## 0.5.0\n\nAPI:\n\n- dynamic variable reordering in `dd.bdd.BDD` (by default disabled)\n- method `bdd.BDD.sat_len`: count only levels in support (similar to CUDD)\n- class `autoref.Function`:\n  - rename attribute `bdd` to `manager`\n- classes `cudd.Function`, `autoref.Function`, `sylvan.Function`:\n  - add attributes `var, support, bdd`\n  - add method `__hash__`\n- classes `cudd.Function` and `sylvan.Function`:\n  - hide attribute `index` as `_index`\n- classes `cudd.BDD` and `sylvan.BDD`:\n  - do not memoize attributes `false` and `true`\n- classes `cudd.BDD` and `autoref.BDD`:\n  - add method `find_or_add`\n- method `BDD.sat_iter`:\n  - rm arg `full`\n  - `care_bits = support` as default\n  - `care_bits < support` allowed\n- function `bdd.to_pydot`: plot only levels in support of given node\n- add function `autoref.reorder`\n\n\n## 0.4.3\n\nAPI:\n\n- build `dd.cudd` using CUDD v3.0.0\n  (an older CUDD via an older `download.py` should work too)\n\n\n## 0.4.2\n\nAPI:\n\n- classes `bdd.BDD`, `autoref.BDD`:\n  - rm attribute `ordering`, use `vars`\n  - rename `__init__` argument `ordering` to `levels`\n- allow passing path to CUDD during installation via `--cudd`\n\n\n## 0.4.1\n\n- add Cython interface `dd.sylvan` to Sylvan\n- support TLA+ syntax\n\nBUG:\n\n- in Python 2 use `sys.maxint` for `bdd.BDD.max_nodes`\n\nAPI:\n\n- classes `bdd.BDD` and `cudd.BDD`:\n  - method `apply`: rm `\"bimplies\"` value\n  - raise `AssertionError` if `care_bits < support` in method `sat_iter`\n- rm unused operator `!=` from parser grammar\n- class `autoref.Function`:\n  - rename method `bimplies` to `equiv`\n\n\n## 0.4.0\n\n- require `pydot >= 1.2.2`\n\n\nAPI:\n\n- change quantification syntax to `\\E x, y: x`\n- add renaming syntax `\\S x / y,  z / w: y & w`\n- class `BDD` in `dd.bdd`, `dd.autoref`, `dd.cudd`:\n  - add operators `'ite', '\\E', '\\A'` to method `apply`\n  - add methods `forall` and `exist` as wrappers of `quantify`\n  - add method `_add_int` for checking presence of\n    a BDD node represented as an integer\n  - add method `succ` to obtain `(level, low, high)`\n- class `cudd.BDD`:\n  - add method `compose`\n  - add method `ite`\n  - add method `to_expr`\n- class `cudd.Function`:\n  - add method `__int__` to represent CUDD nodes\n    uniquely as integers (by shifting the C pointer value to\n    avoid possible conflicts with reserved values)\n  - add method `__str__` to return integer repr as `str`\n  - add attribute `level`\n  - add attribute `negated`\n- module `cudd`:\n  - add function `restrict`\n  - add function `count_nodes`\n- remove \"extra\" named `dot`, because `pydot` is now required\n\n\n## 0.3.1\n\nBUG:\n\n- `dd.bdd.BDD.dump`: if argument `roots is None` (default),\n  then dump all nodes\n- `dd.autoref.BDD.compose`: call wrapped method correctly\n\n\n## 0.3.0\n\nAPI:\n\n- `dd.bdd.BDD.rename`, `dd.bdd.image`, `dd.bdd.preimage`: allow non-adjacent variable levels\n- `dd.bdd.BDD.descendants`:\n  - arg `roots` instead of single node `u`\n  - iteration instead of recursion\n  - breadth-first instead of depth-first search\n- `dd.bdd.BDD.dump`:\n  - dump nodes reachable from given roots\n  - dump only variable levels and nodes to pickle file\n  - correct error that ignored explicit file type for PDF, PNG, SVG\n- `dd.bdd.BDD.load`:\n  - instance method to load nodes\n- `dd.bdd.to_pydot`:\n  - add arg `roots`\n- hide methods that dump and load entire manager\n  - `dd.bdd.BDD._dump_manager` and `_load_manager`\n- remove `dd.autoref.Function.from_expr`\n\n\n## 0.2.2\n\n- install without extensions by default\n- try to read git information, but assume release if this fails for any reason\n\n\n## 0.2.1\n\n- optionally import `gitpython` in `setup.py` to retrieve\n  version info from `git` repo.\n- version identifier when `git` available:\n  `X.Y.Z.dev0+SHA[.dirty]`\n- require `psutil >= 3.2.2`\n- require `setuptools >= 19.6` to avoid `cython` affecting `psutil` build\n- detect 64-bit system using `ctypes.sizeof` for CUDD flags\n\nAPI:\n\n- `dd.cudd.BDD.__cinit__`:\n  - rename arg `memory` -> `memory_estimate`\n  - assert memory estimate less than `psutil.virtual_memory().total`\n  - add arg `initial_cache_size`\n- `dd.cudd.BDD.statistics`:\n  - distinguish between peak and live nodes\n  - cache statistics\n  - unique table statistics\n  - read node count without removing unused nodes\n- `dd.cudd.BDD.configure`:\n  - accept keyword args, instead of `dict`\n  - first read config (returned `dict`), then set given values\n  - reordering\n  - garbage collection\n  - max cache soft\n  - max swaps\n  - max variables per reordering\n- `dd.bdd`, `dd.autoref`, `dd.cudd`:\n  - add method `BDD.copy` for copying nodes between managers\n  - add method `BDD.rename` for substituting variables\n  - deprecate functions `rename` and `copy_bdd`\n- add method `dd.cudd.BDD.sat_iter`\n- add function `dd.cudd.count_nodes_per_level`\n- add functions that track variable order when saving:\n  - `dd.cudd.dump`\n  - `dd.cudd.load`\n\n\n## 0.2.0\n\n- add user documentation\n- support Python 3\n- require `pydot3k` in Python 3, `pydot` in Python 2\n- expose more control over CUDD configuration\n\nAPI:\n\n- add `dd.cudd.BDD.configure`\n- do not set manager parameters in `__cinit__`\n- rename `BDD.False` -> `BDD.false` (same for “true”), to avoid syntax errors in Python 3\n- remove `dd.bdd.BDD.add_ast`\n- `dd.cudd.reorder` invokes sifting if variable order is `None`\n- default to pickle protocol 2\n\n\n## 0.1.3\n\nBugfix release to add file `download.py` missing from MANIFEST.\n\nAPI:\n\n- add `dd.cudd.BDD.statistics`\n- add functions `copy_vars` and `copy_bdd`\n- remove `dd.bdd.BDD.level_to_variable`\n\n\n## 0.1.2\n\n- add Cython interface `dd.cudd` to CUDD\n- add Cython interface `dd.buddy` to BuDDy\n\n\n## 0.1.1\n\n- dynamic variable addition in `dd.bdd.BDD`\n- add `dd.autoref` wrapper around `dd.bdd`\n- avoid randomization inside `sat_iter`\n\nAPI:\n\n- add `BDD.True` and `BDD.False`\n- move `Function` interface to `dd.autoref`\n- move parser to `dd._parser`\n- rename `BDD.level_to_variable` -> `var_at_level`\n- deprecate `BDD.ordering` in favor of `BDD.vars`\n\n\n## 0.0.4\n\n- add `dd.mdd` for multi-terminal decision diagrams\n- add quantifiers to syntax\n- add complemented edges to syntax\n- require `networkx`\n\nAPI:\n\n- add `dd.bdd.BDD.cube`\n- add `dd.bdd.BDD.descendants`\n- add function `reorder_pairs`\n\n\n## 0.0.3\n\n- add PLY parser for Boolean expressions\n- require `astutils`\n\nAPI:\n\n- add `dd.bdd.BDD.ref`\n- assign `bool` as model values\n\n\n## 0.0.2\n\n- test on Travis\n\nAPI:\n\n- add `\"diff\"` operator to `dd.bdd.BDD.apply`\n\n\n## 0.0.1\n\nInitial release.\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2014-2022 by California Institute of Technology\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions\nare met:\n\n1. Redistributions of source code must retain the above copyright\n   notice, this list of conditions and the following disclaimer.\n\n2. Redistributions in binary form must reproduce the above copyright\n   notice, this list of conditions and the following disclaimer in the\n   documentation and/or other materials provided with the distribution.\n\n3. Neither the name of the California Institute of Technology nor\n   the names of its contributors may be used to endorse or promote\n   products derived from this software without specific prior\n   written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n\"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\nLIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR\nA PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL CALTECH OR THE\nCONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,\nEXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,\nPROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR\nPROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF\nLIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING\nNEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\nSOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "include README.md\ninclude LICENSE\ninclude doc.md\ninclude CHANGES.md\ninclude AUTHORS\ninclude requirements.txt\ninclude download.py\ninclude tests/README.md\ninclude tests/pytest.ini\ninclude tests/inspect_cython_signatures.py\ninclude tests/common.py\ninclude tests/common_bdd.py\ninclude tests/common_cudd.py\ninclude tests/iterative_recursive_flattener.py\ninclude tests/*_test.py\ninclude tests/sample*.dddmp\ninclude examples/README.md\ninclude examples/*.py\ninclude examples/*.sh\ninclude dd/py.typed\ninclude dd/*.pyx\ninclude dd/*.pxd\ninclude dd/*.c\ninclude dd/CUDD_LICENSE\ninclude dd/GLIBC_COPYING.LIB\ninclude dd/GLIBC_LICENSES\ninclude dd/PYTHON_LICENSE\n"
  },
  {
    "path": "Makefile",
    "content": "# build, install, test, release `dd`\n\n\nSHELL := bash\nwheel_file := $(wildcard dist/*.whl)\n\n.PHONY: cudd install test\n.PHONY: clean clean_all clean_cudd wheel_deps\n\n\nbuild_cudd: clean cudd install test\n\nbuild_sylvan: clean wheel_deps\n\t-pip uninstall -y dd\n\texport DD_SYLVAN=1; \\\n\tpip install . -vvv --use-pep517 --no-build-isolation\n\tpip install pytest\n\tmake test\n\nsdist_test: clean wheel_deps\n\tpip install -U build cython\n\texport DD_CUDD=1 DD_BUDDY=1; \\\n\tpython -m build --sdist --no-isolation\n\tpushd dist; \\\n\tpip install dd*.tar.gz; \\\n\ttar -zxf dd*.tar.gz && \\\n\tpopd\n\tpip install pytest\n\tmake -C dist/dd*/ -f ../../Makefile test\n\nsdist_test_cudd: clean wheel_deps\n\tpip install build cython ply\n\texport DD_CUDD=1 DD_BUDDY=1; \\\n\tpython -m build --sdist --no-isolation\n\tyes | pip uninstall cython ply\n\tpushd dist; \\\n\ttar -zxf dd*.tar.gz; \\\n\tpushd dd*/; \\\n\texport DD_FETCH=1 DD_CUDD=1; \\\n\tpip install . -vvv --use-pep517 --no-build-isolation && \\\n\tpopd && popd\n\tpip install pytest\n\tmake -C dist/dd*/ -f ../../Makefile test\n\n# use to create source distributions for PyPI\nsdist: clean wheel_deps\n\t-rm dist/*.tar.gz\n\tpip install -U build cython\n\texport DD_CUDD=1 DD_CUDD_ZDD=1 DD_BUDDY=1 DD_SYLVAN=1; \\\n\tpython -m build --sdist --no-isolation\n\nwheel_deps:\n\tpip install --upgrade \\\n\t    cython \\\n\t    pip \\\n\t    setuptools \\\n\t    wheel\n\n# use to create binary distributions for PyPI\nwheel: clean wheel_deps\n\t-rm dist/*.whl\n\t-rm wheelhouse/*.whl\n\texport DD_CUDD=1 DD_CUDD_ZDD=1; \\\n\tpip wheel . \\\n\t    -vvv \\\n\t    --wheel-dir dist \\\n\t    --no-deps\n\t@echo \"-------------\"\n\tauditwheel show dist/*.whl\n\t@echo \"-------------\"\n\tauditwheel repair --plat manylinux_2_17_x86_64 dist/*.whl\n\t@echo \"-------------\"\n\tauditwheel show wheelhouse/*.whl\n\ninstall: wheel_deps\n\texport DD_CUDD=1; \\\n\tpip install . -vvv --use-pep517 --no-build-isolation\n\nreinstall: uninstall wheel_deps\n\texport DD_CUDD=1 DD_CUDD_ZDD=1 DD_SYLVAN=1; \\\n\tpip install . -vvv --use-pep517 --no-build-isolation\n\nreinstall_buddy: uninstall wheel_deps\n\texport DD_BUDDY=1; \\\n\tpip install . -vvv --use-pep517 --no-build-isolation\n\nreinstall_cudd: uninstall wheel_deps\n\texport DD_CUDD=1 DD_CUDD_ZDD=1; \\\n\tpip install . -vvv --use-pep517 --no-build-isolation\n\nreinstall_sylvan: uninstall wheel_deps\n\texport DD_SYLVAN=1; \\\n\tpip install . -vvv --use-pep517 --no-build-isolation\n\nuninstall:\n\tpip uninstall -y dd\n\ntest:\n\tset -x; \\\n\tpushd tests; \\\n\tpython -X dev -m pytest -v --continue-on-collection-errors . && \\\n\tpopd\n# `pytest -Werror` turns all warnings into errors\n#     <https://docs.pytest.org/en/latest/how-to/capture-warnings.html>\n# including pytest warnings about unraisable exceptions:\n#     <https://docs.pytest.org/en/latest/how-to/failures.html\n#         #warning-about-unraisable-exceptions-and-unhandled-thread-exceptions>\n#     <https://docs.pytest.org/en/latest/reference/reference.html\n#         #pytest.PytestUnraisableExceptionWarning>\n\ntest_abc:\n\tpython -X dev tests/inspect_cython_signatures.py\n\ntest_examples:\n\tpushd examples/; \\\n\tfor script in `ls *.py`;  \\\n\tdo \\\n\t    echo \"Running: $$script\"; \\\n\t    python -X dev $$script; \\\n\tdone && \\\n\tpopd\n\nshow_deprecated:\n\tpython -X dev -Wall -c \"from dd import bdd\"\n\ntypecheck:\n\tpytype \\\n\t    -k \\\n\t    -v 1 \\\n\t    -j 'auto' \\\n\t        dd/*.py \\\n\t        setup.py \\\n\t        examples/*.py\n\t        # tests/*.py\n\t        # download.py\n\nclean_type_cache:\n\t-rm -rf .pytype/\n\ncudd:\n\tpushd cudd-*/; \\\n\t./configure \"CFLAGS=-fPIC -std=c99\" \\\n\tmake build XCFLAGS=\"\\\n\t    -fPIC \\\n\t    -mtune=native \\\n\t    -DHAVE_IEEE_754 \\\n\t    -DBSD \\\n\t    -DSIZEOF_VOID_P=8 \\\n\t    -DSIZEOF_LONG=8\" && \\\n\tpopd\n\ndoc:\n\tgrip --export doc.md index.html\n\ndownload_licenses:\n\tpython -c 'import download; \\\n\tdownload.download_licenses()'\n\nclean_all: clean_cudd clean\n\nclean_cudd:\n\tpushd cudd-*/; make clean && popd\n\nclean:\n\t-rm -rf build/ dist/ dd.egg-info/\n\t-rm dd/*.so\n\t-rm dd/buddy.c\n\t-rm dd/cudd.c\n\t-rm dd/cudd_zdd.c\n\t-rm dd/sylvan.c\n\t-rm *.pyc */*.pyc\n\t-rm -rf __pycache__ */__pycache__\n\t-rm -rf wheelhouse\n\nrm_cudd:\n\t-rm -rf cudd*/ cudd*.tar.gz\n"
  },
  {
    "path": "README.md",
    "content": "[![Build Status][build_img]][ci]\n\n\nAbout\n=====\n\nA pure-Python (Python >= 3.11) package for manipulating:\n\n- [Binary decision diagrams](\n    https://en.wikipedia.org/wiki/Binary_decision_diagram) (BDDs).\n- [Multi-valued decision diagrams](\n    https://dx.doi.org/10.1109/ICCAD.1990.129849) (MDDs).\n\nas well as [Cython](https://cython.org) bindings to the C libraries:\n\n- [CUDD](\n    https://web.archive.org/web/20180127051756/http://vlsi.colorado.edu/~fabio/CUDD/html/index.html)\n  (also read [the introduction](\n    https://web.archive.org/web/20150317121927/http://vlsi.colorado.edu/~fabio/CUDD/node1.html),\n  and note that the original link for CUDD is <http://vlsi.colorado.edu/~fabio/CUDD/>)\n- [Sylvan](https://github.com/utwente-fmt/sylvan) (multi-core parallelization)\n- [BuDDy](https://sourceforge.net/projects/buddy/)\n\nThese bindings expose almost identical interfaces as the Python implementation.\nThe intended workflow is:\n\n- develop your algorithm in pure Python (easy to debug and introspect),\n- use the bindings to benchmark and deploy\n\nYour code remains the same.\n\n\nContains:\n\n- All the standard functions defined, e.g.,\n  by [Bryant](https://www.cs.cmu.edu/~bryant/pubdir/ieeetc86.pdf).\n- Dynamic variable reordering using [Rudell's sifting algorithm](\n    http://www.eecg.toronto.edu/~ece1767/project/rud.pdf).\n- Reordering to obtain a given order.\n- Parser of quantified Boolean expressions in either\n  [TLA+](https://en.wikipedia.org/wiki/TLA%2B) or\n  [Promela](https://en.wikipedia.org/wiki/Promela) syntax.\n- Pre/Image computation (relational product).\n- Renaming variables.\n- Zero-omitted binary decision diagrams (ZDDs) in CUDD\n- Conversion from BDDs to MDDs.\n- Conversion functions to [`networkx`](https://networkx.org) and\n  [DOT](https://www.graphviz.org/doc/info/lang.html) graphs.\n- BDDs have methods to `dump` and `load` them using [JSON](\n    https://wikipedia.org/wiki/JSON), or [`pickle`](\n    https://docs.python.org/3/library/pickle.html).\n- BDDs dumped by CUDD's DDDMP can be loaded using fast iterative parser.\n- [Garbage collection](\n    https://en.wikipedia.org/wiki/Garbage_collection_(computer_science))\n  that combines reference counting and tracing\n\n\nIf you prefer to work with integer variables instead of Booleans, and have\nBDD computations occur underneath, then use the module\n[`omega.symbolic.fol`](\n    https://github.com/tulip-control/omega/blob/main/omega/symbolic/fol.py)\nfrom the [`omega` package](\n    https://github.com/tulip-control/omega/blob/main/doc/doc.md).\n\nIf you are interested in computing minimal covers (two-level logic minimization)\nthen use the module `omega.symbolic.cover` of the `omega` package.\nThe method `omega.symbolic.fol.Context.to_expr` converts BDDs to minimal\nformulas in disjunctive normal form (DNF).\n\n\nDocumentation\n=============\n\nIn the [Markdown](https://en.wikipedia.org/wiki/Markdown) file\n[`doc.md`](https://github.com/tulip-control/dd/blob/main/doc.md).\n\nThe [changelog](https://en.wiktionary.org/wiki/changelog) is in\nthe file [`CHANGES.md`](\n    https://github.com/tulip-control/dd/blob/main/CHANGES.md).\n\n\nExamples\n========\n\nThe module `dd.autoref` wraps the pure-Python BDD implementation `dd.bdd`.\nThe API of `dd.cudd` is almost identical to `dd.autoref`.\nYou can skip details about `dd.bdd`, unless you want to implement recursive\nBDD operations at a low level.\n\n\n```python\nfrom dd.autoref import BDD\n\nbdd = BDD()\nbdd.declare('x', 'y', 'z', 'w')\n\n# conjunction (in TLA+ syntax)\nu = bdd.add_expr(r'x /\\ y')\n    # symbols `&`, `|` are supported too\n    # note the \"r\" before the quote,\n    # which signifies a raw string and is\n    # needed to allow for the backslash\nprint(u.support)\n# substitute variables for variables (rename)\nrename = dict(x='z', y='w')\nv = bdd.let(rename, u)\n# substitute constants for variables (cofactor)\nvalues = dict(x=True, y=False)\nv = bdd.let(values, u)\n# substitute BDDs for variables (compose)\nd = dict(x=bdd.add_expr(r'z \\/ w'))\nv = bdd.let(d, u)\n# as Python operators\nv = bdd.var('z') & bdd.var('w')\nv = ~ v\n# quantify universally (\"forall\")\nu = bdd.add_expr(r'\\A x, y:  (x /\\ y) => y')\n# quantify existentially (\"exist\")\nu = bdd.add_expr(r'\\E x, y:  x \\/ y')\n# less readable but faster alternative,\n# (faster because of not calling the parser;\n# this may matter only inside innermost loops)\nu = bdd.var('x') | bdd.var('y')\nu = bdd.exist(['x', 'y'], u)\nassert u == bdd.true, u\n# inline BDD references\nu = bdd.add_expr(rf'x /\\ {v}')\n# satisfying assignments (models):\n# an assignment\nd = bdd.pick(u, care_vars=['x', 'y'])\n# iterate over all assignments\nfor d in bdd.pick_iter(u):\n    print(d)\n# how many assignments\nn = bdd.count(u)\n# write to and load from JSON file\nfilename = 'bdd.json'\nbdd.dump(filename, roots=dict(res=u))\nother_bdd = BDD()\nroots = other_bdd.load(filename)\nprint(other_bdd.vars)\n```\n\nTo run the same code with CUDD installed, change the first line to:\n\n```python\nfrom dd.cudd import BDD\n```\n\nMost useful functionality is available via methods of the class `BDD`.\nA few of the functions can prove useful too, among them `to_nx()`.\nUse the method `BDD.dump` to write a `BDD` to a `pickle` file, and\n`BDD.load` to load it back. A CUDD dddmp file can be loaded using\nthe function `dd.dddmp.load`.\n\nA `Function` object wraps each BDD node and decrements its reference count\nwhen disposed by Python's garbage collector. Lower-level details are\ndiscussed in the documentation.\n\nFor using ZDDs, change the first line to\n\n```python\nfrom dd.cudd_zdd import ZDD as BDD\n```\n\n\nInstallation\n============\n\n\n## pure-Python\n\nFrom the [Python Package Index (PyPI)](https://pypi.org) using the\npackage installer [`pip`](https://pip.pypa.io):\n\n```shell\npip install dd\n```\n\nor from the directory of source files:\n\n```shell\npip install .\n```\n\nFor graph layout, install also [graphviz](https://graphviz.org).\n\nThe `dd` package requires Python 3.11 or later.\nFor Python 2.7, use `dd == 0.5.7`.\n\n\n## Cython bindings\n\nTo compile also the module `dd.cudd` (which interfaces to CUDD)\nwhen installing from PyPI, run:\n\n```shell\npip install --upgrade wheel cython\nexport DD_FETCH=1 DD_CUDD=1\npip install dd -vvv --use-pep517 --no-build-isolation\n```\n\n(`DD_FETCH=1 DD_CUDD=1 pip install dd` also works,\nwhen the source tarball includes cythonized code.)\n\nTo confirm that the installation succeeded:\n\n```shell\npython -c 'import dd.cudd'\n```\n\nThe [environment variables](\n    https://en.wikipedia.org/wiki/Environment_variable)\nabove mean:\n- `DD_FETCH=1`: download CUDD v3.0.0 sources from the internet,\n  unpack the tarball (after checking its hash), and `make` CUDD.\n- `DD_CUDD=1`: build the Cython module `dd.cudd`\n\nMore about environment variables that configure the\nC extensions of `dd` is described in the file [`doc.md`](\n    https://github.com/tulip-control/dd/blob/main/doc.md)\n\n\n## Wheel files with compiled CUDD\n\n[Wheel files](\n    https://www.python.org/dev/peps/pep-0427/)\nare [available from PyPI](\n    https://pypi.org/project/dd/#files),\nwhich contain the module `dd.cudd`,\nwith the CUDD library compiled and linked.\nIf you have a Linux system and Python version compatible with\none of the PyPI wheels,\nthen `pip install dd` will install also `dd.cudd`.\n\n\n### Licensing of the compiled modules `dd.cudd` and `dd.cudd_zdd` in the wheel\n\nThese notes apply to the compiled modules `dd.cudd` and `dd.cudd_zdd` that are\ncontained in the [wheel file](https://www.python.org/dev/peps/pep-0427/) on\nPyPI (namely the files `dd/cudd.cpython-39-x86_64-linux-gnu.so` and\n`dd/cudd_zdd.cpython-39-x86_64-linux-gnu.so` in the [`*.whl` file](\n    https://pypi.org/project/dd/#files), which can\nbe obtained using [`unzip`](http://infozip.sourceforge.net/UnZip.html)).\nThese notes do not apply to the source code of the modules\n`dd.cudd` and `dd.cudd_zdd`.\nThe source distribution of `dd` on PyPI is distributed under a 3-clause BSD\nlicense.\n\nThe following libraries and their headers were used when building the modules\n`dd.cudd` and `dd.cudd_zdd` that are included in the wheel:\n\n- Python: <https://www.python.org/ftp/python/3.A.B/Python-3.A.B.tgz>\n  (where `A` and `B` the numerals of\n   the corresponding Python version used;\n   for example `10` and `2` to signify Python 3.10.2).\n  CPython releases are described at:\n    <https://www.python.org/downloads/>\n- [CUDD](https://sourceforge.net/projects/cudd-mirror/files/cudd-3.0.0.tar.gz/download).\n\nThe licenses of Python and CUDD are included in the wheel archive.\n\nCython [does not](https://github.com/cython/cython/blob/master/COPYING.txt)\nadd its license to C code that it generates.\n\nGCC was used to compile the modules `dd.cudd` and `dd.cudd_zdd` in the wheel,\nand the GCC [runtime library exception](\n    https://github.com/gcc-mirror/gcc/blob/master/COPYING.RUNTIME#L61-L66)\napplies.\n\nThe modules `dd.cudd` and `dd.cudd_zdd` in the wheel dynamically link to the:\n\n- Linux kernel (in particular [`linux-vdso.so.1`](\n    https://man7.org/linux/man-pages/man7/vdso.7.html)),\n  which allows system calls (read the kernel's file [`COPYING`](\n    https://github.com/torvalds/linux/blob/master/COPYING) and the explicit\n  syscall exception in the file [`LICENSES/exceptions/Linux-syscall-note`](\n    https://github.com/torvalds/linux/blob/master/LICENSES/exceptions/Linux-syscall-note))\n- [GNU C Library](https://www.gnu.org/software/libc/) (glibc) (in particular\n  `libpthread.so.0`, `libc.so.6`, `/lib64/ld-linux-x86-64.so.2`), which uses\n  the [LGPLv2.1](https://sourceware.org/git/?p=glibc.git;a=blob_plain;f=COPYING.LIB;hb=HEAD)\n  that allows dynamic linking, and other [licenses](\n    https://sourceware.org/git/?p=glibc.git;a=blob_plain;f=LICENSES;hb=HEAD).\n  These licenses are included in the wheel file and apply to the GNU C Library\n  that is dynamically linked.\n\n\nTests\n=====\n\nUse [`pytest`](https://pypi.org/project/pytest). Run with:\n\n```shell\npushd tests/\npytest -v --continue-on-collection-errors .\npopd\n```\n\nTests of Cython modules that were not installed will fail.\nThe code is covered well by tests.\n\n\nLicense\n=======\n[BSD-3](https://opensource.org/licenses/BSD-3-Clause), read file `LICENSE`.\n\n\n[build_img]: https://github.com/tulip-control/dd/actions/workflows/main.yml/badge.svg?branch=main\n[ci]: https://github.com/tulip-control/dd/actions\n"
  },
  {
    "path": "dd/__init__.py",
    "content": "\"\"\"Package of algorithms based on decision diagrams.\"\"\"\ntry:\n    import dd._version as _version\n    __version__ = _version.version\nexcept ImportError:\n    __version__ = None\ntry:\n    import dd.cudd as _bdd\nexcept ImportError:\n    import dd.autoref as _bdd\nBDD = _bdd.BDD\n"
  },
  {
    "path": "dd/_abc.py",
    "content": "\"\"\"Interface specification.\n\nThis specification is implemented by the modules:\n\n- `dd.autoref`\n- `dd.cudd`\n- `dd.cudd_zdd`\n- `dd.sylvan` (partially)\n- `dd.buddy` (partially)\n\"\"\"\n# Copyright 2017 by California Institute of Technology\n# All rights reserved. Licensed under BSD-3.\n#\nimport collections.abc as _abc\nimport typing as _ty\n\n\ndef _literals_of(\n        type_alias:\n            type\n        ) -> set[str]:\n    \"\"\"Return arguments of `type_alias`.\n\n    Recursive computation.\n    Assumes `str` literals.\n    \"\"\"\n    return set(_literals_of_recurse(type_alias))\n\n\ndef _literals_of_recurse(\n        type_alias:\n            type\n        ) -> _abc.Iterable[str]:\n    \"\"\"Yield literals of `type_alias`.\"\"\"\n    args = _ty.get_args(type_alias)\n    literals = set()\n    for arg in args:\n        match arg:\n            case str():\n                yield arg\n            case _:\n                yield from _literals_of_recurse(arg)\n    return literals\n\n\nYes: _ty.TypeAlias = bool\nNat: _ty.TypeAlias = int\n    # ```tla\n    # Nat\n    # ```\nCardinality: _ty.TypeAlias = Nat\nNumberOfBytes: _ty.TypeAlias = Cardinality\nVariableName: _ty.TypeAlias = str\nLevel: _ty.TypeAlias = Nat\nVariableLevels: _ty.TypeAlias = dict[\n    VariableName,\n    Level]\nRef = _ty.TypeVar('Ref')\nAssignment: _ty.TypeAlias = dict[\n    VariableName,\n    bool]\nRenaming: _ty.TypeAlias = dict[\n    VariableName,\n    VariableName]\nFork: _ty.TypeAlias = tuple[\n    Level,\n    Ref | None,\n    Ref | None]\nFormula: _ty.TypeAlias = str\n_UnaryOperatorSymbol: _ty.TypeAlias = _ty.Literal[\n    # negation\n    'not',\n    '~',\n    '!']\nUNARY_OPERATOR_SYMBOLS: _ty.Final = _literals_of(\n    _UnaryOperatorSymbol)\n# These assertions guard against typos in\n# the enumerations.\nif len(UNARY_OPERATOR_SYMBOLS) != 3:\n    raise AssertionError(UNARY_OPERATOR_SYMBOLS)\n_BinaryOperatorSymbol: _ty.TypeAlias = _ty.Literal[\n    # conjunction\n    'and',\n    '/\\\\',\n    '&',\n    '&&',\n    # disjunction\n    'or',\n    r'\\/',\n    '|',\n    '||',\n    # different\n    '#',\n    'xor',\n    '^',\n    # implication\n    '=>',\n    '->',\n    'implies',\n    # equivalence\n    '<=>',\n    '<->',\n    'equiv',\n    # subtraction (i.e., `a /\\ ~ b`)\n    'diff',\n    '-',\n    # quantification\n    r'\\A',\n    'forall',\n    r'\\E',\n    'exists']\nBINARY_OPERATOR_SYMBOLS: _ty.Final = _literals_of(\n    _BinaryOperatorSymbol)\nif len(BINARY_OPERATOR_SYMBOLS) != 23:\n    raise AssertionError(BINARY_OPERATOR_SYMBOLS)\n_TernaryOperatorSymbol: _ty.TypeAlias = _ty.Literal[\n    # ternary conditional\n    # (if-then-else)\n    'ite']\nTERNARY_OPERATOR_SYMBOLS: _ty.Final = _literals_of(\n    _TernaryOperatorSymbol)\nif len(TERNARY_OPERATOR_SYMBOLS) != 1:\n    raise AssertionError(TERNARY_OPERATOR_SYMBOLS)\nBDD_OPERATOR_SYMBOLS: _ty.Final = {\n    *UNARY_OPERATOR_SYMBOLS,\n    *BINARY_OPERATOR_SYMBOLS,\n    *TERNARY_OPERATOR_SYMBOLS}\nif len(BDD_OPERATOR_SYMBOLS) != 3 + 23 + 1:\n    raise AssertionError(BDD_OPERATOR_SYMBOLS)\nOperatorSymbol: _ty.TypeAlias = (\n    _UnaryOperatorSymbol |\n    _BinaryOperatorSymbol |\n    _TernaryOperatorSymbol)\nImageFileType: _ty.TypeAlias = _ty.Literal[\n    'pdf',\n    'png',\n    'svg']\nJSONFileType: _ty.TypeAlias = _ty.Literal[\n    'json']\nPickleFileType: _ty.TypeAlias = _ty.Literal[\n    'pickle']\nBDDFileType: _ty.TypeAlias = (\n    ImageFileType |\n    JSONFileType)\n\n\nclass BDD(_ty.Protocol[Ref]):\n    \"\"\"Shared reduced ordered binary decision diagram.\"\"\"\n\n    vars: VariableLevels\n\n    def __init__(\n            self,\n            levels:\n                dict |\n                None=None\n            ) -> None:\n        ...\n\n    def __eq__(\n            self,\n            other\n            ) -> Yes:\n        \"\"\"Return `True` if `other` has same manager\"\"\"\n\n    def __len__(\n            self\n            ) -> Cardinality:\n        \"\"\"Return number of nodes.\"\"\"\n\n    def __contains__(\n            self,\n            u:\n                Ref\n            ) -> Yes:\n        \"\"\"Return `True` \"\"\"\n\n    def __str__(\n            self\n            ) -> str:\n        return 'Specification of BDD class.'\n\n    def configure(\n            self,\n            **kw\n            ) -> dict[\n                str,\n                _ty.Any]:\n        \"\"\"Read and apply parameter values.\"\"\"\n\n    def statistics(\n            self\n            ) -> dict[\n                str,\n                _ty.Any]:\n        \"\"\"Return BDD manager statistics.\"\"\"\n        # default implementation that offers no info\n        return dict()\n\n    def succ(\n            self,\n            u:\n                Ref\n            ) -> Fork:\n        \"\"\"Return `(level, low, high)` for node `u`.\n\n        The manager uses complemented edges,\n        so `low` and `high` correspond to the rectified `u`.\n        \"\"\"\n\n    def declare(\n            self,\n            *variables:\n                VariableName\n            ) -> None:\n        \"\"\"Add names in `variables` to `self.vars`.\n\n        ```python\n        bdd.declare('x', 'y', 'z')\n        ```\n        \"\"\"\n\n    def var(\n            self,\n            var:\n                VariableName\n            ) -> Ref:\n        \"\"\"Return node for variable named `var`.\"\"\"\n\n    def var_at_level(\n            self,\n            level:\n                Level\n            ) -> VariableName:\n        \"\"\"Return variable with `level`.\"\"\"\n\n    def level_of_var(\n            self,\n            var:\n                VariableName\n            ) -> (\n                Level |\n                None):\n        \"\"\"Return level of `var`, or `None`.\"\"\"\n\n    @property\n    def var_levels(\n            self\n            ) -> VariableLevels:\n        \"\"\"Return mapping from variables to levels.\"\"\"\n\n    def copy(\n            self,\n            u:\n                Ref,\n            other:\n                'BDD'\n            ) -> Ref:\n        \"\"\"Copy operator `u` from `self` to `other` manager.\"\"\"\n\n    def support(\n            self,\n            u:\n                Ref,\n            as_levels:\n                Yes=False\n            ) -> (\n                set[VariableName] |\n                set[Level]):\n        \"\"\"Return variables that node `u` depends on.\n\n        @param as_levels:\n            if `True`, then return variables\n            as integers, insted of strings\n        \"\"\"\n\n    def let(\n            self,\n            definitions:\n                dict[VariableName, VariableName] |\n                Assignment |\n                dict[VariableName, Ref],\n            u:\n                Ref\n            ) -> Ref:\n        \"\"\"Substitute variables in `u`.\n\n        The mapping `definitions` need not have\n        all declared variables as keys.\n        \"\"\"\n\n    def forall(\n            self,\n            variables:\n                _abc.Iterable[\n                    VariableName],\n            u:\n                Ref\n            ) -> Ref:\n        \"\"\"Quantify `variables` in `u` universally.\"\"\"\n\n    def exist(\n            self,\n            variables:\n                _abc.Iterable[\n                    VariableName],\n            u:\n                Ref\n            ) -> Ref:\n        \"\"\"Quantify `variables` in `u` existentially.\"\"\"\n\n    def count(\n            self,\n            u:\n                Ref,\n            nvars:\n                Cardinality |\n                None=None\n            ) -> Cardinality:\n        \"\"\"Return number of models of node `u`.\n\n        @param nvars:\n            number of variables to assume.\n\n            If omitted, then assume those in `support(u)`.\n            If `nvars >= len(support(u))` then the count\n            is multiplied by `2**(nvars-len(support(u)))`,\n            compared to the case `nvars is None`.\n        \"\"\"\n\n    def pick(\n            self,\n            u:\n                Ref,\n            care_vars:\n                set[VariableName] |\n                None=None\n            ) -> (\n                Assignment |\n                None):\n        r\"\"\"Return a single assignment.\n\n        An assignment is a `dict` that maps\n        each variable to a `bool`. Examples:\n\n        ```python\n        >>> u = bdd.add_expr('x')\n        >>> bdd.pick(u)\n        {'x': True}\n\n        >>> u = bdd.add_expr('y')\n        >>> bdd.pick(u)\n        {'y': True}\n\n        >>> u = bdd.add_expr('y')\n        >>> bdd.pick(u, care_vars=['x', 'y'])\n        {'x': False, 'y': True}\n\n        >>> u = bdd.add_expr(r'x \\/ y')\n        >>> bdd.pick(u)\n        {'x': False, 'y': True}\n\n        >>> u = bdd.false\n        >>> bdd.pick(u) is None\n        True\n        ```\n\n        By default, `care_vars = support(u)`.\n        Log a warning if `care_vars < support(u)`.\n\n        Thin wrapper around `pick_iter`.\n        \"\"\"\n        picks = self.pick_iter(u, care_vars)\n        return next(iter(picks), None)\n\n    def pick_iter(\n            self,\n            u:\n                Ref,\n            care_vars:\n                set[VariableName] |\n                None=None\n            ) -> _abc.Iterable[\n                Assignment]:\n        \"\"\"Return iterator over assignments.\n\n        By default, `care_vars = support(u)`.\n        Log a warning if `care_vars < support(u)`.\n\n        CASES:\n\n        1. `None`: return (uniform) assignments that\n           include exactly those variables in `support(u)`\n\n        2. `set`: return (possibly partial) assignments\n           that include at least all bits in `care_vars`\n        \"\"\"\n\n    # def to_bdd(\n    #         self,\n    #         expr):\n    #     raise NotImplementedError('use `add_expr`')\n\n    def add_expr(\n            self,\n            expr:\n                Formula\n            ) -> Ref:\n        \"\"\"Return node for expression `expr`.\n\n        Nodes are created for the BDD that\n        represents the expression `expr`.\n        \"\"\"\n\n    def to_expr(\n            self,\n            u:\n                Ref\n            ) -> Formula:\n        \"\"\"Return a Boolean expression for node `u`.\"\"\"\n\n    def ite(\n            self,\n            g:\n                Ref,\n            u:\n                Ref,\n            v:\n                Ref\n            ) -> Ref:\n        \"\"\"Ternary conditional `IF g THEN u ELSE v`.\n\n        @param g:\n            condition\n        @param u:\n            high\n        @param v:\n            low\n        \"\"\"\n\n    def apply(\n            self,\n            op:\n                OperatorSymbol,\n            u:\n                Ref,\n            v:\n                Ref |\n                None=None,\n            w:\n                Ref |\n                None=None\n            ) -> Ref:\n        r\"\"\"Apply operator `op` to nodes `u`, `v`, `w`.\"\"\"\n\n    def _add_int(\n            self,\n            i:\n                int\n            ) -> Ref:\n        \"\"\"Return node from `i`.\"\"\"\n\n    def cube(\n            self,\n            dvars:\n                Assignment\n            ) -> Ref:\n        \"\"\"Return node for conjunction of literals in `dvars`.\"\"\"\n\n    # TODO: homogeneize i/o API with `dd.cudd`\n    def dump(\n            self,\n            filename:\n                str,\n            roots:\n                dict[str, Ref] |\n                list[Ref] |\n                None=None,\n            filetype:\n                ImageFileType |\n                None=None,\n            **kw\n            ) -> None:\n        \"\"\"Write BDDs to `filename`.\n\n        The file type is inferred from the\n        extension (case insensitive),\n        unless a `filetype` is explicitly given.\n\n        `filetype` can have the values:\n\n          - `'pickle'` for Pickle\n          - `'pdf'` for PDF\n          - `'png'` for PNG\n          - `'svg'` for SVG\n\n        If `filetype is None`, then `filename`\n        must have an extension that matches\n        one of the file types listed above.\n\n        Dump nodes reachable from `roots`.\n        If `roots is None`,\n        then all nodes in the manager are dumped.\n\n        @type roots:\n            - `list` of nodes, or\n            - for Pickle: `dict` that maps\n              names to nodes\n        \"\"\"\n\n    def load(\n            self,\n            filename:\n                str,\n            levels:\n                Yes=True\n            ) -> (\n                dict[str, Ref] |\n                list[Ref]):\n        \"\"\"Load nodes from Pickle file `filename`.\n\n        If `levels is True`,\n        then load variables at the same levels.\n        Otherwise, add missing variables.\n\n        @return:\n            roots of the loaded BDDs\n        @rtype:\n            depends on the contents of the file,\n            either:\n            - `dict` that maps names\n              to nodes, or\n            - `list` of nodes\n        \"\"\"\n\n    @property\n    def false(\n            self\n            ) -> Ref:\n        \"\"\"Return Boolean constant false.\"\"\"\n\n    @property\n    def true(\n            self\n            ) -> Ref:\n        \"\"\"Return Boolean constant true.\"\"\"\n\n\ndef reorder(\n        bdd:\n            BDD,\n        order:\n            VariableLevels |\n            None=None\n        ) -> None:\n    \"\"\"Apply Rudell's sifting algorithm to `bdd`.\n\n    @param order:\n        reorder to this specific order,\n        if `None` then invoke group sifting\n    \"\"\"\n\n\nclass Operator(_ty.Protocol):\n    \"\"\"Convenience wrapper for edges returned by `BDD`.\"\"\"\n\n    def __init__(\n            self,\n            node,\n            bdd\n            ) -> None:\n        self.bdd: BDD\n        self.manager: object\n        self.node: object\n\n    def __hash__(\n            self\n            ) -> int:\n        return self.node\n\n    def to_expr(\n            self\n            ) -> Formula:\n        \"\"\"Return Boolean expression of function.\"\"\"\n\n    def __int__(\n            self\n            ) -> int:\n        \"\"\"Return integer ID of node.\n\n        To invert this method call `BDD._add_int`.\n        \"\"\"\n\n    def __str__(\n            self\n            ) -> str:\n        \"\"\"Return string form of node as `@INT`.\n\n        \"INT\" is an integer that depends on\n        the implementation. For example \"@54\".\n        The integer value is `int(self)`.\n\n        The integer value is recognized by the method\n        `BDD._add_int` of the same manager that the\n        node belongs to.\n        \"\"\"\n        return f'@{int(self)}'\n\n    def __len__(\n            self\n            ) -> Cardinality:\n        \"\"\"Number of nodes reachable from this node.\"\"\"\n\n    def __del__(\n            self\n            ) -> None:\n        r\"\"\"Dereference node in manager.\"\"\"\n\n    def __eq__(\n            self,\n            other\n            ) -> Yes:\n        r\"\"\"`|= self \\equiv other`.\n\n        Return `False` if `other is None`.\n        \"\"\"\n\n    def __ne__(\n            self,\n            other\n            ) -> Yes:\n        r\"\"\"`~ |= self \\equiv other`.\n\n        Return `True` if `other is None`.\n        \"\"\"\n\n    def __lt__(\n            self,\n            other\n            ) -> Yes:\n        r\"\"\"`(|= self => other) /\\ ~ |= self \\equiv other`.\"\"\"\n\n    def __le__(\n            self,\n            other\n            ) -> Yes:\n        \"\"\"`|= self => other`.\"\"\"\n\n    def __invert__(\n            self\n            ) -> 'Operator':\n        \"\"\"Negation `~ self`.\"\"\"\n\n    def __and__(\n            self,\n            other\n            ) -> 'Operator':\n        r\"\"\"Conjunction `self /\\ other`.\"\"\"\n\n    def __or__(\n            self,\n            other\n            ) -> 'Operator':\n        r\"\"\"Disjunction `self \\/ other`.\"\"\"\n\n    def __xor__(\n            self,\n            other\n            ) -> 'Operator':\n        \"\"\"Exclusive-or `self ^ other`.\"\"\"\n\n    def implies(\n            self,\n            other\n            ) -> 'Operator':\n        \"\"\"Logical implication `self => other`.\"\"\"\n\n    def equiv(\n            self,\n            other\n            ) -> 'Operator':\n        r\"\"\"Logical equivalence `self <=> other`.\n\n        The result is *different* from `__eq__`:\n\n        - Logical equivalence is the Boolean function that is\n          `TRUE` for models for which both `self` and `other`\n          are `TRUE`, and `FALSE` otherwise.\n\n        - BDD equality (`__eq__`) is the Boolean function\n          that results from universal quantification of the\n          logical equivalence, over all declared variables.\n\n        In other words:\n\n        \"A <=> B\" versus \"\\A x, y, ..., z:  A <=> B\"\n        or, from a metatheoretic viewpoint:\n        \"A <=> B\" versus \"|= A <=> B\"\n\n        In the metatheory, [[A <=> B]] (`equiv`) is different from\n        [[A]] = [[B]] (`__eq__`).\n\n        Also, `equiv` differs from `__eq__` in that it returns a BDD\n        as `Function`, instead of `bool`.\n        \"\"\"\n\n    @property\n    def level(\n            self\n            ) -> Level:\n        \"\"\"Level where this node currently is.\"\"\"\n\n    @property\n    def var(\n            self\n            ) -> (\n                VariableName |\n                None):\n        \"\"\"Variable at level where this node is.\"\"\"\n\n    @property\n    def low(\n            self\n            ) -> '''(\n                Operator |\n                None\n                )''':\n        \"\"\"Return \"else\" node.\"\"\"\n\n    @property\n    def high(\n            self\n            ) -> '''(\n                Operator |\n                None\n                )''':\n        \"\"\"Return \"then\" node.\"\"\"\n\n    @property\n    def ref(\n            self\n            ) -> Cardinality:\n        \"\"\"Sum of reference counts of node and its negation.\"\"\"\n\n    @property\n    def negated(\n            self\n            ) -> Yes:\n        \"\"\"Return `True` if `self` is a complemented edge.\"\"\"\n\n    @property\n    def support(\n            self\n            ) -> set[\n                VariableName]:\n        \"\"\"Return variables in support.\"\"\"\n\n    def let(\n            self,\n            **definitions: '''(\n                VariableName |\n                Operator |\n                bool\n                )'''\n            ) -> 'Operator':\n        return self.bdd.let(definitions, self)\n\n    def exist(\n            self,\n            *variables:\n                VariableName\n            ) -> 'Operator':\n        return self.bdd.exist(variables, self)\n\n    def forall(\n            self,\n            *variables:\n                VariableName\n            ) -> 'Operator':\n        return self.bdd.forall(variables, self)\n\n    def pick(\n            self,\n            care_vars:\n                set[VariableName] |\n                None=None\n            ) -> (\n                Assignment |\n                None):\n        return self.bdd.pick(self, care_vars)\n\n    def count(\n            self,\n            nvars:\n                Cardinality |\n                None=None\n            ) -> Cardinality:\n        return self.bdd.count(self, nvars)\n"
  },
  {
    "path": "dd/_copy.py",
    "content": "\"\"\"Utilities for transferring BDDs.\"\"\"\n# Copyright 2016-2018 by California Institute of Technology\n# All rights reserved. Licensed under 3-clause BSD.\n#\nimport collections.abc as _abc\nimport contextlib as _ctx\nimport json\nimport os\nimport shelve\nimport shutil\nimport typing as _ty\n\nimport dd._abc\nimport dd._utils as _utils\n\n\nSHELVE_DIR: _ty.Final = '__shelve__'\n_Yes: _ty.TypeAlias = dd._abc.Yes\n\n\nclass _BDD(\n        dd._abc.BDD[dd._abc.Ref],\n        _ty.Protocol):\n    \"\"\"BDD context.\"\"\"\n\n    def add_var(\n            self,\n            var:\n                str,\n            level:\n                dd._abc.Level |\n                None=None\n            ) -> dd._abc.Level:\n        ...\n\n    def _top_cofactor(\n            self,\n            u:\n                dd._abc.Ref,\n            level:\n                dd._abc.Level\n            ) -> tuple[\n                dd._abc.Ref,\n                dd._abc.Ref]:\n        ...\n\n    def reorder(\n            self,\n            var_order:\n                dict[\n                    dd._abc.VariableName,\n                    dd._abc.Level] |\n                None=None\n            ) -> None:\n        ...\n\n    def find_or_add(\n            self,\n            level:\n                dd._abc.Level,\n            u:\n                dd._abc.Ref,\n            v:\n                dd._abc.Ref\n            ) -> dd._abc.Ref:\n        ...\n\n    def incref(\n            self,\n            node:\n                dd._abc.Ref\n            ) -> None:\n        ...\n\n    def decref(\n            self,\n            node:\n                dd._abc.Ref,\n            **kw\n            ) -> None:\n        ...\n\n    def assert_consistent(\n            self\n            ) -> None:\n        ...\n\n\nRef = _ty.TypeVar('Ref')\n\n\nclass _Ref(_ty.Protocol):\n    var: str | None\n    level: int\n    low: '_Ref | None'\n    high: '_Ref | None'\n    bdd: _BDD\n    negated: _Yes\n    ref: int\n\n    def __int__(\n            self\n            ) -> int:\n        ...\n\n    def __invert__(\n            self\n            ) -> '_Ref':\n        ...\n\n\nclass _Shelf(\n        _ctx.AbstractContextManager,\n        _ty.Protocol):\n    \"\"\"Used for type checking.\"\"\"\n\n    # `_abc.MutableMapping` cannot be\n    # in the bases, because not\n    # itself a `_ty.Protocol`.\n\n    def __setitem__(\n            self,\n            key,\n            value\n            ) -> None:\n        ...\n\n    def __getitem__(\n            self,\n            key):\n        ...\n\n    def __iter__(\n            self\n            ) -> _abc.Iterable:\n        ...\n\n    def __contains__(\n            self,\n            item\n            ) -> _Yes:\n        ...\n\n\ndef _open_shelf(\n        name:\n            str\n        ) -> _Shelf:\n    \"\"\"Wrapper for type-checking.\"\"\"\n    return shelve.open(name)\n\n\ndef copy_vars(\n        source:\n            dd._abc.BDD,\n        target\n        ) -> None:\n    \"\"\"Copy variables, preserving levels.\"\"\"\n    for var in source.vars:\n        level = source.level_of_var(var)\n        target.add_var(var, level=level)\n\n\ndef copy_bdds_from(\n        roots:\n            _abc.Iterable[_Ref],\n        target:\n            _BDD\n        ) -> list[_Ref]:\n    \"\"\"Copy BDDs in `roots` to manager `target`.\"\"\"\n    cache = dict()\n    return [\n        copy_bdd(u, target, cache)\n        for u in roots]\n\n\ndef copy_bdd(\n        root:\n            _Ref,\n        target:\n            _BDD,\n        cache:\n            dict |\n            None=None\n        ) -> _Ref:\n    \"\"\"Copy BDD with `root` to manager `target`.\n\n    @param target:\n        BDD or ZDD context\n    @param cache:\n        for memoizing results\n    \"\"\"\n    if cache is None:\n        cache = dict()\n    return _copy_bdd(root, target, cache)\n\n\ndef _copy_bdd(\n        u:\n            _Ref,\n        bdd:\n            _BDD,\n        cache:\n            dict\n        ) -> _Ref:\n    \"\"\"Recurse to copy node `u` to `bdd`.\"\"\"\n    # terminal ?\n    if u == u.bdd.true:\n        return bdd.true\n    # could be handled via cache,\n    # but frequent case\n    if u == u.bdd.false:\n        return bdd.false\n    # rectify\n    z = _flip(u, u)\n    # non-terminal\n    # memoized ?\n    k = int(z)\n    if k in cache:\n        r = cache[k]\n        return _flip(r, u)\n    # recurse\n    low = _copy_bdd(u.low, bdd, cache)\n    high = _copy_bdd(u.high, bdd, cache)\n    # canonicity\n    # if low.negated != u.low.negated:\n    #     raise AssertionError((low, u.low))\n    # if high.negated:\n    #     raise AssertionError(high)\n    # add node\n    g = bdd.var(u.var)\n    r = bdd.ite(g, high, low)\n    # if r.negated:\n    #     raise AssertionError(r)\n    # memoize\n    cache[k] = r\n    return _flip(r, u)\n\n\ndef _flip(\n        r:\n            _Ref,\n        u:\n            _Ref\n        ) -> _Ref:\n    \"\"\"Negate `r` if `u` is negated.\n\n    Else return `r`.\n    \"\"\"\n    return ~ r if u.negated else r\n\n\ndef copy_zdd(\n        root:\n            _Ref,\n        target:\n            _BDD,\n        cache:\n            dict |\n            None=None\n        ) -> _Ref:\n    \"\"\"Copy ZDD with `root` to manager `target`.\n\n    @param target:\n        BDD or ZDD context\n    @param cache:\n        for memoizing results\n    \"\"\"\n    if cache is None:\n        cache = dict()\n    level = 0\n    return _copy_zdd(level, root, target, cache)\n\n\ndef _copy_zdd(\n        level:\n            int,\n        u:\n            _Ref,\n        target:\n            _BDD,\n        cache:\n            dict[int, _Ref]\n        ) -> _Ref:\n    \"\"\"Recurse to copy node `u` to `target`.\"\"\"\n    src: _BDD = u.bdd\n    # terminal ?\n    if u == src.false:\n        return target.false\n    if level == len(src.vars):\n        return target.true\n    # memoized ?\n    k = int(u)\n    if k in cache:\n        return cache[k]\n    # recurse\n    v, w = src._top_cofactor(u, level)\n    low = _copy_zdd(\n        level + 1, v, target, cache)\n    high = _copy_zdd(\n        level + 1, w, target, cache)\n    # add node\n    var = src.var_at_level(level)\n    g = target.var(var)\n    r = target.ite(g, high, low)\n    # memoize\n    cache[k] = r\n    return r\n\n\ndef dump_json(\n        nodes:\n            dict[str, Ref] |\n            list[Ref],\n        file_name:\n            str\n        ) -> None:\n    \"\"\"Write reachable nodes to JSON file.\n\n    Writes the nodes that are reachable from\n    the roots in `nodes` to the JSON file\n    named `file_name`.\n\n    Also dumps the variable names and the\n    variable order, to the same JSON file.\n\n    @param nodes:\n        maps names to roots of\n        the BDDs that will be written to\n        the JSON file\n    \"\"\"\n    if not nodes:\n        raise ValueError(\n            'Need nonempty `nodes` as roots.')\n    tmp_fname = os.path.join(\n        SHELVE_DIR, 'temporary_shelf')\n    os.makedirs(SHELVE_DIR)\n    try:\n        with _open_shelf(tmp_fname) as cache,\\\n                open(file_name, 'w') as fd:\n            _dump_json(nodes, fd, cache)\n    finally:\n        # `shelve` file naming\n        # depends on context\n        shutil.rmtree(SHELVE_DIR)\n\n\ndef _dump_json(\n        nodes:\n            dict[str, _Ref] |\n            list[_Ref],\n        fd:\n            _ty.TextIO,\n        cache:\n            _abc.MutableMapping[str, bool]\n        ) -> None:\n    \"\"\"Dump BDD as JSON to file `fd`.\n\n    Use `cache` to keep track of\n    visited nodes.\n    \"\"\"\n    fd.write('{')\n    _dump_bdd_info(nodes, fd)\n    for u in _utils.values_of(nodes):\n        _dump_bdd(u, fd, cache)\n    fd.write('\\n}\\n')\n\n\ndef _dump_bdd_info(\n        nodes:\n            dict[str, _Ref] |\n            list[_Ref],\n        fd):\n    \"\"\"Dump variable levels and roots.\n\n    @param nodes:\n        maps names to roots of BDDs\n    \"\"\"\n    roots = _utils.map_container(_node_to_int, nodes)\n    u = next(iter(_utils.values_of(nodes)))\n    bdd = u.bdd\n    var_level = {\n        var: bdd.level_of_var(var)\n        for var in bdd.vars}\n    info = (\n        '\\n\"level_of_var\": {level}'\n        ',\\n\"roots\": {roots}').format(\n            level=json.dumps(var_level),\n            roots=json.dumps(roots))\n    fd.write(info)\n\n\ndef _dump_bdd(\n        u:\n            _Ref,\n        fd:\n            _ty.TextIO,\n        cache:\n            _abc.MutableMapping[str, bool]\n        ) -> (\n            int |\n            str):\n    \"\"\"Recursive step of dumping nodes.\"\"\"\n    # terminal ?\n    if u == u.bdd.true:\n        return '\"T\"'\n    if u == u.bdd.false:\n        return '\"F\"'\n    # rectify\n    z = _flip(u, u)\n    # non-terminal\n    # dumped ?\n    k = int(z)\n    if str(k) in cache:\n        return -k if u.negated else k\n    # recurse\n    low = _dump_bdd(u.low, fd, cache)\n    high = _dump_bdd(u.high, fd, cache)\n    # dump node\n    s = f',\\n\"{k}\": [{u.level}, {low}, {high}]'\n    fd.write(s)\n    # record as dumped\n    cache[str(k)] = True\n    return -k if u.negated else k\n\n\ndef load_json(\n        file_name:\n            str,\n        bdd,\n        load_order:\n            _Yes=False\n        ) -> (\n            dict[str, _Ref] |\n            list[_Ref]):\n    \"\"\"Add BDDs from JSON `file_name` to `bdd`.\n\n    @param load_order:\n        if `True`,\n        then load variable order\n        from `file_name`.\n    @return:\n        - keys (or indices) are names\n        - values are BDD roots\n    \"\"\"\n    tmp_fname = os.path.join(\n        SHELVE_DIR, 'temporary_shelf')\n    os.makedirs(SHELVE_DIR)\n    try:\n        with _open_shelf(tmp_fname) as cache,\\\n                open(file_name, 'r') as fd:\n            nodes = _load_json(\n                fd, bdd, load_order, cache)\n    finally:\n        shutil.rmtree(SHELVE_DIR)\n    return nodes\n\n\ndef _load_json(\n        fd:\n            _abc.Iterable[str],\n        bdd,\n        load_order:\n            _Yes,\n        cache:\n            _abc.MutableMapping[str, int]\n        ) -> (\n            dict[str, _Ref] |\n            list[_Ref]):\n    \"\"\"Load BDDs from JSON file `fd` to `bdd`.\"\"\"\n    context = dict(load_order=load_order)\n    # if the variable order is going to be loaded,\n    # then turn off dynamic reordering,\n    # because it can change the order midway,\n    # which would not return the loaded order,\n    # and can also cause failure of\n    # the assertion below\n    if load_order:\n        old_reordering = bdd.configure(\n            reordering=False)\n    for line in fd:\n        d = _parse_line(line)\n        _store_line(d, bdd, context, cache)\n    roots = context['roots']\n    if hasattr(roots, 'items'):\n        roots = {\n            name: _node_from_int(k, bdd, cache)\n            for name, k in roots.items()}\n    else:\n        roots = [\n            _node_from_int(k, bdd, cache)\n            for k in roots]\n    # rm refs to cached nodes\n    for uid in cache:\n        u = _node_from_int(int(uid), bdd, cache)\n        if u.ref < 2:\n            raise AssertionError(u.ref)\n            # +1 ref due to `incref` in `_make_node`\n            # +1 ref due to the `_node_from_int`\n            #   call for `u`\n        if load_order and u.ref < 3:\n            raise AssertionError(u.ref)\n            # +1 ref due to `incref` in `_make_node`\n            # +1 ref due to either:\n            #   - being a successor node\n            #   - being a root node\n            #     (thus referenced in `roots` above)\n            # +1 ref due to the `_node_from_int`\n            #   call for `u`\n        bdd.decref(u, _direct=True)\n            # this module is unusual,\n            # in that `incref` and `decref` need\n            # to be called on different `Function`\n            # instances for the same node\n    bdd.assert_consistent()\n    if load_order:\n        bdd.configure(\n            reordering=old_reordering)\n    return roots\n\n\ndef _parse_line(\n        line:\n            str\n        ) -> (\n            dict |\n            None):\n    \"\"\"Parse JSON from `line`.\"\"\"\n    line = line.rstrip()\n    if line == '{' or line == '}':\n        return None\n    if line.endswith(','):\n        line = line.rstrip(',')\n    return json.loads('{' + line + '}')\n\n\ndef _store_line(\n        d:\n            dict |\n            None,\n        bdd:\n            _BDD,\n        context:\n            dict,\n        cache:\n            _abc.MutableMapping[str, int]\n        ) -> None:\n    \"\"\"Interpret data in `d`.\"\"\"\n    if d is None:\n        return\n    order = d.get('level_of_var')\n    if order is not None:\n        order = {\n            str(k): v\n            for k, v in order.items()}\n        bdd.declare(*order)\n        context['level_of_var'] = order\n        context['var_at_level'] = {\n            v: k for k, v in order.items()}\n        if context['load_order']:\n            bdd.reorder(order)\n        return\n    roots = d.get('roots')\n    if roots is not None:\n        context['roots'] = roots\n        return\n    _make_node(d, bdd, context, cache)\n\n\ndef _make_node(\n        d:\n            dict,\n        bdd:\n            _BDD,\n        context:\n            dict,\n        cache:\n            _abc.MutableMapping[str, int]\n        ) -> None:\n    \"\"\"Create a new node in `bdd` from `d`.\"\"\"\n    (uid, (level, low_id, high_id)), = d.items()\n    k, level = map(int, (uid, level))\n    if k <= 0:\n        raise AssertionError(k)\n    if level < 0:\n        raise AssertionError(level)\n    low_id = _decode_node(low_id)\n    high_id = _decode_node(high_id)\n    if str(k) in cache:\n        return\n    low = _node_from_int(low_id, bdd, cache)\n    high = _node_from_int(high_id, bdd, cache)\n    var = context['var_at_level'][level]\n    if context['load_order']:\n        u = bdd.find_or_add(var, low, high)\n    else:\n        g = bdd.var(var)\n        u = bdd.ite(g, high, low)\n    if u.negated:\n        raise AssertionError(u)\n    # memoize\n    cache[str(k)] = int(u)\n    bdd.incref(u)\n\n\ndef _decode_node(\n        s:\n            str\n        ) -> int:\n    \"\"\"Map `s` to node-like number.\"\"\"\n    match s:\n        case 'F':\n            return -1\n        case 'T':\n            return 1\n    return int(s)\n\n\ndef _node_from_int(\n        uid:\n            int,\n        bdd:\n            _BDD,\n        cache:\n            _abc.Mapping[str, int]\n        ) -> _Ref:\n    \"\"\"Return `bdd` node represented by `uid`.\"\"\"\n    if uid == -1:\n        return bdd.false\n    elif uid == 1:\n        return bdd.true\n    # not constant\n    k = cache[str(abs(uid))]\n    u = bdd._add_int(k)\n    return ~ u if uid < 0 else u\n\n\ndef _node_to_int(\n        u:\n            _Ref\n        ) -> int:\n    \"\"\"Return numeric representation of `u`.\"\"\"\n    z = _flip(u, u)\n    k = int(z)\n    return -k if u.negated else k\n"
  },
  {
    "path": "dd/_parser.py",
    "content": "\"\"\"Construct BDD nodes from quantified Boolean formulae.\"\"\"\n# Copyright 2015 by California Institute of Technology\n# All rights reserved. Licensed under BSD-3.\n#\nimport collections.abc as _abc\nimport logging\nimport typing as _ty\n\nimport astutils\n\n\n_TABMODULE: _ty.Final[str] =\\\n    'dd._expr_parser_state_machine'\n\n\nclass _Token(_ty.Protocol):\n    type: str\n    value: str\n\n\nclass Lexer(astutils.Lexer):\n    \"\"\"Lexer for Boolean formulae.\"\"\"\n\n    def __init__(\n            self\n            ) -> None:\n        self.reserved = {\n            'ite':\n                'ITE',\n            'False':\n                'FALSE',\n            'True':\n                'TRUE',\n            'FALSE':\n                'FALSE',\n            'TRUE':\n                'TRUE'}\n        self.delimiters = [\n            'LPAREN',\n            'RPAREN',\n            'COMMA']\n        self.operators = [\n            'NOT',\n            'AND',\n            'OR',\n            'XOR',\n            'IMPLIES',\n            'EQUIV',\n            'EQUALS',\n            'MINUS',\n            'DIV',\n            'AT',\n            'COLON',\n            'FORALL',\n            'EXISTS',\n            'RENAME']\n        self.misc = [\n            'NAME',\n            'NUMBER']\n        super().__init__()\n\n    def t_NAME(\n            self,\n            token:\n                _Token\n            ) -> _Token:\n        r\"\"\"\n        [A-Za-z_]\n        [A-Za-z0-9_']*\n        \"\"\"\n        token.type = self.reserved.get(\n            token.value, 'NAME')\n        return token\n\n    def t_AND(\n            self,\n            token:\n                _Token\n            ) -> _Token:\n        r\"\"\"\n          \\&\\&\n        | \\&\n        | /\\\\\n        \"\"\"\n        token.value = '&'\n        return token\n\n    def t_OR(\n            self,\n            token:\n                _Token\n            ) -> _Token:\n        r\"\"\"\n          \\|\\|\n        | \\|\n        | \\\\/\n        \"\"\"\n        token.value = '|'\n        return token\n\n    def t_NOT(\n            self,\n            token:\n                _Token\n            ) -> _Token:\n        r\"\"\"\n          \\~\n        | !\n        \"\"\"\n        token.value = '!'\n        return token\n\n    def t_IMPLIES(\n            self,\n            token:\n                _Token\n            ) -> _Token:\n        r\"\"\"\n          =>\n        | \\->\n        \"\"\"\n        token.value = '=>'\n        return token\n\n    def t_EQUIV(\n            self,\n            token:\n                _Token\n            ) -> _Token:\n        r\"\"\"\n          <=>\n        | <\\->\n        \"\"\"\n        token.value = '<->'\n        return token\n\n    t_XOR = r'''\n          \\#\n        | \\^\n        '''\n    t_EQUALS = r' = '\n    t_LPAREN = r' \\( '\n    t_RPAREN = r' \\) '\n    t_MINUS = r' \\- '\n    t_NUMBER = r' \\d+ '\n    t_COMMA = r' , '\n    t_COLON = r' : '\n    t_FORALL = r' \\\\ A '\n    t_EXISTS = r' \\\\ E '\n    t_RENAME = r' \\\\ S '\n    t_DIV = r' / '\n    t_AT = r' @ '\n    t_ignore = ''.join(['\\x20', '\\t'])\n\n    def t_trailing_comment(\n            self,\n            token:\n                _Token\n            ) -> None:\n        r' \\\\ \\* .* '\n        return None\n\n    def t_doubly_delimited_comment(\n            self,\n            token:\n                _Token\n            ) -> None:\n        r\"\"\"\n        \\( \\*\n        [\\s\\S]*?\n        \\* \\)\n        \"\"\"\n        return None\n\n    def t_newline(\n            self,\n            token\n            ) -> None:\n        r' \\n+ '\n\n\n_ParserResult = _ty.TypeVar('_ParserResult')\n\n\nclass _ParserProtocol(\n        _ty.Protocol,\n        _ty.Generic[\n            _ParserResult]):\n    \"\"\"Parser internal interface.\"\"\"\n\n    def _apply(\n            self,\n            operator:\n                str,\n            *operands:\n                _ty.Any\n            ) -> _ParserResult:\n        ...\n\n    def _add_var(\n            self,\n            name:\n                str\n            ) -> _ParserResult:\n        ...\n\n    def _add_int(\n            self,\n            numeric_literal:\n                str\n            ) -> _ParserResult:\n        ...\n\n    def _add_bool(\n            self,\n            bool_literal:\n                str\n            ) -> _ParserResult:\n        ...\n\n\nclass Parser(\n        astutils.Parser,\n        _ParserProtocol):\n    \"\"\"Parser for Boolean formulae.\"\"\"\n\n    def __init__(\n            self\n            ) -> None:\n        tabmodule_is_defined = (\n            hasattr(self, 'tabmodule') and\n            self.tabmodule)\n        if not tabmodule_is_defined:\n            self.tabmodule = _TABMODULE\n        self.start = 'expr'\n        # low to high\n        self.precedence = (\n            ('left',\n                'COLON'),\n            ('left',\n                'EQUIV'),\n            ('left',\n                'IMPLIES'),\n            ('left',\n                'MINUS'),\n            ('left',\n                'XOR'),\n            ('left',\n                'OR'),\n            ('left',\n                'AND'),\n            ('left',\n                'EQUALS'),\n            ('right',\n                'NOT'),\n            )\n        kw = dict()\n        kw.setdefault('lexer', Lexer())\n        super().__init__(**kw)\n\n    def _apply(\n            self,\n            operator:\n                str,\n            *operands:\n                _ty.Any):\n        \"\"\"Return syntax tree of application.\"\"\"\n        if operator == r'\\S':\n            match operands:\n                case subs, expr:\n                    pass\n                case _:\n                    raise AssertionError(operands)\n            return self.nodes.Operator(\n                operator, expr, subs)\n        return self.nodes.Operator(\n            operator, *operands)\n\n    def _add_var(\n            self,\n            name:\n                str):\n        \"\"\"Return syntax tree for identifier.\"\"\"\n        return self.nodes.Terminal(\n            name, 'var')\n\n    def _add_int(\n            self,\n            numeric_literal:\n                str):\n        \"\"\"Return syntax tree of given index.\"\"\"\n        return self.nodes.Terminal(\n            numeric_literal, 'num')\n\n    def _add_bool(\n            self,\n            bool_literal:\n                str):\n        \"\"\"Return syntax tree for Boolean.\"\"\"\n        return self.nodes.Terminal(\n            bool_literal, 'bool')\n\n    def p_bool(\n            self,\n            p:\n                list\n            ) -> None:\n        \"\"\"expr : TRUE\n                | FALSE\n        \"\"\"\n        p[0] = self._add_bool(p[1])\n\n    def p_node(\n            self,\n            p:\n                list\n            ) -> None:\n        \"\"\"expr : AT number\"\"\"\n        p[0] = p[2]\n\n    def p_number(\n            self,\n            p:\n                list\n            ) -> None:\n        \"\"\"number : NUMBER\"\"\"\n        p[0] = self._add_int(p[1])\n\n    def p_negative_number(\n            self,\n            p:\n                list\n            ) -> None:\n        \"\"\"number : MINUS NUMBER \"\"\"\n        numeric_literal = f'{p[1]}{p[2]}'\n        p[0] = self._add_int(numeric_literal)\n\n    def p_var(\n            self,\n            p:\n                list\n            ) -> None:\n        \"\"\"expr : name\"\"\"\n        p[0] = self._add_var(p[1].value)\n\n    def p_unary(\n            self,\n            p:\n                list\n            ) -> None:\n        \"\"\"expr : NOT expr\"\"\"\n        p[0] = self._apply(\n            p[1], p[2])\n\n    def p_binary(\n            self,\n            p:\n                list\n            ) -> None:\n        \"\"\"expr : expr AND expr\n                | expr OR expr\n                | expr XOR expr\n                | expr IMPLIES expr\n                | expr EQUIV expr\n                | expr EQUALS expr\n                | expr MINUS expr\n        \"\"\"\n        p[0] = self._apply(\n            p[2], p[1], p[3])\n\n    def p_ternary_conditional(\n            self,\n            p:\n                list\n            ) -> None:\n        (\"\"\"expr : ITE LPAREN \"\"\"\n         \"\"\"             expr COMMA \"\"\"\n         \"\"\"             expr COMMA \"\"\"\n         \"\"\"             expr RPAREN\"\"\")\n        p[0] = self._apply(\n            p[1], p[3], p[5], p[7])\n\n    def p_quantifier(\n            self,\n            p:\n                list\n            ) -> None:\n        \"\"\"expr : EXISTS names COLON expr\n                | FORALL names COLON expr\n        \"\"\"\n        p[0] = self._apply(\n            p[1], p[2], p[4])\n\n    def p_rename(\n            self,\n            p:\n                list\n            ) -> None:\n        \"\"\"expr : RENAME subs COLON expr\"\"\"\n        p[0] = self._apply(\n            p[1], p[2], p[4])\n\n    def p_substitutions_iter(\n            self,\n            p:\n                list\n            ) -> None:\n        \"\"\"subs : subs COMMA sub\"\"\"\n        u = p[1]\n        u.append(p[3])\n        p[0] = u\n\n    def p_substitutions_end(\n            self,\n            p:\n                list\n            ) -> None:\n        \"\"\"subs : sub\"\"\"\n        p[0] = [p[1]]\n\n    def p_substitution(\n            self,\n            p:\n                list\n            ) -> None:\n        \"\"\"sub : name DIV name\"\"\"\n        new = p[1]\n        old = p[3]\n        p[0] = (old, new)\n\n    def p_names_iter(\n            self,\n            p:\n                list\n            ) -> None:\n        \"\"\"names : names COMMA name\"\"\"\n        u = p[1]\n        u.append(p[3])\n        p[0] = u\n\n    def p_names_end(\n            self,\n            p:\n                list\n            ) -> None:\n        \"\"\"names : name\"\"\"\n        p[0] = [p[1]]\n\n    def p_name(\n            self,\n            p:\n                list\n            ) -> None:\n        \"\"\"name : NAME\"\"\"\n        p[0] = self.nodes.Terminal(\n            p[1], 'var')\n\n    def p_paren(\n            self,\n            p:\n                list\n            ) -> None:\n        \"\"\"expr : LPAREN expr RPAREN\"\"\"\n        p[0] = p[2]\n\n\n_Ref = _ty.TypeVar('_Ref')\n\n\nclass _BDD(_ty.Protocol[\n        _Ref]):\n    \"\"\"Interface of BDD context.\"\"\"\n\n    @property\n    def false(\n            self\n            ) -> _Ref:\n        ...\n\n    @property\n    def true(\n            self\n            ) -> _Ref:\n        ...\n\n    def var(\n            self,\n            name:\n                str\n            ) -> _Ref:\n        ...\n\n    def apply(\n            self,\n            op:\n                str,\n            u:\n                _Ref,\n            v:\n                _Ref |\n                None=None,\n            w:\n                _Ref |\n                None=None\n            ) -> _Ref:\n        ...\n\n    def quantify(\n            self,\n            u:\n                _Ref,\n            qvars:\n                set[str],\n            forall:\n                bool=False\n            ) -> _Ref:\n        ...\n\n    def rename(\n            self,\n            u:\n                _Ref,\n            renaming:\n                _abc.Mapping\n            ) -> _Ref:\n        ...\n\n    def _add_int(\n            self,\n            number:\n                int\n            ) -> _Ref:\n        ...\n\n\nclass _Translator(Parser):\n    \"\"\"Parser for Boolean formulas.\"\"\"\n\n    def __init__(\n            self\n            ) -> None:\n        super().__init__()\n        self._reset_state()\n\n    def parse(\n            self,\n            expression:\n                str,\n            bdd:\n                _BDD[_Ref]\n            ) -> _Ref:\n        \"\"\"Return BDD of `expression`.\n\n        The returned BDD is stored in\n        the BDD manager `bdd`.\n        \"\"\"\n        self._bdd = bdd\n        u = super().parse(expression)\n        self._reset_state()\n        return u\n\n    def _reset_state(\n            self\n            ) -> None:\n        \"\"\"Set default attribute values.\"\"\"\n        self._bdd = None\n        has_lr_stack = (\n            self.parser is not None and\n            hasattr(self.parser, 'statestack') and\n            hasattr(self.parser, 'symstack'))\n        if not has_lr_stack:\n            return\n        self.parser.restart()\n            # Avoid references to BDD nodes\n            # remaining in the LR stack,\n            # because this side-effect would\n            # change the reference-counts.\n\n    def _add_bool(\n            self,\n            bool_literal:\n                str):\n        \"\"\"Return BDD for Boolean values.\"\"\"\n        value = bool_literal.lower()\n        if value not in {'false', 'true'}:\n            raise ValueError(value)\n        return getattr(self._bdd, value)\n\n    def _add_int(\n            self,\n            numeric_literal:\n                str):\n        \"\"\"Return BDD with given index.\"\"\"\n        number = int(numeric_literal)\n        return self._bdd._add_int(number)\n\n    def _add_var(\n            self,\n            name:\n                str):\n        \"\"\"Return BDD for variable `name`.\"\"\"\n        return self._bdd.var(name)\n\n    def _apply(\n            self,\n            operator:\n                str,\n            *operands:\n                _ty.Any):\n        \"\"\"Return BDD from applying `operator`.\"\"\"\n        match operator:\n            case r'\\A' | r'\\E':\n                names, expr = operands\n                names = {\n                    x.value\n                    for x in names}\n                forall = (operator == r'\\A')\n                return self._bdd.quantify(\n                    expr, names,\n                    forall=forall)\n            case r'\\S':\n                subs, expr = operands\n                renaming = {\n                    k.value: v.value\n                    for k, v in subs}\n                return self._bdd.rename(\n                    expr, renaming)\n        return self._bdd.apply(\n            operator, *operands)\n\n\n_parsers = dict()\n\n\ndef add_expr(\n        expression:\n            str,\n        bdd:\n            _BDD[_Ref]\n        ) -> _Ref:\n    \"\"\"Return `bdd` node for `expression`.\n\n    Creates in `bdd` a node that represents\n    `expression`, and returns this node.\n    \"\"\"\n    if 'boolean' not in _parsers:\n        _parsers['boolean'] = _Translator()\n    translator = _parsers['boolean']\n    return translator.parse(expression, bdd)\n\n\ndef _rewrite_tables(\n        outputdir:\n            str='./'\n        ) -> None:\n    \"\"\"Recache state machine of parser.\"\"\"\n    astutils.rewrite_tables(\n        Parser, _TABMODULE, outputdir)\n    astutils.rewrite_tables(\n        _Translator, _TABMODULE, outputdir)\n\n\ndef _main(\n        ) -> None:\n    \"\"\"Recompute parser state machine.\n\n    Cache the state machine in a file.\n    Configure logging.\n    \"\"\"\n    log = logging.getLogger('astutils')\n    log.setLevel('DEBUG')\n    log.addHandler(logging.StreamHandler())\n    _rewrite_tables()\n\n\nif __name__ == '__main__':\n    _main()\n"
  },
  {
    "path": "dd/_utils.py",
    "content": "\"\"\"Convenience functions.\"\"\"\n# Copyright 2017-2018 by California Institute of Technology\n# All rights reserved. Licensed under 3-clause BSD.\n#\nimport collections as _cl\nimport collections.abc as _abc\nimport os\nimport shlex as _sh\nimport subprocess as _sbp\nimport textwrap as _tw\nimport types\nimport typing as _ty\n\nimport dd._abc\n\ntry:\n    import networkx as _nx\nexcept ImportError as error:\n    _nx = None\n    _nx_error = error\n\n\nif _nx is not None:\n    MultiDiGraph: _ty.TypeAlias = _nx.MultiDiGraph\n\n\n# The mapping from values of argument `op` of\n# `__richcmp__()` of Cython objects,\n# to the corresponding operator symbols.\n# Constants are defined in `cpython.object`.\n_CY_SYMBOLS: _ty.Final = {\n    2: '==',\n    3: '!=',\n    0: '<',\n    1: '<=',\n    4: '>',\n    5: '>='}\n\n\ndef import_module(\n        module_name:\n            str\n        ) -> types.ModuleType:\n    \"\"\"Return module with `module_name`, if present.\n\n    Raise `ImportError` otherwise.\n    \"\"\"\n    modules = dict(\n        networkx=_nx)\n    if module_name in modules:\n        return modules[module_name]\n    errors = dict(\n        networkx=_nx_error)\n    raise errors[module_name]\n\n\ndef print_var_levels(\n        bdd\n        ) -> None:\n    \"\"\"Print `bdd` variables ordered by level.\"\"\"\n    n = len(bdd.vars)\n    levels = [\n        bdd.var_at_level(level)\n        for level in range(n)]\n    print(\n        'Variable order (starting at level 0):\\n'\n        f'{levels}')\n\n\ndef var_counts(\n        bdd\n        ) -> str:\n    \"\"\"Return levels and numbers of variables, CUDD indices.\n\n    @type bdd:\n        `dd.cudd.BDD` or\n        `dd.cudd_zdd.ZDD`\n    \"\"\"\n    n_declared_vars = len(bdd.vars)\n    n_cudd_vars = bdd._number_of_cudd_vars()\n    return _tw.dedent(f'''\n        There are:\n        {n_cudd_vars} variable indices in CUDD,\n        {n_declared_vars} declared variables in {bdd!r}.\n\n        So the set of levels of the declared variables\n        is not a contiguous range of integers.\n\n        This can occur when specific levels have been\n        given to `{type(bdd)}.add_var()`.\n\n        The declared variables and their levels are:\n        {bdd.var_levels}\n        ''')\n\n\ndef contiguous_levels(\n        callable:\n            str,\n        bdd\n        ) -> str:\n    \"\"\"Return requirement about contiguous levels.\n\n    @type bdd:\n        `dd.cudd.BDD` or\n        `dd.cudd_zdd.ZDD`\n    \"\"\"\n    return _tw.dedent(f'''\n        The callable `{callable}()` requires that\n        the number of variable indices in CUDD, and\n        the number of declared variables in {bdd!r}\n        be equal.\n        ''')\n\n\ndef raise_runtimerror_about_ref_count(\n        ref_count_lb:\n            int,\n        name:\n            str,\n        class_name:\n            str\n        ) -> _ty.NoReturn:\n    \"\"\"Raise `RuntimeError` about reference count lower bound.\n\n    Call this function when an unexpected nonpositive\n    lower bound on a node's reference count is detected\n    for a `Function` instance.\n\n    @param ref_count_lb:\n        lower bound on the reference count of\n        the node that the `Function` instance points to.\n        ```tla\n        ASSUME\n            ref_count_lb <= 0\n        ```\n    @param name:\n        to mention as location where\n        the error was detected. For example:\n        ```python\n        'method `dd.cudd.BDD.decref`'\n        ```\n    @param class_name:\n        to mention as name of\n        the class of the object where the value\n        `ref_count_lb` was found. For example:\n        ```python\n        '`dd.cudd.Function`'\n        ```\n    \"\"\"\n    if ref_count_lb > 0:\n        raise ValueError(ref_count_lb)\n    raise RuntimeError(\n        f'The {name} requires '\n        'that `u._ref > 0` '\n        f'(where `u` is an instance of {class_name}). '\n        'This ensures that deallocated memory '\n        'in CUDD will not be accessed. The current '\n        f'value of attribute `_ref` is:\\n{ref_count_lb}\\n'\n        'For more information read the docstring of '\n        f'the class {class_name}.')\n\n\n\n@_ty.overload\ndef map_container(\n        mapper:\n            _abc.Callable,\n        container:\n            _abc.Mapping\n        ) -> dict:\n    ...\n\n\n@_ty.overload\ndef map_container(\n        mapper:\n            _abc.Callable,\n        container:\n            _abc.Iterable\n        ) -> list:\n    ...\n\n\ndef map_container(\n        mapper,\n        container):\n    \"\"\"Map `container`, using `mapper()`.\n\n    If `container` is a sequence,\n    then map each item.\n\n    If `container` is a mapping of\n    keys to values, then map each value.\n    \"\"\"\n    if isinstance(container, _abc.Mapping):\n        return _map_values(mapper, container)\n    return list(map(mapper, container))\n\n\ndef _map_values(\n        mapper:\n            _abc.Callable,\n        kv:\n            _abc.Mapping\n        ) -> dict:\n    \"\"\"Map each value of `kv` using `mapper()`.\n\n    The keys of `kv` remain unchanged.\n    \"\"\"\n    return {k: mapper(v) for k, v in kv.items()}\n\n\ndef values_of(\n        container:\n            _abc.Mapping |\n            _abc.Collection\n        ) -> _abc.Iterable:\n    \"\"\"Return container values.\n\n    @return:\n        - `container.values()` if\n          `container` is a mapping\n        - `container` otherwise\n    \"\"\"\n    if isinstance(container, _abc.Mapping):\n        return container.values()\n    return container\n\n\ndef total_memory(\n        ) -> (\n            int |\n            None):\n    \"\"\"Return number of bytes of memory.\n\n    Requires that:\n    - `SC_PAGE_SIZE` and\n    - `SC_PHYS_PAGES`\n    be readable via `os.sysconf()`.\n    \"\"\"\n    names = os.sysconf_names\n    has_both = (\n        'SC_PAGE_SIZE' in names and\n        'SC_PHYS_PAGES' in names)\n    if not has_both:\n        print(\n            'skipping check that '\n            'initial memory estimate fits '\n            'in available memory of system, '\n            \"because either `'SC_PAGE_SIZE'` or \"\n            \"`'SC_PHYS_PAGES'` undefined in \"\n            '`os.sysconf_names`.')\n        return None\n    page_size = os.sysconf('SC_PAGE_SIZE')\n    n_pages = os.sysconf('SC_PHYS_PAGES')\n    both_defined = (\n        page_size >= 0 and\n        n_pages >= 0)\n    if not both_defined:\n        return None\n    return page_size * n_pages\n\n\n_OPERATOR_MAP: _ty.Final = dict(\n    bdd=dict(\n        unary=dd._abc.UNARY_OPERATOR_SYMBOLS,\n        binary=dd._abc.BINARY_OPERATOR_SYMBOLS,\n        ternary=dd._abc.TERNARY_OPERATOR_SYMBOLS,\n        all=dd._abc.BDD_OPERATOR_SYMBOLS))\n\n\ndef assert_operator_arity(\n        op:\n            str,\n        v:\n            object |\n            None,\n        w:\n            object |\n            None,\n        diagram_type:\n            _ty.Literal[\n                'bdd']\n        ) -> None:\n    \"\"\"Raise `ValueError` if unexpected values.\n\n    Asserts:\n    - `op` is an operator symbol\n    - `v` is `None` if `op` is a unary operator\n    - `w` is `None` if `op` has arity <= 2\n    \"\"\"\n    operators = _OPERATOR_MAP[diagram_type]\n    if op not in operators['all']:\n        raise ValueError(\n            f'Unknown operator: \"{op}\"')\n    if op in operators['unary']:\n        if v is not None:\n            raise ValueError(\n                f'`v is not None`, but: {v}')\n        if w is not None:\n            raise ValueError(\n                f'`w is not None`, but: {w}')\n    elif op in operators['binary']:\n        if v is None:\n            raise ValueError(\n                '`v is None`')\n        if w is not None:\n            raise ValueError(\n                f'`w is not None`, but: {w}')\n    elif op in operators['ternary']:\n        if v is None:\n            raise ValueError(\n                '`v is None`')\n        if w is None:\n            raise ValueError(\n                '`w is None`')\n\n\n_GraphType: _ty.TypeAlias = _ty.Literal[\n    'digraph',\n    'graph',\n    'subgraph']\nDOT_FILE_TYPES: _ty.Final = {\n    'pdf', 'svg', 'png', 'dot'}\n\n\nclass DotGraph:\n    def __init__(\n            self,\n            graph_type:\n                _GraphType='digraph',\n            rank:\n                str |\n                None=None\n            ) -> None:\n        \"\"\"A DOT graph.\"\"\"\n        self.graph_type = graph_type\n        self.rank = rank\n        self.nodes = _cl.defaultdict(dict)\n        self.edges = _cl.defaultdict(list)\n        self.subgraphs = list()\n\n    def add_node(\n            self,\n            node,\n            **kw\n            ) -> None:\n        \"\"\"Add node with attributes `kw`.\n\n        If node exists, update its attributes.\n        \"\"\"\n        self.nodes[node].update(kw)\n\n    def add_edge(\n            self,\n            start_node,\n            end_node,\n            **kw\n            ) -> None:\n        \"\"\"Add edge with attributes `kw`.\n\n        Multiple edges can exist between the same nodes.\n        \"\"\"\n        self.edges[start_node, end_node].append(kw)\n\n    def to_dot(\n            self,\n            graph_type:\n                _GraphType |\n                None=None\n            ) -> str:\n        \"\"\"Return DOT code.\"\"\"\n        subgraphs = ''.join(\n            g.to_dot(\n                graph_type='subgraph')\n            for g in self.subgraphs)\n        def format_attributes(\n                attr\n                ) -> str:\n            \"\"\"Return formatted assignment.\"\"\"\n            return ', '.join(\n                f'{k}=\"{v}\"'\n                for k, v in attr.items())\n        def format_node(\n                u,\n                attr\n                ) -> str:\n            \"\"\"Return DOT code for node.\"\"\"\n            attributes = format_attributes(attr)\n            return f'{u} [{attributes}];'\n        def format_edge(\n                u,\n                v,\n                attr\n                ) -> str:\n            \"\"\"Return DOT code for edge.\"\"\"\n            attributes = format_attributes(attr)\n            return f'{u} -> {v} [{attributes}];'\n        nodes = '\\n'.join(\n            format_node(u, attr)\n            for u, attr in self.nodes.items())\n        edges = list()\n        for (u, v), attrs in self.edges.items():\n            for attr in attrs:\n                edge = format_edge(u, v, attr)\n                edges.append(edge)\n        edges = '\\n'.join(edges)\n        indent_level = 4 * '\\x20'\n        def fmt(\n                text:\n                    str\n                ) -> str:\n            \"\"\"Return indented text.\"\"\"\n            newline = '\\n' if text else ''\n            return newline + _tw.indent(\n                text,\n                prefix=4 * indent_level)\n        nodes = fmt(nodes)\n        edges = fmt(edges)\n        subgraphs = fmt(subgraphs)\n        if graph_type is None:\n            graph_type = self.graph_type\n        if self.rank is None:\n            rank = ''\n        else:\n            rank = f'rank = {self.rank}'\n        return _tw.dedent(f'''\n            {graph_type} {{\n                {rank}{nodes}{edges}{subgraphs}\n            }}\n            ''')\n\n    def dump(\n            self,\n            filename:\n                str,\n            filetype:\n                str,\n            **kw\n            ) -> None:\n        \"\"\"Write to file.\"\"\"\n        if filetype not in DOT_FILE_TYPES:\n            raise ValueError(\n                f'Unknown file type \"{filetype}\" '\n                f'for \"{filename}\"')\n        dot_code = self.to_dot()\n        if filetype == 'dot':\n            with open(filename, 'w') as fd:\n                fd.write(dot_code)\n            return\n        dot = _sh.split(f'''\n            dot\n                -T{filetype}\n                -o '{filename}'\n            ''')\n        _sbp.run(\n            dot,\n            encoding='utf8',\n            input=dot_code,\n            capture_output=True,\n            check=True)\n"
  },
  {
    "path": "dd/autoref.py",
    "content": "\"\"\"Wraps `dd.bdd` to automate reference counting.\n\nFor function docstrings, refer to `dd.bdd`.\n\"\"\"\n# Copyright 2015 by California Institute of Technology\n# All rights reserved. Licensed under BSD-3.\n#\nimport collections.abc as _abc\nimport logging\nimport typing as _ty\nimport warnings\n\nimport dd._abc\nimport dd._copy as _copy\nimport dd._utils as _utils\nimport dd.bdd as _bdd\n\n\nlog = logging.getLogger(__name__)\n\n\n_Yes: _ty.TypeAlias = dd._abc.Yes\n_Cardinality: _ty.TypeAlias = dd._abc.Cardinality\n_VariableName: _ty.TypeAlias = dd._abc.VariableName\n_Level: _ty.TypeAlias = dd._abc.Level\n_VariableLevels: _ty.TypeAlias = dd._abc.VariableLevels\n_Ref: _ty.TypeAlias = _ty.Union['Function']\n_MaybeRef: _ty.TypeAlias = '''(\n    _Ref |\n    None\n    )'''\n_Fork: _ty.TypeAlias = '''(\n    tuple[\n        _Level,\n        _MaybeRef,\n        _MaybeRef]\n    )'''\n_Assignment: _ty.TypeAlias = dd._abc.Assignment\n_Renaming: _ty.TypeAlias = dd._abc.Renaming\n_Formula: _ty.TypeAlias = dd._abc.Formula\n\n\nclass BDD(dd._abc.BDD[_Ref]):\n    \"\"\"Shared ordered binary decision diagram.\n\n    It takes and returns `Function` instances,\n    which automate reference counting.\n\n    Attributes:\n\n      - `vars`: `dict` mapping `variables` to `int` levels\n          Do not assign the `dict` itself.\n\n    For docstrings, refer to methods of `dd.bdd.BDD`,\n    with the difference that `Function`s replace nodes\n    as arguments and returned types.\n    \"\"\"\n    # omitted docstrings are inheritted from `super()`\n\n    def __init__(\n            self,\n            levels:\n                _VariableLevels |\n                None=None):\n        manager = _bdd.BDD(levels)\n        self._bdd = manager\n        self.vars: _VariableLevels = manager.vars\n\n    def __eq__(\n            self,\n            other:\n                'BDD'\n            ) -> _Yes:\n        if not isinstance(other, BDD):\n            raise NotImplementedError\n        return (self._bdd is other._bdd)\n\n    def __len__(\n            self\n            ) -> _Cardinality:\n        return len(self._bdd)\n\n    def __contains__(\n            self,\n            u:\n                _Ref\n            ) -> _Yes:\n        if self is not u.bdd:\n            raise ValueError('`self is not u.bdd`')\n        return u.node in self._bdd\n\n    def __str__(\n            self\n            ) -> str:\n        return (\n            'Binary decision diagram (`dd.bdd.BDD` wrapper):\\n'\n            '------------------------\\n'\n            f'\\t {len(self.vars)} BDD variables\\n'\n            f'\\t {len(self)} nodes\\n')\n\n    def _wrap(\n            self,\n            u:\n                int\n            ) -> _Ref:\n        \"\"\"Return reference to node `u`.\n\n        References can be thought of also\n        as edges.\n\n        @param u:\n            node in `self._bdd`\n        \"\"\"\n        if u not in self._bdd:\n            raise ValueError(u)\n        return Function(u, self)\n\n    def configure(\n            self,\n            **kw\n            ) -> dict[\n                str,\n                _ty.Any]:\n        return self._bdd.configure(**kw)\n\n    def succ(\n            self,\n            u\n            ) -> _Fork:\n        i, v, w = self._bdd.succ(u.node)\n        def wrap(\n                node:\n                    int |\n                    None\n                ) -> _MaybeRef:\n            match node:\n                case None:\n                    return None\n                case int():\n                    return self._wrap(node)\n            raise AssertionError(node)\n        return i, wrap(v), wrap(w)\n\n    def incref(\n            self,\n            u:\n                _Ref\n            ) -> None:\n        self._bdd.incref(u.node)\n\n    def decref(\n            self,\n            u:\n                _Ref,\n            **kw\n            ) -> None:\n        self._bdd.decref(u.node)\n\n    def declare(\n            self,\n            *variables:\n                _VariableName\n            ) -> None:\n        for var in variables:\n            self.add_var(var)\n\n    def add_var(\n            self,\n            var:\n                _VariableName,\n            level:\n                _Level |\n                None=None\n            ) -> _Level:\n        return self._bdd.add_var(var, level=level)\n\n    def var(\n            self,\n            var:\n                _VariableName\n            ) -> _Ref:\n        r = self._bdd.var(var)\n        return self._wrap(r)\n\n    def var_at_level(\n            self,\n            level:\n                _Level\n            ) -> _VariableName:\n        return self._bdd.var_at_level(level)\n\n    def level_of_var(\n            self,\n            var:\n                _VariableName\n            ) -> _Level:\n        return self._bdd.level_of_var(var)\n\n    @property\n    def var_levels(\n            self\n            ) -> _VariableLevels:\n        return self._bdd.var_levels\n\n    def reorder(\n            self,\n            var_order:\n                _VariableLevels |\n                None=None\n            ) -> None:\n        reorder(self, var_order)\n\n    def copy(\n            self,\n            u:\n                _Ref,\n            other:\n                'BDD'\n            ) -> _Ref:\n        if u not in self:\n            raise ValueError(u)\n        if self is other:\n            log.warning('copying node to same manager')\n            return u\n        r = self._bdd.copy(u.node, other._bdd)\n        return other._wrap(r)\n\n    def support(\n            self,\n            u:\n                _Ref,\n            as_levels:\n                _Yes=False\n            ) -> set[_VariableName]:\n        if u not in self:\n            raise ValueError(u)\n        return self._bdd.support(u.node, as_levels)\n\n    def let(\n            self,\n            definitions:\n                _Renaming |\n                _Assignment |\n                dict[_VariableName, _Ref],\n            u:\n                _Ref\n            ) -> _Ref:\n        if u not in self:\n            raise ValueError(u)\n        if not definitions:\n            return u\n        var = next(iter(definitions))\n        value = definitions[var]\n        match value:\n            case str() | bool():\n                d = definitions\n            case Function():\n                def node_of(\n                        ref\n                        ) -> int:\n                    if isinstance(ref, Function):\n                        return ref.node\n                    raise ValueError(\n                        'Expected homogeneous type '\n                        'for `dict` values.')\n                d = {\n                    var: node_of(value)\n                    for var, value in\n                        definitions.items()}\n            case _:\n                raise TypeError(value)\n        r = self._bdd.let(d, u.node)\n        return self._wrap(r)\n\n    def quantify(\n            self,\n            u:\n                _Ref,\n            qvars:\n                _abc.Iterable[\n                    _VariableName],\n            forall:\n                _Yes=False\n            ) -> _Ref:\n        if u not in self:\n            raise ValueError(u)\n        r = self._bdd.quantify(u.node, qvars, forall)\n        return self._wrap(r)\n\n    def forall(\n            self,\n            qvars:\n                _abc.Iterable[_VariableName],\n            u:\n                _Ref\n            ) -> _Ref:\n        return self.quantify(u, qvars, forall=True)\n\n    def exist(\n            self,\n            qvars:\n                _abc.Iterable[\n                    _VariableName],\n            u:\n                _Ref\n            ) -> _Ref:\n        return self.quantify(u, qvars, forall=False)\n\n    def ite(\n            self,\n            g:\n                _Ref,\n            u:\n                _Ref,\n            v:\n                _Ref\n            ) -> _Ref:\n        if g not in self:\n            raise ValueError(g)\n        if u not in self:\n            raise ValueError(u)\n        if v not in self:\n            raise ValueError(v)\n        r = self._bdd.ite(g.node, u.node, v.node)\n        return self._wrap(r)\n\n    def find_or_add(\n            self,\n            var:\n                _VariableName,\n            low:\n                _Ref,\n            high:\n                _Ref\n            ) -> _Ref:\n        \"\"\"Return node `IF var THEN high ELSE low`.\"\"\"\n        level = self.level_of_var(var)\n        r = self._bdd.find_or_add(level, low.node, high.node)\n        return self._wrap(r)\n\n    def count(\n            self,\n            u:\n                _Ref,\n            nvars:\n                _Cardinality |\n                None=None\n            ) -> _Cardinality:\n        if u not in self:\n            raise ValueError(u)\n        return self._bdd.count(u.node, nvars)\n\n    def pick_iter(\n            self,\n            u:\n                _Ref,\n            care_vars:\n                set[_VariableName] |\n                None=None\n            ) -> _abc.Iterable[\n                _Assignment]:\n        if u not in self:\n            raise ValueError(u)\n        return self._bdd.pick_iter(u.node, care_vars)\n\n    def add_expr(\n            self,\n            e:\n                _Formula\n            ) -> _Ref:\n        r = self._bdd.add_expr(e)\n        return self._wrap(r)\n\n    def to_expr(\n            self,\n            u:\n                _Ref\n            ) -> _Formula:\n        if u not in self:\n            raise ValueError(u)\n        return self._bdd.to_expr(u.node)\n\n    def apply(\n            self,\n            op:\n                dd._abc.OperatorSymbol,\n            u:\n                _Ref,\n            v:\n                _MaybeRef\n                =None,\n            w:\n                _MaybeRef\n                =None\n            ) -> _Ref:\n        if u not in self:\n            raise ValueError(u)\n        if v is None and w is not None:\n            raise ValueError(w)\n        if v is not None and v not in self:\n            raise ValueError(v)\n        if w is not None and w not in self:\n            raise ValueError(w)\n        if v is None:\n            r = self._bdd.apply(op, u.node)\n        elif w is None:\n            r = self._bdd.apply(op, u.node, v.node)\n        else:\n            r = self._bdd.apply(op, u.node, v.node, w.node)\n        return self._wrap(r)\n\n    def _add_int(\n            self,\n            i:\n                int\n            ) -> _Ref:\n        r = self._bdd._add_int(i)\n        return self._wrap(r)\n\n    def cube(\n            self,\n            dvars:\n                _Assignment\n            ) -> _Ref:\n        r = self._bdd.cube(dvars)\n        return self._wrap(r)\n\n    def collect_garbage(\n            self\n            ) -> None:\n        \"\"\"Recursively remove nodes with zero reference count.\"\"\"\n        self._bdd.collect_garbage()\n\n    def dump(\n            self,\n            filename:\n                str,\n            roots:\n                dict[str, _Ref] |\n                list[_Ref] |\n                None=None,\n            filetype:\n                dd._abc.BDDFileType |\n                dd._abc.PickleFileType |\n                None=None,\n            **kw\n            ) -> None:\n        \"\"\"Write BDDs to `filename`.\n\n        The file type is inferred from the\n        extension (case insensitive),\n        unless a `filetype` is explicitly given.\n\n        `filetype` can have the values:\n\n          - `'pickle'` for Pickle\n          - `'pdf'` for PDF\n          - `'png'` for PNG\n          - `'svg'` for SVG\n          - `'json'` for JSON\n\n        If `filetype is None`, then `filename`\n        must have an extension that matches\n        one of the file types listed above.\n\n        Dump nodes reachable from `roots`.\n        If `roots is None`,\n        then all nodes in the manager are dumped.\n\n        Dumping a JSON file requires that `roots`\n        be nonempty.\n\n        @type roots:\n            - `list` of nodes, or\n            - for JSON or Pickle:\n              `dict` that maps names\n              to nodes\n        \"\"\"\n        # The method's docstring is a slight modification\n        # of the docstring of the method `dd._abc.BDD.dump`.\n        if filetype is None:\n            name = filename.lower()\n            if name.endswith('.pdf'):\n                filetype = 'pdf'\n            elif name.endswith('.png'):\n                filetype = 'png'\n            elif name.endswith('.svg'):\n                filetype = 'svg'\n            elif name.endswith('.dot'):\n                filetype = 'dot'\n            elif name.endswith('.p'):\n                filetype = 'pickle'\n            elif name.endswith('.json'):\n                filetype = 'json'\n            else:\n                raise ValueError(\n                    'cannot infer file type '\n                    'from extension of file '\n                    f'name \"{filename}\"')\n        if filetype == 'json':\n            if roots is None:\n                raise ValueError(roots)\n            _copy.dump_json(roots, filename)\n            return\n        elif (filetype != 'pickle' and\n                filetype not in _utils.DOT_FILE_TYPES):\n            raise ValueError(filetype)\n        if roots is not None:\n            def mapper(u):\n                return u.node\n            roots = _utils.map_container(\n                mapper, roots)\n        self._bdd.dump(\n            filename,\n            roots=roots,\n            filetype=filetype)\n\n    def load(\n            self,\n            filename:\n                str,\n            levels:\n                _Yes=True\n            ) -> (\n                dict[str, _Ref] |\n                list[_Ref]):\n        \"\"\"Load nodes from Pickle or JSON file `filename`.\n\n        If `levels is True`,\n        then load variables at the same levels.\n        Otherwise, add missing variables.\n\n        @return:\n            roots of the loaded BDDs\n        @rtype:\n            depends on the contents of the file,\n            either:\n            - `dict` that maps names\n              to nodes, or\n            - `list` of nodes\n        \"\"\"\n        # This method's docstring is a slight\n        # modification of the docstring of\n        # the method `dd._abc.BDD.dump`.\n        name = filename.lower()\n        if name.endswith('.p'):\n            return self._load_pickle(\n                filename, levels=levels)\n        elif name.endswith('.json'):\n            nodes = _copy.load_json(filename, self)\n            def check(\n                    node\n                    ) -> Function:\n                if isinstance(node, Function):\n                    return node\n                raise AssertionError(node)\n            match nodes:\n                case dict():\n                    return {\n                        k: check(v)\n                        for k, v in nodes.items()}\n                case list():\n                    return list(map(check, nodes))\n                case _:\n                    raise AssertionError(nodes)\n        else:\n            raise ValueError(\n                f'Unknown file type of \"{filename}\"')\n\n    def _load_pickle(\n            self,\n            filename:\n                str,\n            levels:\n                _Yes=True\n            ) -> (\n                dict[str, _Ref] |\n                list[_Ref]):\n        roots = self._bdd.load(filename, levels=levels)\n        return _utils.map_container(self._wrap, roots)\n\n    def assert_consistent(\n            self\n            ) -> None:\n        self._bdd.assert_consistent()\n\n    @property\n    def false(\n            self\n            ) -> _Ref:\n        u = self._bdd.false\n        return self._wrap(u)\n\n    @property\n    def true(\n            self\n            ) ->_Ref:\n        u = self._bdd.true\n        return self._wrap(u)\n\n\ndef image(\n        trans:\n            _Ref,\n        source:\n            _Ref,\n        rename:\n            _Renaming,\n        qvars:\n            set[_VariableName],\n        forall:\n            _Yes=False\n        ) -> _Ref:\n    if trans.bdd is not source.bdd:\n        raise ValueError(\n            (trans.bdd, source.bdd))\n    u = _bdd.image(\n        trans.node, source.node, rename,\n        qvars, trans.manager, forall)\n    return trans.bdd._wrap(u)\n\n\ndef preimage(\n        trans:\n            _Ref,\n        target:\n            _Ref,\n        rename:\n            _Renaming,\n        qvars:\n            set[_VariableName],\n        forall:\n            _Yes=False\n        ) -> _Ref:\n    if trans.bdd is not target.bdd:\n        raise ValueError(\n            (trans.bdd, target.bdd))\n    u = _bdd.preimage(\n        trans.node, target.node, rename,\n        qvars, trans.manager, forall)\n    return trans.bdd._wrap(u)\n\n\ndef reorder(\n        bdd:\n            BDD,\n        order:\n            _VariableLevels |\n            None=None\n        ) -> None:\n    \"\"\"Apply Rudell's sifting algorithm to `bdd`.\"\"\"\n    _bdd.reorder(bdd._bdd, order=order)\n\n\ndef copy_vars(\n        source:\n            BDD,\n        target:\n            BDD\n        ) -> None:\n    _copy.copy_vars(source._bdd, target._bdd)\n\n\ndef copy_bdd(\n        u:\n            _Ref,\n        target:\n            BDD\n        ) -> _Ref:\n    r = _bdd.copy_bdd(u.node, u.manager, target._bdd)\n    return target._wrap(r)\n\n\nclass Function(dd._abc.Operator):\n    r\"\"\"Convenience wrapper for edges returned by `BDD`.\n\n    ```python\n    import dd.autoref\n\n    bdd = dd.autoref.BDD()\n    bdd.declare('x', 'y')\n    nd = bdd._bdd.add_expr(r'x /\\ y')\n        # `nd` is an integer\n        # `bdd._bdd` is an instance of the\n        # class `dd.bdd.BDD`\n    u = _bdd.Function(nd, bdd)\n    ```\n\n    Attributes:\n\n    - `node`: `int` that describes edge (signed node)\n    - `bdd`: `dd.autoref.BDD` instance that node belongs to\n    - `manager`: `dd.bdd.BDD` instance that node belongs to\n\n    Operations are valid only between functions with\n    the same `BDD` in `Function.bdd`.\n\n    After all references to a `Function` have been deleted,\n    the reference count of its associated node is decremented.\n    To explicitly release a `Function` instance, invoke `del f`.\n\n    The design here is inspired by the PyEDA package.\n    \"\"\"\n\n    def __init__(\n            self,\n            node:\n                int,\n            bdd:\n                BDD\n            ) -> None:\n        if node not in bdd._bdd:\n            raise ValueError(node)\n        self.bdd = bdd\n        self.manager = bdd._bdd\n        self.node = node\n        self.manager.incref(node)\n\n    def __hash__(\n            self\n            ) -> int:\n        return self.node\n\n    def to_expr(\n            self\n            ) -> _Formula:\n        \"\"\"Return Boolean expression of function.\"\"\"\n        return self.manager.to_expr(self.node)\n\n    def __int__(\n            self\n            ) -> int:\n        return self.node\n\n    def __len__(\n            self\n            ) -> _Cardinality:\n        return len(self.manager.descendants([self.node]))\n\n    @property\n    def dag_size(\n            self\n            ) -> _Cardinality:\n        return len(self)\n\n    def __del__(\n            self\n            ) -> None:\n        \"\"\"Decrement reference count of `self.node` in `self.bdd`.\"\"\"\n        if self.node is None:\n            return\n        node = self.node\n        self.node = None\n        self.manager.decref(node)\n\n    def __eq__(\n            self,\n            other\n            ) -> _Yes:\n        if other is None:\n            return False\n        if not isinstance(other, Function):\n            raise NotImplementedError\n        if self.bdd is not other.bdd:\n            raise ValueError((self.bdd, other.bdd))\n        return self.node == other.node\n\n    def __ne__(\n            self,\n            other\n            ) -> _Yes:\n        if other is None:\n            return True\n        if not isinstance(other, Function):\n            raise NotImplementedError\n        if self.bdd is not other.bdd:\n            raise ValueError((self.bdd, other.bdd))\n        return not (self == other)\n\n    def __le__(\n            self,\n            other\n            ) -> _Yes:\n        if not isinstance(other, Function):\n            raise NotImplementedError\n        return (other | ~ self) == self.bdd.true\n\n    def __lt__(\n            self,\n            other\n            ) -> _Yes:\n        if not isinstance(other, Function):\n            raise NotImplementedError\n        return self <= other and self != other\n\n    def __invert__(\n            self\n            ) -> _Ref:\n        return self._apply('not', other=None)\n\n    def __and__(\n            self,\n            other:\n                _Ref\n            ) -> _Ref:\n        return self._apply('and', other)\n\n    def __or__(\n            self,\n            other:\n                _Ref\n            ) -> _Ref:\n        return self._apply('or', other)\n\n    def __xor__(\n            self,\n            other:\n                _Ref\n            ) -> _Ref:\n        return self._apply('xor', other)\n                \n    def implies(\n            self,\n            other:\n                _Ref\n            ) -> _Ref:\n        return self._apply('implies', other)\n\n    def equiv(\n            self,\n            other:\n                _Ref\n            ) -> _Ref:\n        return self._apply('equiv', other)\n\n    def _apply(\n            self,\n            op:\n                dd._abc.OperatorSymbol,\n            other:\n                _MaybeRef\n            ) -> _Ref:\n        \"\"\"Return result of operation `op` with `other`.\"\"\"\n        # unary op ?\n        if other is None:\n            u = self.manager.apply(op, self.node)\n        else:\n            if self.bdd is not other.bdd:\n                raise ValueError((self.bdd, other.bdd))\n            u = self.manager.apply(op, self.node, other.node)\n        return Function(u, self.bdd)\n\n    @property\n    def level(\n            self\n            ) -> _Level:\n        i, _, _ = self.manager._succ[abs(self.node)]\n        return i\n\n    @property\n    def var(\n            self\n            ) -> (\n                _VariableName |\n                None):\n        i, low, _ = self.manager._succ[abs(self.node)]\n        if low is None:\n            return None\n        return self.manager.var_at_level(i)\n\n    @property\n    def low(\n            self\n            ) -> '''(\n                _Ref |\n                None\n                )''':\n        _, v, _ = self.manager._succ[abs(self.node)]\n        if v is None:\n            return None\n        return Function(v, self.bdd)\n\n    @property\n    def high(\n            self\n            ) -> '''(\n                _Ref |\n                None\n                )''':\n        _, _, w = self.manager._succ[abs(self.node)]\n        if w is None:\n            return None\n        return Function(w, self.bdd)\n\n    @property\n    def ref(\n            self\n            ) -> _Cardinality:\n        return self.manager._ref[abs(self.node)]\n\n    @property\n    def negated(\n            self\n            ) -> _Yes:\n        return self.node < 0\n\n    @property\n    def support(\n            self\n            ) -> set[_VariableName]:\n        return self.manager.support(self.node)\n"
  },
  {
    "path": "dd/bdd.py",
    "content": "\"\"\"Ordered binary decision diagrams.\n\n\nReferences\n==========\n\nRandal E. Bryant\n    \"Graph-based algorithms for Boolean function manipulation\"\n    IEEE Transactions on Computers\n    Volume C-35, No. 8, August, 1986, pages 677--690\n\nKarl S. Brace, Richard L. Rudell, Randal E. Bryant\n    \"Efficient implementation of a BDD package\"\n    27th ACM/IEEE Design Automation Conference (DAC), 1990\n    pages 40--45\n\nRichard Rudell\n    \"Dynamic variable ordering for\n    ordered binary decision diagrams\"\n    IEEE/ACM International Conference on\n    Computer-Aided Design (ICCAD), 1993\n    pages 42--47\n\nChristel Baier and Joost-Pieter Katoen\n    \"Principles of model checking\"\n    MIT Press, 2008\n    Section 6.7, pages 381--421\n\nFabio Somenzi\n    \"Binary decision diagrams\"\n    Calculational system design, Vol.173\n    NATO Science Series F: Computer and systems sciences\n    pages 303--366, IOS Press, 1999\n\nHenrik R. Andersen\n    \"An introduction to binary decision diagrams\"\n    Lecture notes for \"Efficient Algorithms and Programs\", 1999\n    The IT University of Copenhagen\n\"\"\"\n# Copyright 2014 by California Institute of Technology\n# All rights reserved. Licensed under BSD-3.\n#\nimport collections.abc as _abc\nimport functools as _ft\nimport inspect\nimport logging\nimport pickle\nimport pprint as _pp\nimport sys\nimport typing as _ty\nimport warnings\n\nimport dd._abc\nimport dd._parser as _parser\nimport dd._utils as _utils\n\n\nlogger = logging.getLogger(__name__)\nREORDER_STARTS = 100\nREORDER_FACTOR = 2\nGROWTH_FACTOR = 2\n\n\ndef _request_reordering(\n        bdd:\n            'BDD'\n        ) -> None:\n    \"\"\"Raise `NeedsReordering` if `len(bdd)` >= threshold.\"\"\"\n    if bdd._last_len is None:\n        return\n    if len(bdd) >= REORDER_FACTOR * bdd._last_len:\n        raise _NeedsReordering()\n\n\n_Ret = _ty.TypeVar('_Ret')\n_CallablePR: _ty.TypeAlias = _abc.Callable[..., _Ret]\n\n\ndef _try_to_reorder(\n        func:\n            _CallablePR\n        ) -> _CallablePR:\n    \"\"\"Decorator that serves reordering requests.\"\"\"\n    @_ft.wraps(func)\n    def _wrapper(\n            bdd:\n                'BDD',\n            *args,\n            **kwargs\n            ) -> _Ret:\n        with _ReorderingContext(bdd):\n            return func(\n                bdd,\n                *args,\n                **kwargs)\n        logger.info('Reordering needed...')\n        # disable reordering requests while swapping\n        bdd._last_len = None\n        reorder(bdd)\n        len_after = len(bdd)\n        # try again,\n        # reordering disabled to avoid livelock\n        with _ReorderingContext(bdd):\n            r = func(\n                bdd,\n                *args, **kwargs)\n        # enable reordering requests\n        bdd._last_len = GROWTH_FACTOR * len_after\n        return r\n    return _wrapper\n\n\nclass _ReorderingContext:\n    \"\"\"Context manager that tracks decorator nesting.\"\"\"\n\n    def __init__(\n            self,\n            bdd:\n                'BDD'\n            ) -> None:\n        self.bdd = bdd\n        self.nested = None\n\n    def __enter__(\n            self):\n        self.nested = self.bdd._reordering_context\n        self.bdd._reordering_context = True\n\n    def __exit__(\n            self,\n            ex_type,\n            ex_value,\n            tb):\n        self.bdd._reordering_context = self.nested\n        not_nested = (\n            ex_type is _NeedsReordering and\n            not self.nested)\n        if not_nested:\n            return True\n\n\nclass _NeedsReordering(Exception):\n    \"\"\"Raise this to request reordering.\"\"\"\n\n\n_Yes: _ty.TypeAlias = dd._abc.Yes\n_Nat: _ty.TypeAlias = dd._abc.Nat\n_Cardinality: _ty.TypeAlias = dd._abc.Cardinality\n_VariableName: _ty.TypeAlias = dd._abc.VariableName\n_Level: _ty.TypeAlias = dd._abc.Level\n_VariableLevels: _ty.TypeAlias = dd._abc.VariableLevels\n_Assignment: _ty.TypeAlias = dd._abc.Assignment\n_Renaming: _ty.TypeAlias = dd._abc.Renaming\n_Node: _ty.TypeAlias = _Nat\n_Ref: _ty.TypeAlias = int\n    # ```tla\n    # ASSUME\n    #     _Ref \\neq 0\n    # ```\n_Fork: _ty.TypeAlias = tuple[\n    _Level,\n    _Ref | None,\n    _Node | None]\n_Formula: _ty.TypeAlias = dd._abc.Formula\n\n\nclass BDD(dd._abc.BDD[_Ref]):\n    \"\"\"Shared ordered binary decision diagram.\n\n    The terminal node is 1.\n    Nodes are positive integers,\n    edges signed integers.\n    Complemented edges are represented as\n    negative integers.\n    Values returned by methods are edges,\n    possibly complemented.\n\n    Attributes:\n      - `vars`:\n        `dict` mapping `variables` to `int` levels\n      - `roots`:\n        (optional) edges\n      - `max_nodes`:\n        raise `Exception` if this limit is reached.\n        The default value is `sys.maxsize` in Python 3.\n        Increase it if needed.\n\n    To ensure that the target node of a returned edge\n    is not garbage collected during reordering,\n    increment its reference counter:\n\n    `bdd.incref(edge)`\n\n    To ensure that `ite` maintains reducedness add new\n    nodes using `find_or_add` to keep the table updated,\n    or call `update_predecessors` prior to calling `ite`.\n    \"\"\"\n    # omitted docstrings are inheritted from `super()`\n\n    def __init__(\n            self,\n            levels:\n                _VariableLevels |\n                None=None\n            ) -> None:\n        if levels is None:\n            levels = dict()\n        _assert_valid_ordering(levels)\n        self._pred: dict[\n            _Fork,\n            _Node\n            ] = dict()\n        self._succ: dict[\n            _Node,\n            _Fork\n            ] = dict()\n        self._ref: dict[\n            _Node,\n            _Nat\n            ] = dict()\n        # all smaller positive integers\n        # are used as node indices, and\n        # no larger integers are used\n        # as node indices\n        self._min_free: _Nat = 2\n            # minimum number unused as BDD index\n        self._ite_table: dict[\n            tuple[_Ref, _Ref, _Ref],\n            _Ref\n            ] = dict()\n            # `(predicate, then, else) |-> edge`\n            # cache for ternary conditional\n            # (\"ite\" means \"if-then-else\")\n        self.vars: _VariableLevels = dict()\n        self._level_to_var: dict[\n            _Level,\n            _VariableName\n            ] = dict()\n            # inverse of `self.vars`\n        # handle no vars\n        self._init_terminal(len(self.vars))\n        # for decorator nesting\n        self._reordering_context: _Yes = False\n        # after last reordering\n        self._last_len: _Nat | None = None\n        for var, level in levels.items():\n            self.add_var(var, level)\n        # set of edges\n        # optional\n        self.roots: set = set()\n        self.max_nodes: _Nat = sys.maxsize\n\n    def __copy__(\n            self\n            ) -> 'BDD':\n        bdd = BDD(self.vars)\n        bdd._pred = dict(self._pred)\n        bdd._succ = dict(self._succ)\n        bdd._ref = dict(self._ref)\n        bdd._min_free = self._min_free\n        bdd.roots = set(self.roots)\n        bdd.max_nodes = self.max_nodes\n        return bdd\n\n    def __del__(\n            self\n            ) -> None:\n        \"\"\"Assert that all remaining nodes are garbage.\"\"\"\n        if self._ref[1] > 0:\n            self.decref(1)\n                # free ref from `self._init_terminal()`\n        self.collect_garbage()\n        refs_exist = any(\n            v != 0\n            for v in self._ref.values())\n        if not refs_exist:\n            return\n        stack = inspect.stack()\n        stack_str = _pp.pformat(stack)\n        raise AssertionError(\n            'There are nodes still referenced '\n            'upon shutdown. Details:\\n'\n            f'{self._ref}\\n'\n            f'{self._succ}\\n'\n            f'{self.vars}\\n'\n            f'{self._ite_table}\\n'\n            f'{type(self)}\\n'\n            f'{stack_str}')\n\n    def __len__(\n            self\n            ) -> _Cardinality:\n        return len(self._succ)\n\n    def __contains__(\n            self,\n            u:\n                _Ref\n            ) -> _Yes:\n        return abs(u) in self._succ\n\n    def __iter__(\n            self):\n        return iter(self._succ)\n\n    def __str__(\n            self\n            ) -> str:\n        return (\n            'Binary decision diagram:\\n'\n            '------------------------\\n'\n            f'var levels: {self.vars}\\n'\n            f'roots: {self.roots}\\n')\n\n    def configure(\n            self,\n            **kw\n            ) -> dict[\n                str,\n                _ty.Any]:\n        \"\"\"Read and apply parameter values.\n\n        First read parameter values (returned as `dict`),\n        then apply `kw`. Available keyword arguments:\n\n        - `'reordering'`:\n          if `True` then enable, else disable\n        \"\"\"\n        d = dict(\n            reordering=(self._last_len is not None))\n        for k, v in kw.items():\n            if k == 'reordering':\n                if v:\n                    self._last_len = max(\n                        REORDER_STARTS, len(self))\n                else:\n                    self._last_len = None\n            else:\n                raise ValueError(\n                    f'Unknown parameter \"{k}\"')\n        return d\n\n    @property\n    def ordering(\n            self):\n        raise DeprecationWarning(\n            'use `dd.bdd.BDD.vars` '\n            'instead of `.ordering`')\n\n    def _init_terminal(\n            self,\n            level:\n                _Level\n            ) -> None:\n        \"\"\"Place constant node `1`.\n\n        Used for initialization and to shift node `1` to\n        lower levels, as fresh variables are being added.\n        \"\"\"\n        u = 1\n        t = (level, None, None)\n        told = self._succ.setdefault(u, t)\n        self._pred.pop(told, None)\n        self._succ[u] = t\n        self._pred[t] = u\n        self._ref.setdefault(u, 1)\n\n    def succ(\n            self,\n            u:\n                _Ref\n            ) -> _Fork:\n        \"\"\"Return `(level, low, high)` for `abs(u)`.\"\"\"\n        return self._succ[abs(u)]\n\n    def incref(\n            self,\n            u:\n                _Ref\n            ) -> None:\n        \"\"\"Increment reference count of node `u`.\"\"\"\n        self._ref[abs(u)] += 1\n\n    def decref(\n            self,\n            u:\n                _Ref\n            ) -> None:\n        \"\"\"Decrement reference count of node `u`,\n\n        with 0 as minimum value.\n        \"\"\"\n        if self._ref[abs(u)] <= 0:\n            n = self._ref[abs(u)]\n            warnings.warn(\n                'The method `dd.bdd.BDD.decref` was called '\n                f'for BDD node {u} with reference count {n}. '\n                'This call has no effect. Calling `decref` '\n                'for a node with nonpositive reference count '\n                'may indicate a programming error.',\n                UserWarning)\n            return\n        self._ref[abs(u)] -= 1\n\n    def ref(\n            self,\n            u:\n                _Ref\n            ) -> _Nat:\n        \"\"\"Return reference count of edge `u`.\"\"\"\n        return self._ref[abs(u)]\n\n    def declare(\n            self,\n            *variables:\n                _VariableName\n            ) -> None:\n        for var in variables:\n            self.add_var(var)\n\n    def add_var(\n            self,\n            var:\n                _VariableName,\n            level:\n                _Level |\n                None=None\n            ) -> _Level:\n        \"\"\"Declare a variable named `var` at `level`.\n\n        The new variable is Boolean-valued.\n\n        If `level` is absent, then add the new variable\n        at the bottom level.\n\n        Raise `ValueError` if:\n        - `var` already exists at a level\n          different than the given `level`, or\n        - the given `level` is already used by\n          another variable\n        - `level` is not given and `var` does not exist,\n          and the next level larger than the\n          current bottom level is already used by\n          another variable.\n\n        If `var` already exists, and either `level`\n        is not given, or `var` has `level`,\n        then return without raising exceptions.\n\n        @param var:\n            name of new variable to declare\n        @param level:\n            level of new variable to declare\n        @return:\n            level of variable `var`\n        \"\"\"\n        # var already exists ?\n        if var in self.vars:\n            return self._check_var(var, level)\n        # level already used ?\n        level = self._next_free_level(var, level)\n        # update the mappings between\n        # vars and levels\n        self.vars[var] = level\n        self._level_to_var[level] = var\n        # move the leaf node to\n        # the new bottom level\n        self._init_terminal(len(self.vars))\n        return level\n\n    def _check_var(\n            self,\n            var:\n                _VariableName,\n            level:\n                _Level |\n                None\n            ) -> _Level:\n        \"\"\"Assert that `var` has `level`.\n\n        Return the level of `var`.\n\n        Exceptions:\n        - raise `ValueError` if:\n          - `var` is not a declared variable, or\n          - `level is not None` and\n            `level` is not the level of variable `var`\n        - raise `RuntimeError` if an unexpected\n          value of level is found in `self.vars[var]`\n\n        @param var:\n            name of variable\n        @param level:\n            level of variable\n        \"\"\"\n        if var not in self.vars:\n            raise ValueError(\n                f'\"{var}\" is not the name of '\n                'a declared variable')\n        var_level = self.vars[var]\n        if var_level is None or var_level < 0:\n            raise RuntimeError(\n                f'`{self.vars[var] = }` '\n                '(expected integer >= 0)')\n        if level is None or level == var_level:\n            return var_level\n        raise ValueError(\n            f'for variable \"{var}\": '\n            f'{level} = level != '\n            f'level of \"{var}\" = {var_level}')\n\n    def _next_free_level(\n            self,\n            var,\n            level:\n                _Level |\n                None\n            ) -> _Nat:\n        \"\"\"Return a free level.\n\n        Raise `ValueError`:\n        - if the given `level` is already used by\n          a variable, or\n        - if `level is None` and the next level is\n          used by a variable.\n\n        If `level is None`, then return the\n        next level after the current largest level.\n        Otherwise, return the given `level`.\n\n        @param var:\n            name of intended new variable,\n            used only to form the `ValueError` message\n        @param level:\n            level of intended new variable\n        \"\"\"\n        # assume next level is unoccupied\n        if level is None:\n            level = len(self.vars)\n        if level < 0:\n            raise AssertionError(\n                f'`{level = } < 0')\n        # level already used ?\n        other = self._level_to_var.get(level)\n        if other is None:\n            return level\n        raise ValueError(\n            f'level {level} is already '\n            f'used by variable \"{other}\", '\n            'choose another level for the '\n            f'new variable \"{var}\"')\n\n    @_try_to_reorder\n    def var(\n            self,\n            var:\n                _VariableName\n            ) -> _Ref:\n        if var not in self.vars:\n            raise ValueError(\n                f'undeclared variable \"{var}\", '\n                'the declared variables are:\\n'\n                f' {self.vars}')\n        j = self.vars[var]\n        u = self.find_or_add(j, -1, 1)\n        return u\n\n    def var_at_level(\n            self,\n            level:\n                _Level\n            ) -> _VariableName:\n        if level not in self._level_to_var:\n            raise ValueError(\n                f'no variable has level:  {level}, '\n                'the current levels of all variables '\n                f'are:  {self.vars}')\n        return self._level_to_var[level]\n\n    def level_of_var(\n            self,\n            var:\n                _VariableName\n            ) -> _Level:\n        if var not in self.vars:\n            raise ValueError(\n                f'name \"{var}\" is not '\n                'a declared variable, '\n                'the declared variables are:'\n                f' {self.vars}')\n        return self.vars[var]\n\n    @property\n    def var_levels(\n            self\n            ) -> _VariableLevels:\n        return dict(self.vars)\n\n    @_ty.overload\n    def _map_to_level(\n            self,\n            d:\n                _abc.Mapping[\n                    _VariableName,\n                    _ty.Any] |\n                _abc.Mapping[\n                    _Level,\n                    _ty.Any]\n            ) -> dict[_Level, bool]:\n        ...\n\n    @_ty.overload\n    def _map_to_level(\n            self,\n            d:\n                _abc.Set[\n                    _VariableName] |\n                _abc.Set\n                    [_Level]\n            ) -> set[_Level]:\n        ...\n\n    def _map_to_level(\n            self,\n            d:\n                _abc.Mapping[\n                    _VariableName,\n                    _ty.Any] |\n                _abc.Mapping[\n                    _Level,\n                    _ty.Any] |\n                _abc.Set[\n                    _VariableName] |\n                _abc.Set[\n                    _Level]\n            ) -> (\n                dict[_Level, bool] |\n                set[_Level]):\n        \"\"\"Map keys of `d` to variable levels.\n\n        Uses `self.vars` to map the keys to levels.\n\n        If `d` is an iterable but not a mapping,\n        then an iterable is returned.\n        \"\"\"\n        match d:\n            case _abc.Mapping():\n                d = dict(d)\n            case _abc.Set():\n                d = set(d)\n            case _:\n                raise TypeError(d)\n        if not d:\n            match d:\n                case dict():\n                    return dict()\n                case set():\n                    return set()\n                case _:\n                    raise TypeError(d)\n        # are keys variable names ?\n        u = next(iter(d))\n        if u not in self.vars:\n            self._assert_keys_are_levels(d)\n            match d:\n                case dict():\n                    return {\n                        int(k): v\n                        for k, v in d.items()}\n                case set():\n                    return set(map(int, d))\n                case _:\n                    raise ValueError(d)\n        if isinstance(d, _abc.Mapping):\n            return {\n                self.vars[var]: bool(val)\n                for var, val in\n                    d.items()}\n        else:\n            return {\n                self.vars[k]\n                for k in d}\n\n    def _assert_keys_are_levels(\n            self,\n            kv:\n                _abc.Iterable\n            ) -> None:\n        \"\"\"Assert that `kv` values are levels.\n\n        Raise `ValueError` if not.\n        \"\"\"\n        not_levels = set()\n        def key_is_level(\n                key\n                ) -> _Yes:\n            is_level = (\n                key in self._level_to_var)\n            if not is_level:\n                not_levels.add(key)\n            return is_level\n        keys_are_levels = all(map(\n            key_is_level, kv))\n        if keys_are_levels:\n            return\n        def fmt(key):\n            return (\n                f'key `{key}` '\n                'is not a level')\n        errors = ',\\n'.join(map(\n            fmt, not_levels))\n        raise ValueError(\n            f'{errors},\\n'\n            'currently the levels are:\\n'\n            f'{self._level_to_var = }')\n\n    def _top_var(\n            self,\n            *nodes:\n                _Ref\n            ) -> _Level:\n        def level_of(node):\n            level, *_ = self._succ[abs(node)]\n            return level\n        return min(map(level_of, nodes))\n\n    def copy(\n            self,\n            u:\n                _Ref,\n            other:\n                'BDD'\n            ) -> _Ref:\n        \"\"\"Transfer BDD with root `u` to `other`.\"\"\"\n        return copy_bdd(u, self, other)\n\n    def descendants(\n            self,\n            roots:\n                _abc.Iterable[_Ref]\n            ) -> set[_Ref]:\n        \"\"\"Return nodes reachable from `roots`.\n\n        Nodes pointed to by references in\n        `roots` are included.\n        Nodes are represented as positive integers.\n        \"\"\"\n        abs_roots = set(map(abs, roots))\n        visited = set()\n        for u in abs_roots:\n            visited.add(1)\n            self._descendants(u, visited)\n        if not abs_roots.issubset(visited):\n            raise AssertionError(\n                (abs_roots, visited))\n        return visited\n\n    def _descendants(\n            self,\n            u:\n                _Ref,\n            visited:\n                set[_Node]\n            ) -> None:\n        r = abs(u)\n        if r == 1 or r in visited:\n            return\n        _, v, w = self._succ[r]\n        if not v:\n            raise AssertionError(v)\n        if not w:\n            raise AssertionError(w)\n        self._descendants(v, visited)\n        self._descendants(w, visited)\n        visited.add(r)\n\n    def is_essential(\n            self,\n            u:\n                _Ref,\n            var:\n                _VariableName\n            ) -> _Yes:\n        \"\"\"Return `True` if `var` is essential for node `u`.\n\n        If `var` is a name undeclared in\n        `self.vars`, return `False`.\n        \"\"\"\n        i = self.vars.get(var)\n        if i is None:\n            return False\n        iu, v, w = self._succ[abs(u)]\n        # var above node u ?\n        if i < iu:\n            return False\n        if i == iu:\n            return True\n        # u depends on node labeled with var ?\n        if not v:\n            raise AssertionError(v)\n        if not w:\n            raise AssertionError(w)\n        if self.is_essential(v, var):\n            return True\n        if self.is_essential(w, var):\n            return True\n        return False\n\n    def support(\n            self,\n            u:\n                _Ref,\n            as_levels:\n                _Yes=False\n            ) -> set[\n                _VariableName]:\n        levels = set()\n        nodes = set()\n        self._support(u, levels, nodes)\n        if as_levels:\n            return levels\n        return {self.var_at_level(i) for i in levels}\n\n    def _support(\n            self,\n            u:\n                _Ref,\n            levels:\n                set[_Level],\n            nodes:\n                set[_Ref]):\n        \"\"\"Recurse to collect variables in support.\"\"\"\n        # exhausted all vars ?\n        if len(levels) == len(self.vars):\n            return\n        # visited ?\n        r = abs(u)\n        if r in nodes:\n            return\n        nodes.add(r)\n        # terminal ?\n        if r == 1:\n            return\n        # add var\n        i, v, w = self._succ[r]\n        if not v:\n            raise AssertionError(v)\n        if not w:\n            raise AssertionError(w)\n        levels.add(i)\n        # recurse\n        self._support(v, levels, nodes)\n        self._support(w, levels, nodes)\n\n    def levels(\n            self,\n            skip_terminals:\n                _Yes=False\n            ) -> _abc.Iterable[\n                tuple[\n                    _Ref,\n                    _Level,\n                    _Ref,\n                    _Node]]:\n        \"\"\"Return generator of tuples `(u, i, v, w)`.\n\n        Where `i` ranges from terminals to root.\n\n        @param skip_terminals:\n            if `True`, then omit\n            terminal nodes.\n        \"\"\"\n        if skip_terminals:\n            n = len(self.vars) - 1\n        else:\n            n = len(self.vars)\n        for i in range(n, -1, -1):\n            for u, (j, v, w) in self._succ.items():\n                if i != j:\n                    continue\n                yield u, i, v, w\n\n    def _levels(\n            self\n            ) -> dict[\n                _Level,\n                set[_Node]]:\n        \"\"\"Return mapping from levels to nodes.\"\"\"\n        n = len(self.vars)\n        levels = {\n            i: set()\n            for var, i in\n                self.vars.items()}\n        levels[n] = set()\n        for u, (i, v, w) in self._succ.items():\n            levels[i].add(u)\n        levels.pop(n)\n        return levels\n\n    @_try_to_reorder\n    def reduction(\n            self):\n        \"\"\"Return copy reduced with respect to `self.vars`.\n\n        This function has educational value.\n        \"\"\"\n        # terminals\n        bdd = BDD(self.vars)\n        umap = {1: 1}\n        # non-terminals\n        levels = self.levels(\n            skip_terminals=True)\n        for u, i, v, w in levels:\n            if u <= 0:\n                raise AssertionError(u)\n            p, q = umap[abs(v)], umap[abs(w)]\n            p = _flip(p, v)\n            q = _flip(q, w)\n            r = bdd.find_or_add(i, p, q)\n            if r <= 0:\n                raise AssertionError(r)\n            umap[u] = r\n        for v in self.roots:\n            p = umap[abs(v)]\n            p = _flip(p, v)\n            bdd.roots.add(p)\n        return bdd\n\n    def undeclare_vars(\n            self,\n            *vrs\n            ) -> set[str]:\n        \"\"\"Remove unused variables `vrs` from `self.vars`.\n\n        Asserts that each variable in `vrs` corresponds to\n        an empty level.\n\n        If `vrs` is empty, then remove all unused variables.\n\n        Garbage collection may need to be called before\n        calling `undeclare_vars`, in order to collect\n        unused nodes to obtain empty levels.\n        \"\"\"\n        for var in vrs:\n            if var not in self.vars:\n                raise ValueError(\n                    f'name \"{var}\" is not '\n                    'a declared variable. '\n                    'The declared variables are:\\n'\n                    f'{self.vars}')\n        full_levels = {\n            i\n            for i, _, _ in\n                self._succ.values()}\n        # remove only unused variables\n        for var in vrs:\n            level = self.level_of_var(var)\n            if level in full_levels:\n                raise ValueError(\n                    f'the given variable \"{var}\" is not '\n                    'at an empty level (i.e., there still '\n                    f'exist BDD nodes at level {level}, '\n                    f'where variable \"{var}\" is)')\n        # keep unused variables not in `vrs`\n        if vrs:\n            full_levels |= {\n                level\n                for var, level in\n                    self.vars.items()\n                if var not in vrs}\n        # map old to new levels\n        n = 1 + len(self.vars)\n            # include terminal\n        new_levels = [\n            i\n            for i in range(n)\n            if i in full_levels]\n        new_levels = {\n            i: new\n            for new, i in\n                enumerate(new_levels)}\n        # update variables and level declarations\n        rm_vars = {\n            var for var, level in\n                self.vars.items()\n            if level not in full_levels}\n        self.vars = {\n            var: new_levels[old]\n            for var, old in self.vars.items()\n            if old in full_levels}\n        self._level_to_var = {\n            k: var\n            for var, k in self.vars.items()}\n        # update node levels\n        self._succ = {\n            u: (new_levels[i], v, w)\n            for u, (i, v, w) in\n                self._succ.items()}\n        self._pred = {\n            v: k\n            for k, v in\n                self._succ.items()}\n        # clear cache\n        self._ite_table = dict()\n        return rm_vars\n\n    def let(\n            self,\n            definitions:\n                _Renaming |\n                _Assignment |\n                dict[\n                    _VariableName,\n                    _Ref],\n            u:\n                _Ref\n            ) -> _Ref:\n        d = definitions\n        if not d:\n            logger.warning(\n                'Call to `BDD.let` with no effect: '\n                '`defs` is empty.')\n            return u\n        var = next(iter(definitions))\n        value = d[var]\n        if isinstance(value, bool):\n            return self.cofactor(u, d)\n        elif isinstance(value, int):\n            return self.compose(u, d)\n        try:\n            value + 's'\n        except TypeError:\n            raise ValueError(\n                'Key must be var name as `str`, '\n                'or Boolean value as `bool`, '\n                'or BDD node as `int`.')\n        return self.rename(u, d)\n\n    @_try_to_reorder\n    def compose(\n            self,\n            f:\n                _Ref,\n            var_sub:\n                dict[\n                    _VariableName,\n                    _Ref]\n            ) -> _Ref:\n        \"\"\"Return substitutions `var_sub` in `f`.\n\n        @param f:\n            node\n        @param var_sub:\n            `dict` that maps variables to BDD nodes\n        \"\"\"\n        cache = dict()\n        if len(var_sub) == 1:\n            (var, g), = var_sub.items()\n            j = self.level_of_var(var)\n            return self._compose(\n                f, j, g, cache)\n        else:\n            dvars = {\n                self.level_of_var(var): g\n                for var, g in\n                    var_sub.items()}\n            return self._vector_compose(\n                f, dvars, cache)\n\n    def _compose(\n            self,\n            f:\n                _Ref,\n            j:\n                _Level,\n            g:\n                _Ref,\n            cache:\n                dict[\n                    tuple[_Ref, _Ref],\n                    _Ref]\n            ) -> _Ref:\n        # terminal ?\n        if abs(f) == 1:\n            return f\n        # cached ?\n        if (f, g) in cache:\n            return cache[(f, g)]\n        # independent of j ?\n        i, v, w = self._succ[abs(f)]\n        if not v:\n            raise AssertionError(v)\n        if not w:\n            raise AssertionError(w)\n        # below j ?\n        if j < i:\n            return f\n        elif i == j:\n            r = self.ite(g, w, v)\n            # complemented edge ?\n            if f < 0:\n                r = -r\n        else:\n            if i >= j:\n                raise AssertionError(\n                    (i, j))\n            k, _, _ = self._succ[abs(g)]\n            z = min(i, k)\n            f0, f1 = self._top_cofactor(f, z)\n            g0, g1 = self._top_cofactor(g, z)\n            p = self._compose(\n                f0, j, g0,\n                cache)\n            q = self._compose(\n                f1, j, g1,\n                cache)\n            r = self.find_or_add(z, p, q)\n        cache[(f, g)] = r\n        return r\n\n    def _vector_compose(\n            self,\n            f:\n                _Ref,\n            level_sub:\n                dict[_Level, _Ref],\n            cache:\n                dict[_Node, _Ref]\n            ) -> _Ref:\n        # terminal ?\n        if abs(f) == 1:\n            return f\n        # cached ?\n        r = cache.get(abs(f))\n        if r is not None:\n            if r == 0:\n                raise AssertionError(r)\n            # complement ?\n            if f < 0:\n                r = -r\n            return r\n        # recurse\n        i, v, w = self._succ[abs(f)]\n        if not v:\n            raise AssertionError(v)\n        if not w:\n            raise AssertionError(w)\n        p = self._vector_compose(\n            v, level_sub,\n            cache)\n        q = self._vector_compose(\n            w, level_sub,\n            cache)\n        # map this level\n        g = level_sub.get(i)\n        if g is None:\n            g = self.find_or_add(i, -1, 1)\n        r = self.ite(g, q, p)\n        # memoize\n        cache[abs(f)] = r\n        # complement ?\n        if f < 0:\n            r = -r\n        return r\n\n    @_try_to_reorder\n    def rename(\n            self,\n            u:\n                _Ref,\n            dvars:\n                _Renaming\n            ) -> _Ref:\n        \"\"\"Efficient rename to non-essential neighbors.\n\n        @param dvars:\n            `dict` from variabe levels to variable levels\n            or from variable names to variable names\n        \"\"\"\n        return rename(u, self, dvars)\n\n    def _top_cofactor(\n            self,\n            u:\n                _Ref,\n            i:\n                _Level\n            ) -> tuple[\n                _Ref,\n                _Ref]:\n        \"\"\"Return successor pair with respect to level `i`.\"\"\"\n        # terminal node ?\n        if abs(u) == 1:\n            return (u, u)\n        # non-terminal node\n        iu, v, w = self._succ[abs(u)]\n        if not v:\n            raise AssertionError(v)\n        if not w:\n            raise AssertionError(w)\n        # u independent of var ?\n        if i < iu:\n            return (u, u)\n        if iu != i:\n            raise AssertionError(\n                'for i > iu, call cofactor instead '\n                f'({i = }, {iu = })')\n        # u labeled with var\n        # complement ?\n        if u < 0:\n            v, w = -v, -w\n        return (v, w)\n\n    @_try_to_reorder\n    def cofactor(\n            self,\n            u:\n                _Ref,\n            values:\n                _Assignment\n            ) -> _Ref:\n        \"\"\"Replace variables in `u` with Booleans.\"\"\"\n        level_values = self._map_to_level(values)\n        cache = dict()\n        ordvar = sorted(level_values)\n        j = 0\n        if abs(u) not in self:\n            raise ValueError(\n                f'node {u} not in `self`')\n        return self._cofactor(\n            u, j, ordvar, level_values, cache)\n\n    def _cofactor(\n            self,\n            u:\n                _Ref,\n            j:\n                _Level,\n            ordvar:\n                list[_Level],\n            values:\n                dict[_Level, bool],\n            cache:\n                dict[_Ref, _Ref]\n            ) -> _Ref:\n        \"\"\"Recurse to compute cofactor.\"\"\"\n        # terminal ?\n        if abs(u) == 1:\n            return u\n        # memoized ?\n        if u in cache:\n            return cache[u]\n        i, v, w = self._succ[abs(u)]\n        if not v:\n            raise AssertionError(v)\n        if not w:\n            raise AssertionError(w)\n        n = len(ordvar)\n        # skip nonessential variables\n        while j < n:\n            if ordvar[j] < i:\n                j += 1\n            else:\n                break\n        if j == n:\n            # exhausted valuation\n            return u\n        if j >= n:\n            raise AssertionError((j, n))\n        # recurse\n        if i in values:\n            val = values[i]\n            if bool(val):\n                v = w\n            r = self._cofactor(\n                v, j,\n                ordvar, values,\n                cache)\n        else:\n            p = self._cofactor(\n                v, j,\n                ordvar, values,\n                cache)\n            q = self._cofactor(\n                w, j,\n                ordvar, values,\n                cache)\n            r = self.find_or_add(i, p, q)\n        # complement ?\n        if u < 0:\n            r = -r\n        cache[u] = r\n        return r\n\n    @_try_to_reorder\n    def quantify(\n            self,\n            u:\n                _Ref,\n            qvars:\n                _abc.Iterable[\n                    _VariableName],\n            forall:\n                _Yes=False\n            ) -> _Ref:\n        \"\"\"Return existential or universal abstraction.\n\n        @param u:\n            node\n        @param qvars:\n            quantified variables\n        @param forall:\n            if `True`,\n            then quantify `qvars` universally,\n            else existentially.\n        \"\"\"\n        qvars = self._map_to_level(set(qvars))\n        cache = dict()\n        ordvar = sorted(qvars)\n        j = 0\n        return self._quantify(\n            u, j, ordvar,\n            qvars, forall,\n            cache)\n\n    def _quantify(\n            self,\n            u:\n                _Ref,\n            j:\n                _Level,\n            ordvar:\n                list[_Level],\n            qvars:\n                set[_Level],\n            forall:\n                _Yes,\n            cache:\n                dict[_Ref, _Ref]\n            ) -> _Ref:\n        \"\"\"Recurse to quantify variables.\"\"\"\n        # terminal ?\n        if abs(u) == 1:\n            return u\n        if u in cache:\n            return cache[u]\n        i, v, w = self._succ[abs(u)]\n        if not v:\n            raise AssertionError(v)\n        if not w:\n            raise AssertionError(w)\n        # complement ?\n        if u < 0:\n            v, w = -v, -w\n        n = len(ordvar)\n        # skip nonessential variables\n        while j < n:\n            if ordvar[j] < i:\n                j += 1\n            else:\n                break\n        else:\n            # exhausted valuation\n            return u\n        # recurse\n        p = self._quantify(\n            v, j, ordvar,\n            qvars, forall,\n            cache)\n        q = self._quantify(\n            w, j, ordvar,\n            qvars, forall,\n            cache)\n        if i in qvars:\n            if forall:\n                r = self.ite(p, q, -1)\n                    # conjoin\n            else:\n                r = self.ite(p, 1, q)\n                    # disjoin\n        else:\n            r = self.find_or_add(i, p, q)\n        cache[u] = r\n        return r\n\n    def forall(\n            self,\n            qvars:\n                _abc.Iterable[\n                    _VariableName],\n            u:\n                _Ref\n            ) -> _Ref:\n        return self.quantify(\n            u, qvars,\n            forall=True)\n\n    def exist(\n            self,\n            qvars:\n                _abc.Iterable[\n                    _VariableName],\n            u:\n                _Ref\n            ) -> _Ref:\n        return self.quantify(\n            u, qvars,\n            forall=False)\n\n    @_try_to_reorder\n    def ite(\n            self,\n            g:\n                _Ref,\n            u:\n                _Ref,\n            v:\n                _Ref\n            ) -> _Ref:\n        # wrap so reordering can\n        # delete unused nodes\n        return self._ite(g, u, v)\n\n    def _ite(\n            self,\n            g:\n                _Ref,\n            u:\n                _Ref,\n            v:\n                _Ref\n            ) -> _Ref:\n        \"\"\"Recurse to compute ternary conditional.\"\"\"\n        # is g terminal ?\n        if g == 1:\n            return u\n        elif g == -1:\n            return v\n        # g is non-terminal\n        # already computed ?\n        r = (g, u, v)\n        w = self._ite_table.get(r)\n        if w is not None:\n            return w\n        z = min(self._succ[abs(g)][0],\n                self._succ[abs(u)][0],\n                self._succ[abs(v)][0])\n        g0, g1 = self._top_cofactor(g, z)\n        u0, u1 = self._top_cofactor(u, z)\n        v0, v1 = self._top_cofactor(v, z)\n        p = self._ite(g0, u0, v0)\n        q = self._ite(g1, u1, v1)\n        w = self.find_or_add(z, p, q)\n        # cache\n        self._ite_table[r] = w\n        return w\n\n    def find_or_add(\n            self,\n            i:\n                _Level,\n            v:\n                _Ref,\n            w:\n                _Ref\n            ) -> _Ref:\n        \"\"\"Return reference to specified node.\n\n        The returned node is at level `i`\n        with successors `v, w`.\n\n        If such a node exists already,\n        then it is quickly found in the cached table,\n        and the reference returned.\n\n        @param i:\n            level in `range(n_vars - 1)`\n        @param v:\n            low edge\n        @param w:\n            high edge\n        \"\"\"\n        _request_reordering(self)\n        if i < 0:\n            raise ValueError(\n                f'The given level: {i = } < 0')\n        if i >= len(self.vars):\n            raise ValueError(\n                f'The given level: {i = } is not < of '\n                'the number of '\n                f'declared variables ({len(self.vars)}) '\n                '(the set of levels is expected to '\n                'comprise of contiguous numbers)')\n        if abs(v) not in self._succ:\n            raise ValueError(\n                f'argument: {v = } is not '\n                'a reference to an existing BDD node')\n        if abs(w) not in self._succ:\n            raise ValueError(\n                f'argument: {w = } is not '\n                'a reference to an existing BDD node')\n        # ensure canonicity of complemented edges\n        if w < 0:\n            v, w = -v, -w\n            r = -1\n        else:\n            r = 1\n        # eliminate\n        if v == w:\n            return r * v\n        # already exists ?\n        t = (i, v, w)\n        u = self._pred.get(t)\n        if u is not None:\n            return r * u\n        # find a free integer\n        u = self._min_free\n        if u <= 1:\n            raise AssertionError(\n                f'min free index is {u}, '\n                'which is <= 1')\n        if u in self._succ:\n            raise AssertionError(\n                f'node index {u} '\n                'is already used. '\n                f'{self._succ = }')\n        # add node\n        self._pred[t] = u\n        self._succ[u] = t\n        self._ref[u] = 0\n        self._min_free = self._next_free_int(u)\n        # increment reference counters\n        self.incref(v)\n        self.incref(w)\n        return r * u\n\n    def _next_free_int(\n            self,\n            start:\n                _Nat\n            ) -> _Nat:\n        \"\"\"Return smallest unused integer `> start`.\"\"\"\n        if start < 1:\n            raise ValueError(\n                f'{start} = start < 1')\n        for i in range(start, self.max_nodes):\n            if i not in self._succ:\n                return i\n        raise RuntimeError(\n            'full: reached `self.max_nodes` nodes '\n            f'({self.max_nodes = }).')\n\n    def collect_garbage(\n            self,\n            roots:\n                _abc.Iterable[_Ref] |\n                None=None\n            ) -> None:\n        \"\"\"Recursively remove unused nodes\n\n        A node is unused when\n        its reference count is zero.\n\n        Removal starts from the nodes in `roots` with zero\n        reference count. If no `roots` are given, then\n        all nodes are scanned for zero reference counts.\n        \"\"\"\n        n = len(self)\n        if roots is None:\n            roots = self._ref\n        def is_unused(\n                u\n                ) -> _Yes:\n            return not self._ref[abs(u)]\n        unused = filter(\n            is_unused, roots)\n        unused = set(map(\n            abs, unused))\n        # keep terminal\n        #\n        # Filtering above implies 1 is kept,\n        # except within `__del__()`.\n        # There `roots` happens to be `None`.\n        if 1 in unused:\n            unused.remove(1)\n        while unused:\n            u = unused.pop()\n            if u == 1:\n                raise AssertionError(u)\n            # remove\n            i, v, w = self._succ.pop(u)\n            if not v:\n                raise AssertionError(v)\n            if not w:\n                raise AssertionError(w)\n            u_ = self._pred.pop((i, v, w))\n            uref = self._ref.pop(u)\n            self._min_free = min(u, self._min_free)\n            if u != u_:\n                raise AssertionError((u, u_))\n            if uref:\n                raise AssertionError(uref)\n            if self._min_free <= 1:\n                raise AssertionError(self._min_free)\n            # decrement reference counters\n            self.decref(v)\n            self.decref(w)\n            # unused ?\n            if not self._ref[abs(v)] and abs(v) != 1:\n                unused.add(abs(v))\n            if not self._ref[w] and w != 1:\n                unused.add(w)\n        self._ite_table = dict()\n        m = len(self)\n        k = n - m\n        if k < 0:\n            raise AssertionError((n, m))\n\n    def update_predecessors(\n            self\n            ) -> None:\n        \"\"\"Update table `self._pred`.\n\n        `self._pred` maps triplets\n        `(level, low, high)` to nodes.\n        \"\"\"\n        for u, t in self._succ.items():\n            if abs(u) == 1:\n                continue\n            self._pred[t] = u\n\n    def swap(\n            self,\n            x:\n                _VariableName |\n                _Level,\n            y:\n                _VariableName |\n                _Level,\n            all_levels:\n                dict[\n                    _Level,\n                    set[_Ref]] |\n                None=None\n            ) -> tuple[\n                _Nat,\n                _Nat]:\n        \"\"\"Permute adjacent variables `x` and `y`.\n\n        Swapping invokes the garbage collector,\n        so be sure to `incref` nodes that should remain.\n\n        @param x, y:\n            variable name or level\n        \"\"\"\n        if all_levels is None:\n            self.collect_garbage()\n            all_levels = self._levels()\n        logger.debug(\n            f'swap variables \"{x}\" and \"{y}\"')\n        if x in self.vars:\n            x = self.vars[x]\n        if y in self.vars:\n            y = self.vars[y]\n        match x:\n            case int():\n                pass\n            case _:\n                raise ValueError(x)\n        match y:\n            case int():\n                pass\n            case _:\n                raise ValueError(y)\n        if not (0 <= x < len(self.vars)):\n            raise ValueError(x)\n        if not (0 <= y < len(self.vars)):\n            raise ValueError(y)\n        # ensure x < y\n        if x > y:\n            x, y = y, x\n        if x >= y:\n            raise ValueError(\n                (x, y))\n        if abs(x - y) != 1:\n            raise ValueError(\n                (x, y))\n        # count nodes\n        oldsize = len(self._succ)\n        # collect levels x and y\n        levels: dict[\n                _Ref,\n                dict[_Ref, tuple]\n            ] = {\n                x: dict(),\n                y: dict()}\n        for j in (x, y):\n            for u in all_levels[j]:\n                i, v, w = self._succ[abs(u)]\n                if i != j:\n                    raise AssertionError(\n                        (i, x, y))\n                u_ = self._pred.pop(\n                    (i, v, w))\n                if u != u_:\n                    raise AssertionError(\n                        (u, u_))\n                levels[j][u] = (v, w)\n        # move level y up\n        for u, (v, w) in levels[y].items():\n            i, _, _ = self._succ[u]\n            if i != y:\n                raise AssertionError((i, y))\n            r = (x, v, w)\n            self._succ[u] = r\n            if r in self._pred:\n                raise AssertionError(r)\n            self._pred[r] = u\n        # move level x down\n        # first x nodes independent of y\n        done = set()\n        for u, (v, w) in levels[x].items():\n            i, _, _ = self._succ[u]\n            if i != x:\n                raise AssertionError((i, x))\n            if not v:\n                raise AssertionError(v)\n            if not w:\n                raise AssertionError(w)\n            iv, v0, v1 = self._low_high(v)\n            iw, w0, w1 = self._low_high(w)\n            # dependeds on y ?\n            if iv <= y or iw <= y:\n                continue\n            # independent of y\n            r = (y, v, w)\n            self._succ[u] = r\n            if r in self._pred:\n                raise AssertionError(r)\n            self._pred[r] = u\n            done.add(u)\n        # x nodes dependent on y\n        garbage = set()\n        xfresh = set()\n        for u, (v, w) in levels[x].items():\n            # for type checking\n            match u:\n                case int():\n                    pass\n                case _:\n                    raise AssertionError(u)\n            if u in done:\n                continue\n            i, _, _ = self._succ[u]\n            if i != x:\n                raise AssertionError((i, x))\n            if not v:\n                raise AssertionError(v)\n            if not w:\n                raise AssertionError(w)\n            self.decref(v)\n            self.decref(w)\n            # possibly unused\n            garbage.add(abs(v))\n            garbage.add(w)\n            # calling cofactor can fail\n            # because y moved\n            iv, v0, v1 = self._swap_cofactor(v, y)\n            iw, w0, w1 = self._swap_cofactor(w, y)\n            # x node depends on y\n            if not (y <= iv and y <= iw):\n                raise AssertionError(\n                    (iv, iw, y))\n            if not (y == iv or y == iw):\n                raise AssertionError(\n                    (iv, iw, y))\n            # complemented edge ?\n            if v < 0 and y == iv:\n                v0, v1 = -v0, -v1\n            p = self.find_or_add(\n                y, v0, w0)\n            q = self.find_or_add(\n                y, v1, w1)\n            if q < 0:\n                raise AssertionError(q)\n            if p == q:\n                raise AssertionError(\n                    'No elimination: '\n                    'node depends on both x and y')\n            if self._succ[abs(p)][0] == y:\n                xfresh.add(abs(p))\n            if self._succ[q][0] == y:\n                xfresh.add(q)\n            r = (x, p, q)\n            self._succ[u] = r\n            if r in self._pred:\n                raise AssertionError(\n                    (u, r, levels, self._pred))\n            self._pred[r] = u\n            self.incref(p)\n            self.incref(q)\n            # garbage collection could be interleaved\n            # but only if there is\n            # substantial loss of efficiency\n        # swap x and y in `vars`\n        vx = self.var_at_level(x)\n        self.vars[vx] = y\n        vy = self.var_at_level(y)\n        self.vars[vy] = x\n        # reset\n        self._level_to_var[y] = vx\n        self._level_to_var[x] = vy\n        self._ite_table = dict()\n        # count nodes\n        self.collect_garbage(garbage)\n        newsize = len(self._succ)\n        # new levels\n        newx = set()\n        newy = set()\n        for u in levels[x]:\n            if u not in self._succ:\n                continue\n            i, _, _ = self._succ[u]\n            if i == x:\n                newy.add(u)\n            elif i == y:\n                newx.add(u)\n            else:\n                raise AssertionError(\n                    (u, i, x, y))\n        for u in xfresh:\n            i, _, _ = self._succ[u]\n            if i != y:\n                raise AssertionError(\n                    (u, i, x, y))\n            newx.add(u)\n        for u in levels[y]:\n            if u not in self._succ:\n                continue\n            i, _, _ = self._succ[u]\n            if i != x:\n                raise AssertionError(\n                    (u, i, x, y))\n            newy.add(u)\n        all_levels[x] = newy\n        all_levels[y] = newx\n        return (\n            oldsize,\n            newsize)\n\n    def _low_high(\n            self,\n            u:\n                _Ref\n            ) -> tuple[\n                _Level,\n                _Ref,\n                _Node]:\n        \"\"\"Return level, low, and high.\n\n        If node `u` is a leaf,\n        then `u` is returned as low and high.\n\n        This method is similar to the\n        method `succ`, but different.\n\n        @return:\n            (level, low, high)\n        \"\"\"\n        i, v, w = self._succ[abs(u)]\n        if abs(u) == 1:\n            return i, u, u\n        if not v:\n            raise AssertionError(v)\n        if not w:\n            raise AssertionError(w)\n        return i, v, w\n\n    def _swap_cofactor(\n            self,\n            u:\n                _Ref,\n            y:\n                _Level\n            ) -> tuple[\n                _Level,\n                _Ref,\n                _Ref]:\n        \"\"\"Return cofactor of node `u` wrt level `y`.\n\n        If node `u` is above level `y`, that means\n        it was at level `y` when the swap started.\n        To account for this,\n        `y` is returned as the node level.\n        \"\"\"\n        i, v, w = self._succ[abs(u)]\n        if y < i:\n            return (i, u, u)\n        # restore index of y node that\n        # moved up\n        if not v:\n            raise AssertionError(v)\n        if not w:\n            raise AssertionError(w)\n        return (y, v, w)\n\n    def count(\n            self,\n            u:\n                _Ref,\n            nvars:\n                _Nat |\n                None=None\n            ) -> _Nat:\n        n = nvars\n        if abs(u) not in self:\n            raise ValueError(u)\n        # index those levels in\n        # support separately\n        levels = {\n            self.level_of_var(var)\n            for var in self.support(u)}\n        k = len(levels)\n        if n is None:\n            n = k\n        slack = n - k\n        if slack < 0:\n            raise ValueError(slack)\n        map_level = dict()\n        for new, old in enumerate(sorted(levels)):\n            map_level[old] = new + slack\n        old, _, _ = self._succ[1]\n        map_level[old] = n\n        map_level['all'] = n\n        r = self._sat_len(\n            u, map_level,\n            d=dict())\n        i, _, _ = self._succ[abs(u)]\n        i = map_level[i]\n        n_models = r * 2**i\n        return self._assert_int(n_models)\n\n    @staticmethod\n    def _assert_int(\n            number:\n                _ty.Any\n            ) -> int:\n        \"\"\"Return `number` if an `int`.\n\n        Raise `AssertionError` otherwise.\n        \"\"\"\n        match number:\n            case int():\n                return number\n        raise AssertionError(\n            'Expected `int` result, '\n            f'but: {number = }')\n\n    def _sat_len(\n            self,\n            u:\n                _Ref,\n            map_level:\n                dict[\n                    _Level |\n                    _ty.Literal['all'],\n                    _Level],\n            d:\n                dict[\n                    _Node,\n                    _Nat]\n            ) -> _Nat:\n        \"\"\"Recurse to compute the number of models.\"\"\"\n        # terminal ?\n        if u == 1:\n            return 1\n        if u == -1:\n            return 0\n        i, v, w = self._succ[abs(u)]\n        if not v:\n            raise AssertionError(v)\n        if not w:\n            raise AssertionError(w)\n        i = map_level[i]\n        # memoized ?\n        if abs(u) in d:\n            n = d[abs(u)]\n            # complement ?\n            if u < 0:\n                n = 2**(map_level['all'] - i) - n\n            return self._assert_int(n)\n        # non-terminal\n        nv = self._sat_len(v, map_level, d)\n        nw = self._sat_len(w, map_level, d)\n        iv, _, _ = self._succ[abs(v)]\n        iw, _, _ = self._succ[w]\n        iv = map_level[iv]\n        iw = map_level[iw]\n        # sum\n        n = self._assert_int(\n            nv * 2**(iv - i - 1) +\n            nw * 2**(iw - i - 1))\n        d[abs(u)] = n\n        # complement ?\n        if u < 0:\n            n = 2**(map_level['all'] - i) - n\n        return self._assert_int(n)\n\n    def pick_iter(\n            self,\n            u:\n                _Ref,\n            care_vars:\n                set[_VariableName] |\n                None=None\n            ) -> _abc.Iterable[\n                _Assignment]:\n        # empty ?\n        if not self._succ:\n            return\n        # non-empty\n        if abs(u) not in self._succ:\n            raise ValueError(\n                f'{u} is not a reference to '\n                'a BDD node in the BDD manager '\n                f'`self` ({self!r})')\n        support = self.support(u)\n        if care_vars is None:\n            care_vars = support\n        missing = {\n            v\n            for v in support\n            if v not in care_vars}\n        if missing:\n            logger.warning(\n                'Missing bits:  '\n                f'support - care_vars = {missing}')\n        cube = dict()\n        value = True\n        cubes = self._sat_iter(\n            u, cube, value)\n        for cube in cubes:\n            minterms = _enumerate_minterms(\n                cube, care_vars)\n            for m in minterms:\n                yield m\n\n    def _sat_iter(\n            self,\n            u:\n                _Ref,\n            cube:\n                dict[\n                    _Level,\n                    bool],\n            value:\n                bool\n            ) -> _abc.Iterable[\n                _Assignment]:\n        \"\"\"Recurse to enumerate models.\"\"\"\n        if u < 0:\n            value = not value\n        # terminal ?\n        if abs(u) == 1:\n            if value:\n                cube = {\n                    self._level_to_var[i]: v\n                    for i, v in cube.items()}\n                yield cube\n            return\n        # non-terminal\n        i, v, w = self._succ[abs(u)]\n        if not v:\n            raise AssertionError(v)\n        if not w:\n            raise AssertionError(w)\n        d0 = dict(cube)\n        d0[i] = False\n        d1 = dict(cube)\n        d1[i] = True\n        for x in self._sat_iter(v, d0, value):\n            yield x\n        for x in self._sat_iter(w, d1, value):\n            yield x\n\n    def assert_consistent(\n            self\n            ) -> None:\n        \"\"\"Raise `AssertionError` if not a valid BDD.\"\"\"\n        for root in self.roots:\n            if abs(root) not in self._succ:\n                raise AssertionError(root)\n        # inverses\n        succ_keys = set(self._succ)\n        succ_values = set(self._succ.values())\n        pred_keys = set(self._pred)\n        pred_values = set(self._pred.values())\n        if succ_keys != pred_values:\n            raise AssertionError(\n                succ_keys.symmetric_difference(\n                    pred_values))\n        if pred_keys != succ_values:\n            raise AssertionError(\n                pred_keys.symmetric_difference(\n                    succ_values))\n        # uniqueness\n        n = len(succ_keys)\n        n_ = len(succ_values)\n        if n != n_:\n            raise AssertionError(n - n_)\n        for u, (i, v, w) in self._succ.items():\n            if not isinstance(i, int):\n                raise TypeError(i)\n            # terminal ?\n            if v is None:\n                if w is not None:\n                    raise AssertionError(w)\n                continue\n            else:\n                if abs(v) not in self._succ:\n                    raise AssertionError(v)\n            if w is None:\n                if v is not None:\n                    raise AssertionError(v)\n                continue\n            else:\n                # \"high\" is regular edge\n                if w < 0:\n                    raise AssertionError(w)\n                if w not in self._succ:\n                    raise AssertionError(w)\n            # var order should increase\n            for x in (v, w):\n                ix, _, _ = self._succ[abs(x)]\n                if not (i < ix):\n                    raise AssertionError((u, i))\n            # `_pred` contains inverse of `_succ`\n            if (i, v, w) not in self._pred:\n                raise AssertionError((i, v, w))\n            if self._pred[(i, v, w)] != u:\n                raise AssertionError(u)\n            # reference count\n            if u not in self._ref:\n                raise AssertionError(u)\n            if self._ref[u] < 0:\n                raise AssertionError(self._ref[u])\n\n    @_try_to_reorder\n    def add_expr(\n            self,\n            expr:\n                _Formula\n            ) -> _Ref:\n        return _parser.add_expr(expr, self)\n\n    def to_expr(\n            self,\n            u:\n                _Ref\n            ) -> _Formula:\n        if u not in self:\n            raise ValueError(\n                f'{u} is not a reference to '\n                'a BDD node in the BDD manager '\n                f'`self` ({self!r})')\n        cache = dict()\n        return self._to_expr(u, cache)\n\n    def _to_expr(\n            self,\n            u:\n                _Ref,\n            cache:\n                dict[int, str]\n            ) -> _Formula:\n        if u == 1:\n            return 'TRUE'\n        if u == -1:\n            return 'FALSE'\n        if u in cache:\n            return cache[u]\n        level, v, w = self._succ[abs(u)]\n        if not v:\n            raise AssertionError(v)\n        if not w:\n            raise AssertionError(w)\n        var = self._level_to_var[level]\n        p = self._to_expr(v, cache)\n        q = self._to_expr(w, cache)\n        # pure var ?\n        if p == 'FALSE' and q == 'TRUE':\n            expr = var\n        else:\n            expr = f'ite({var}, {q}, {p})'\n        # complemented ?\n        if u < 0:\n            expr = f'(~ {expr})'\n        cache[u] = expr\n        return expr\n\n    def apply(\n            self,\n            op:\n                dd._abc.OperatorSymbol,\n            u:\n                _Ref,\n            v:\n                _Ref |\n                None=None,\n            w:\n                _Ref |\n                None=None\n            ) -> _Ref:\n        _utils.assert_operator_arity(op, v, w, 'bdd')\n        if abs(u) not in self:\n            raise ValueError(u)\n        if v is not None and abs(v) not in self:\n            raise ValueError(v)\n        if w is not None and abs(w) not in self:\n            raise ValueError(w)\n        # unary\n        if op in ('~', 'not', '!'):\n            return -u\n        # Implied by `assert_operator_arity()` above,\n        # present here for type-checking.\n        elif v is None:\n            raise ValueError(\n                '`v is None`')\n        # binary\n        elif op in ('or', r'\\/', '|', '||'):\n            return self.ite(u, 1, v)\n        elif op in ('and', '/\\\\', '&', '&&'):\n            return self.ite(u, v, -1)\n        elif op in ('#', 'xor', '^'):\n            return self.ite(u, -v, v)\n        elif op in ('=>', '->', 'implies'):\n            return self.ite(u, v, 1)\n        elif op in ('<=>', '<->', 'equiv'):\n            return self.ite(u, v, -v)\n        elif op in ('diff', '-'):\n            return self.ite(u, -v, -1)\n        elif op in (r'\\A', 'forall'):\n            qvars = self.support(u)\n            return self.quantify(\n                v, qvars,\n                forall=True)\n        elif op in (r'\\E', 'exists'):\n            qvars = self.support(u)\n            return self.quantify(\n                v, qvars,\n                forall=False)\n        # Implied by `assert_operator_arity()` above,\n        # present here for type-checking.\n        elif w is None:\n            raise ValueError(\n                '`w is None`')\n        # ternary\n        elif op == 'ite':\n            return self.ite(u, v, w)\n        raise ValueError(\n            f'unknown operator \"{op}\"')\n\n    def _add_int(\n            self,\n            i:\n                int\n            ) -> _Ref:\n        if i not in self:\n            raise ValueError(\n                f'{i = } is not a reference '\n                'to a BDD node in the BDD manager '\n                f'`self` ({self!r})')\n        return i\n\n    @_try_to_reorder\n    def cube(\n            self,\n            dvars:\n                _Assignment |\n                _abc.Iterable[\n                    _VariableName]\n            ) -> _Ref:\n        if not isinstance(dvars, dict):\n            dvars = {\n                k: True\n                for k in dvars}\n        # `dvars` keys can be var names or levels\n        r = self.true\n        for var, val in dvars.items():\n            u = self.var(var)\n            u = u if val else -u\n            r = self.apply('and', u, r)\n        return r\n\n    def dump(\n            self,\n            filename:\n                str,\n            roots:\n                dict[str, _Ref] |\n                list[_Ref] |\n                None=None,\n             filetype:\n                dd._abc.ImageFileType |\n                dd._abc.PickleFileType |\n                None=None,\n            **kw\n            ) -> None:\n        if filetype is None:\n            name = filename.lower()\n            if name.endswith('.pdf'):\n                filetype = 'pdf'\n            elif name.endswith('.png'):\n                filetype = 'png'\n            elif name.endswith('.svg'):\n                filetype = 'svg'\n            elif name.endswith('.dot'):\n                filetype = 'dot'\n            elif name.endswith('.p'):\n                filetype = 'pickle'\n            else:\n                raise ValueError(\n                    'cannot infer file type '\n                    'from extension of file '\n                    f'name \"{filename}\"')\n        if filetype in _utils.DOT_FILE_TYPES:\n            self._dump_figure(\n                roots, filename,\n                filetype, **kw)\n        elif filetype == 'pickle':\n            self._dump_bdd(roots, filename, **kw)\n        else:\n            raise ValueError(\n                f'unknown file type \"{filetype}\"')\n\n    def _dump_figure(\n            self,\n            roots:\n                _abc.Iterable[_Ref] |\n                None,\n            filename:\n                str,\n            filetype:\n                dd._abc.ImageFileType,\n            **kw\n            ) -> None:\n        \"\"\"Write BDDs to `filename` as figure.\"\"\"\n        g = _to_dot(roots, self)\n        g.dump(filename, filetype=filetype, **kw)\n\n    def _dump_bdd(\n            self,\n            roots:\n                dict[str, _Ref] |\n                list[_Ref] |\n                None,\n            filename:\n                str,\n            **kw\n            ) -> None:\n        \"\"\"Write BDDs to `filename` as pickle.\"\"\"\n        if roots is None:\n            nodes = self._succ\n            roots = list()\n        else:\n            values = _utils.values_of(roots)\n            nodes = self.descendants(values)\n        succ = (\n            (k, self._succ[k])\n            for k in nodes)\n        d = dict(\n            vars=self.vars,\n            succ=dict(succ),\n            roots=roots)\n        kw.setdefault('protocol', 2)\n        with open(filename, 'wb') as f:\n            pickle.dump(d, f, **kw)\n\n    def load(\n            self,\n            filename:\n                str,\n            levels:\n                _Yes=True\n            ) -> (\n                dict[str, _Ref] |\n                list[_Ref]):\n        name = filename.lower()\n        if not name.endswith('.p'):\n            raise ValueError(\n                f'Unknown file type of \"{filename}\"')\n        umap, roots = self._load_pickle(\n            filename, levels=levels)\n        def map_node(u):\n            v = umap[abs(u)]\n            if u < 0:\n                return - v\n            else:\n                return v\n        return _utils.map_container(\n            map_node, roots)\n\n    def _load_pickle(\n            self,\n            filename:\n                str,\n            levels:\n                _Yes=True\n            ) -> tuple[\n                dict,\n                dict[str, _Ref] |\n                list[_Ref]]:\n        with open(filename, 'rb') as f:\n            d = pickle.load(f)\n        var2level = d['vars']\n        succ = d['succ']\n        n = len(var2level)\n        level_map = dict()\n        # level_map[n] = len(self.vars)\n        for var, i in var2level.items():\n            if not (0 <= i < n):\n                raise AssertionError((i, n))\n            if var not in self.vars:\n                logger.warning(\n                    f'variable \"{var}\" added')\n            if levels:\n                j = self.add_var(var, i)\n            else:\n                j = self.add_var(var)\n            level_map[i] = j\n        umap = dict()\n        for u in succ:\n            # already added ?\n            if u in umap:\n                continue\n            # add\n            self._load(\n                u, succ, umap, level_map)\n        return umap, d['roots']\n\n    def _load(\n            self,\n            u:\n                _Ref,\n            succ:\n                dict,\n            umap:\n                dict,\n            level_map:\n                dict\n            ) -> _Ref:\n        \"\"\"Recurse to load BDD `u` from `succ`.\"\"\"\n        # terminal ?\n        if abs(u) == 1:\n            return u\n        # memoized ?\n        if u in umap:\n            r = umap[abs(u)]\n            if r <= 0:\n                raise AssertionError(r)\n            if u < 0:\n                r = -r\n            return r\n        i, v, w = succ[abs(u)]\n        j = level_map[i]\n        p = self._load(\n            v, succ, umap, level_map)\n        q = self._load(\n            w, succ, umap, level_map)\n        r = self.find_or_add(j, p, q)\n        if r <= 0:\n            raise AssertionError(r)\n        umap[abs(u)] = r\n        if u < 0:\n            r = -r\n        return r\n\n    def _dump_manager(\n            self,\n            filename:\n                str,\n            **kw\n            ) -> None:\n        \"\"\"Write `BDD` to `filename` as pickle.\"\"\"\n        d = dict(\n            vars=self.vars,\n            max_nodes=self.max_nodes,\n            roots=self.roots,\n            pred=self._pred,\n            succ=self._succ,\n            ref=self._ref,\n            min_free=self._min_free)\n        kw.setdefault('protocol', 2)\n        with open(filename, 'wb') as f:\n            pickle.dump(d, f, **kw)\n\n    @classmethod\n    def _load_manager(\n            cls,\n            filename:\n                str\n            ) -> 'BDD':\n        \"\"\"Load `BDD` from pickle file `filename`.\"\"\"\n        with open(filename, 'rb') as f:\n            d = pickle.load(f)\n        bdd = cls(d['vars'])\n        bdd.max_nodes = d['max_nodes']\n        bdd.roots = d['roots']\n        bdd._pred = d['pred']\n        bdd._succ = d['succ']\n        bdd._ref = d['ref']\n        bdd._min_free = d['min_free']\n        return bdd\n\n    @property\n    def false(\n            self\n            ) -> _Ref:\n        return -1\n\n    @property\n    def true(\n            self\n            ) -> _Ref:\n        return 1\n\n\ndef _enumerate_minterms(\n        cube:\n            _Assignment,\n        bits:\n            _abc.Iterable[\n                _VariableName]\n        ) -> _abc.Iterator[\n            _Assignment]:\n    \"\"\"Generator of complete assignments in `cube`.\n\n    @param bits:\n        enumerate over those absent from `cube`\n    \"\"\"\n    if cube is None:\n        raise ValueError(cube)\n    if bits is None:\n        raise ValueError(bits)\n    bits = set(bits).difference(cube)\n    # fix order\n    bits = list(bits)\n    n = len(bits)\n    for i in range(2**n):\n        values = bin(i).lstrip('-0b').zfill(n)\n        model = {\n            k: bool(int(v))\n            for k, v in\n                zip(bits, values)}\n        model.update(cube)\n        if len(model) < len(bits):\n            raise AssertionError((model, bits))\n        if len(model) < len(cube):\n            raise AssertionError((model, cube))\n        yield model\n\n\ndef _assert_isomorphic_orders(\n        old:\n            _VariableLevels,\n        new:\n            _VariableLevels,\n        support:\n            set[_VariableName]\n        ) -> None:\n    \"\"\"Raise `AssertionError` if not isomorphic.\n\n    @param old, new:\n        levels\n    @param support:\n        `old` and `new` compared after\n        restriction to `support`.\n    \"\"\"\n    _assert_valid_ordering(old)\n    _assert_valid_ordering(new)\n    s = {\n        k: v\n        for k, v in\n            old.items()\n        if k in support}\n    t = {\n        k: v\n        for k, v in\n            new.items()\n        if k in support}\n    old = sorted(s, key=s.get)\n    new = sorted(t, key=t.get)\n    if old != new:\n        raise AssertionError((old, new))\n\n\ndef _assert_valid_ordering(\n        levels:\n            _VariableLevels\n        ) -> None:\n    \"\"\"Check that `levels` is well-formed.\n\n    - bijection\n    - contiguous levels\n    \"\"\"\n    # `levels` is a mapping from\n    # each variable to a single level\n    if not isinstance(levels, _abc.Mapping):\n        raise TypeError(levels)\n    # levels are contiguous integers ?\n    n = len(levels)\n    numbers = set(levels.values())\n    numbers_ = set(range(n))\n    if numbers != numbers_:\n        raise AssertionError(n, numbers)\n\n\ndef rename(\n        u:\n            _Ref,\n        bdd:\n            BDD,\n        dvars:\n            _Renaming\n        ) -> _Ref:\n    \"\"\"Rename variables of node `u`.\n\n    @param dvars:\n        `dict` from variabe names to variable names\n    \"\"\"\n    if abs(u) not in bdd:\n        raise ValueError(\n            f'{u} (given as `u`) is not a reference to '\n            'a BDD node in the given BDD manager '\n            f'`bdd` ({bdd!r})')\n    # nothing to rename ?\n    if not dvars:\n        return u\n    # map variable names to levels\n    levels = bdd.vars\n    dvars = {\n        levels[var]: levels[dvars.get(var, var)]\n        for var in bdd.vars}\n    cache = dict()\n    return _copy_bdd(u, dvars, bdd, bdd, cache)\n\n\ndef _assert_valid_rename(\n        u:\n            _Ref,\n        bdd:\n            BDD,\n        dvars:\n            dict[\n                _Level,\n                _Level]\n        ) -> None:\n    \"\"\"Assert renaming to only adjacent variables.\n\n    Raise `AssertionError` if\n    renaming to non-adjacent variables.\n\n    @param dvars:\n        `dict` that maps var levels to var levels\n    \"\"\"\n    if not dvars:\n        return\n    # valid levels ?\n    bdd.var_at_level(0)\n    # pairwise disjoint ?\n    _assert_no_overlap(dvars)\n\n\ndef _all_adjacent(\n        dvars:\n            dict,\n        bdd:\n            BDD\n        ) -> _Yes:\n    \"\"\"Return `True` if all levels are adjacent.\n\n    The pairs of levels checked for\n    being adjacent are the key-value pairs\n    of the mapping `dvars`.\n    \"\"\"\n    for v, vp in dvars.items():\n        if not _adjacent(v, vp, bdd):\n            return False\n    return True\n\n\ndef _adjacent(\n        i:\n            _Level,\n        j:\n            _Level,\n        bdd:\n            BDD\n        ) -> _Yes:\n    \"\"\"Warn if levels `i` and `j` not adjacent.\"\"\"\n    if abs(i - j) == 1:\n        return True\n    logger.warning((\n        'level {i} (\"{x}\") not adjacent to '\n        'level {j} (\"{y}\")').format(\n            i=i,\n            j=j,\n            x=bdd.var_at_level(i),\n            y=bdd.var_at_level(j)))\n    return False\n\n\ndef _assert_no_overlap(\n        d:\n            dict\n        ) -> None:\n    \"\"\"Raise `AssertionError` if keys and values overlap.\"\"\"\n    if any((k in d) for k in d.values()):\n        raise AssertionError(\n            f'keys and values overlap: {d}')\n\n\ndef image(\n        trans:\n            _Ref,\n        source:\n            _Ref,\n        rename:\n            _Renaming |\n            dict[_Level, _Level],\n        qvars:\n            _abc.Iterable[_VariableName] |\n            _abc.Iterable[_Level],\n        bdd:\n            BDD,\n        forall:\n            _Yes=False\n        ) -> _Ref:\n    \"\"\"Return set reachable from `source` under `trans`.\n\n    @param trans:\n        transition relation\n    @param source:\n        the transition must start in this set\n    @param rename:\n        maps primed variables in\n        `trans` to unprimed variables in `trans`.\n        Applied to the quantified conjunction of\n        `trans` and `source`.\n    @param qvars:\n        variables to quantify\n    @param forall:\n        if `True`,\n        then quantify `qvars` universally,\n        else existentially.\n    \"\"\"\n    # map to levels\n    qvars = bdd._map_to_level(set(qvars))\n    rename = {\n        bdd.vars.get(k, k): bdd.vars.get(v, v)\n        for k, v in rename.items()}\n    # init\n    cache = dict()\n    rename_u = rename\n    rename_v = None\n    # no overlap and neighbors\n    _assert_no_overlap(rename)\n    if not _all_adjacent(rename, bdd):\n        logger.warning(\n            'BDD.image: not all vars adjacent')\n    # unpriming maps to qvars or\n    # outside support of conjunction\n    s = bdd.support(trans, as_levels=True)\n    s.update(bdd.support(source, as_levels=True))\n    s.difference_update(qvars)\n    s.intersection_update(rename.values())\n    if s:\n        raise AssertionError(s)\n    return _image(\n        trans, source, rename_u, rename_v,\n        qvars, bdd, forall, cache)\n\n\ndef preimage(\n        trans:\n            _Ref,\n        target:\n            _Ref,\n        rename:\n            _Renaming |\n            dict[_Level, _Level],\n        qvars:\n            _abc.Iterable[_VariableName] |\n            _abc.Iterable[_Level],\n        bdd:\n            BDD,\n        forall:\n            _Yes=False\n        ) -> _Ref:\n    \"\"\"Return set that can reach `target` under `trans`.\n\n    Also known as the \"relational product\".\n    Assumes that primed and\n    unprimed variables are neighbors.\n    Variables are identified by their levels.\n\n    @param trans:\n        transition relation\n    @param target:\n        the transition must end in this set\n    @param rename:\n        maps (unprimed) variables in `target` to\n        (primed) variables in `trans`\n    @param qvars:\n        variables to quantify\n    @param forall:\n        if `True`,\n        then quantify `qvars` universally,\n        else existentially.\n    \"\"\"\n    # map to levels\n    qvars = bdd._map_to_level(set(qvars))\n    rename = {\n        bdd.vars.get(k, k): bdd.vars.get(v, v)\n        for k, v in rename.items()}\n    # init\n    cache = dict()\n    rename_u = None\n    rename_v = rename\n    # check\n    _assert_valid_rename(target, bdd, rename)\n    return _image(\n        trans, target, rename_u, rename_v,\n        qvars, bdd, forall, cache)\n\n\ndef _image(\n        u:\n            _Ref,\n        v:\n            _Ref,\n        umap:\n            dict |\n            None,\n        vmap:\n            dict |\n            None,\n        qvars:\n            set[_Level],\n        bdd:\n            BDD,\n        forall:\n            _Yes,\n        cache:\n            dict[\n                tuple[_Ref, _Ref],\n                _Ref]\n        ) -> _Ref:\n    \"\"\"Recursive (pre)image computation.\n\n    Renaming requires that in each pair\n    the variables are adjacent.\n\n    @param umap:\n        renaming of variables in `u`\n        that occurs after conjunction of `u` with `v`\n        and quantification.\n    @param vmap:\n        renaming of variables in `v`\n        that occurs before conjunction with `u`.\n    \"\"\"\n    # controlling values for conjunction ?\n    if u == -1 or v == -1:\n        return -1\n    if u == 1 and v == 1:\n        return 1\n    # already computed ?\n    t = (u, v)\n    w = cache.get(t)\n    if w is not None:\n        return w\n    # recurse (descend)\n    iu, _, _ = bdd._succ[abs(u)]\n    jv, _, _ = bdd._succ[abs(v)]\n    if vmap is None:\n        iv = jv\n    else:\n        iv = vmap.get(jv, jv)\n    z = min(iu, iv)\n    u0, u1 = bdd._top_cofactor(u, z)\n    v0, v1 = bdd._top_cofactor(v, jv + z - iv)\n    p = _image(\n        u0, v0, umap, vmap, qvars,\n        bdd, forall, cache)\n    q = _image(\n        u1, v1, umap, vmap, qvars,\n        bdd, forall, cache)\n    # quantified ?\n    if z in qvars:\n        if forall:\n            r = bdd.ite(p, q, -1)\n                # conjoin\n        else:\n            r = bdd.ite(p, 1, q)\n                # disjoin\n    else:\n        if umap is None:\n            m = z\n        else:\n            m = umap.get(z, z)\n        g = bdd.find_or_add(m, -1, 1)\n        r = bdd.ite(g, q, p)\n    cache[t] = r\n    return r\n\n\ndef reorder(\n        bdd:\n            BDD,\n        order:\n            _VariableLevels |\n            None=None\n        ) -> None:\n    \"\"\"Apply Rudell's sifting algorithm to reduce `bdd` size.\n\n    Reordering invokes the garbage collector,\n    so be sure to `incref` nodes that should remain.\n\n    @param order:\n        if given, then swap vars to obtain this order.\n        The dictionary `order` maps each\n        variable name to a level.\n    \"\"\"\n    len_before = len(bdd)\n    if order is None:\n        _apply_sifting(bdd)\n    else:\n        _sort_to_order(bdd, order)\n    len_after = len(bdd)\n    logger.info(\n        'Reordering changed `BDD` manager size '\n        f'from {len_before} to {len_after} nodes.')\n\n\ndef _apply_sifting(\n        bdd:\n            BDD\n        ) -> None:\n    \"\"\"Apply Rudell's sifting algorithm.\"\"\"\n    bdd.collect_garbage()\n    n = len(bdd)\n    # using `set` injects some randomness\n    levels = bdd._levels()\n    names = set(bdd.vars)\n    for var in names:\n        k = _reorder_var(bdd, var, levels)\n        m = len(bdd)\n        logger.info(\n            f'{m} nodes for variable '\n            f'\"{var}\" at level {k}')\n    if m > n:\n        raise AssertionError(\n            f'expected: m <= n, but {m = } > {n = }')\n    logger.info(f'final variable order:\\n{bdd.vars}')\n\n\ndef _reorder_var(\n        bdd:\n            BDD,\n        var:\n            _VariableName,\n        levels:\n            dict[\n                _Level,\n                set[_Ref]]\n        ) -> _Nat:\n    \"\"\"Reorder by sifting a variable `var`.\"\"\"\n    if var not in bdd.vars:\n        raise ValueError((var, bdd.vars))\n    m = len(bdd)\n    n = len(bdd.vars) - 1\n    if n < 0:\n        raise AssertionError(n)\n    start = 0\n    end = n\n    level = bdd.level_of_var(var)\n    # closer to bottom ?\n    if (2 * level) >= n:\n        start, end = end, start\n    _shift(bdd, level, start, levels)\n    sizes = _shift(bdd, start, end, levels)\n    k = min(sizes, key=sizes.get)\n    _shift(bdd, end, k, levels)\n    m_ = len(bdd)\n    if sizes[k] != m_:\n        raise AssertionError((sizes[k], m_))\n    if m_ > m:\n        raise AssertionError((m_, m))\n    return k\n\n\ndef _shift(\n        bdd:\n            BDD,\n        start:\n            _Level,\n        end:\n            _Level,\n        levels:\n            dict[\n                _Level,\n                set[_Ref]]\n        ) -> dict[\n            _Level,\n            _Level]:\n    r\"\"\"Shift level `start` to become `end`, by swapping.\n\n    ```tla\n    ASSUMPTION\n        LET\n            n_vars == len(bdd.vars)\n            level_range == 0..(n_vars - 1)\n        IN\n            /\\ start \\in level_range\n            /\\ end \\in level_range\n    ```\n    \"\"\"\n    m = len(bdd.vars)\n    if not (0 <= start < m):\n        raise AssertionError((start, m))\n    if not (0 <= end < m):\n        raise AssertionError((end, m))\n    sizes = dict()\n    d = 1 if start < end else -1\n    for i in range(start, end, d):\n        j = i + d\n        oldn, n = bdd.swap(i, j, levels)\n        sizes[i] = oldn\n        sizes[j] = n\n    return sizes\n\n\ndef _sort_to_order(\n        bdd:\n            BDD,\n        order:\n            _VariableLevels\n        ) -> None:\n    \"\"\"Swap variables to obtain `order`.\"\"\"\n    # TODO: use min number of swaps\n    if len(bdd.vars) != len(order):\n        raise ValueError(\n            'The number of BDD variables: '\n            f'{len(bdd.vars) = } is not equal to: '\n            f'{len(order) = }')\n    m = 0\n    levels = bdd._levels()\n    n = len(order)\n    for k in range(n):\n        for i in range(n - 1):\n            for root in bdd.roots:\n                if root not in bdd:\n                    raise ValueError(\n                        f'{root} in `bdd.roots` is not '\n                        'a reference to a BDD node in '\n                        'the given BDD manager `bdd` '\n                        f'({bdd!r})')\n            x = bdd.var_at_level(i)\n            y = bdd.var_at_level(i + 1)\n            p = order[x]\n            q = order[y]\n            if p > q:\n                bdd.swap(i, i + 1, levels)\n                m += 1\n                logger.debug(\n                    f'swap: {p} with {q}, {i}')\n            if logger.getEffectiveLevel() < logging.DEBUG:\n                bdd.assert_consistent()\n    logger.info(f'total swaps: {m}')\n\n\ndef reorder_to_pairs(\n        bdd:\n            BDD,\n        pairs:\n            _Renaming\n        ) -> None:\n    \"\"\"Reorder variables to make adjacent the given pairs.\n\n    @param pairs:\n        has variable names as keys and values\n    \"\"\"\n    m = 0\n    levels = bdd._levels()\n    for x, y in pairs.items():\n        jx = bdd.level_of_var(x)\n        jy = bdd.level_of_var(y)\n        k = abs(jx - jy)\n        if k <= 0:\n            raise AssertionError((jx, jy))\n        # already adjacent ?\n        if k == 1:\n            continue\n        # shift x next to y\n        if jx > jy:\n            jx, jy = jy, jx\n        _shift(bdd, start=jx, end=jy - 1, levels=levels)\n        m += k\n        logger.debug(f'shift by {k}')\n    logger.info(f'total swaps: {m}')\n\n\ndef copy_bdd(\n        u:\n            _Ref,\n        from_bdd:\n            BDD,\n        to_bdd:\n            BDD\n        ) -> _Ref:\n    \"\"\"Copy BDD of node `u` `from_bdd` `to_bdd`.\n\n    @param u:\n        node in `from_bdd`\n    \"\"\"\n    if from_bdd is to_bdd:\n        logger.warning(\n            'copying node to same BDD manager')\n        return u\n    level_map = {\n        from_bdd.level_of_var(var):\n            to_bdd.level_of_var(var)\n        for var in from_bdd.vars\n        if var in to_bdd.vars}\n    r = _copy_bdd(\n        u, level_map,\n        from_bdd, to_bdd,\n        cache=dict())\n    return r\n\n\ndef _copy_bdd(\n        u:\n            _Ref,\n        level_map:\n            dict[_Level, _Level],\n        old_bdd:\n            BDD,\n        bdd:\n            BDD,\n        cache:\n            dict[_Node, _Ref]\n        ) -> _Ref:\n    \"\"\"Recurse to copy nodes from `old_bdd` to `bdd`.\n\n    @param u:\n        node in `old_bdd`\n    @param level_map:\n        maps old to new levels\n    \"\"\"\n    # terminal ?\n    if abs(u) == 1:\n        return u\n    # non-terminal\n    # memoized ?\n    r = cache.get(abs(u))\n    if r is not None:\n        if r <= 0:\n            raise AssertionError(r)\n        # complement ?\n        if u < 0:\n            r = -r\n        return r\n    # recurse\n    jold, v, w = old_bdd._succ[abs(u)]\n    if not v:\n        raise AssertionError(v)\n    if not w:\n        raise AssertionError(w)\n    p = _copy_bdd(\n        v, level_map,\n        old_bdd, bdd,\n        cache)\n    q = _copy_bdd(\n        w, level_map,\n        old_bdd, bdd,\n        cache)\n    if p * v <= 0:\n        raise AssertionError((p, v))\n    if q <= 0:\n        raise AssertionError(q)\n    # map this level\n    jnew = level_map[jold]\n    g = bdd.find_or_add(jnew, -1, 1)\n    r = bdd.ite(g, q, p)\n    # memoize\n    if r <= 0:\n        raise AssertionError(r)\n    cache[abs(u)] = r\n    # complement ?\n    if u < 0:\n        r = -r\n    return r\n\n\ndef _flip(\n        r:\n            _Ref,\n        u:\n            _Ref\n        ) -> _Ref:\n    \"\"\"Flip `r` if `u` is negated, else identity.\"\"\"\n    return -r if u < 0 else r\n\n\ndef to_nx(\n        bdd:\n            BDD,\n        roots:\n            set[_Ref]\n        ) -> '_utils.MultiDiGraph':\n    \"\"\"Convert node references in `roots` to graph.\n\n    The resulting graph has:\n\n      - nodes labeled with:\n        - `level`:\n          `int` from 0 to `len(bdd)`\n      - edges labeled with:\n        - `value`:\n          `False` for low/\"else\",\n          `True` for high/\"then\"\n        - `complement`:\n          `True` if target node is negated\n\n    @param roots:\n        iterable of edges, each a signed `int`\n    \"\"\"\n    _nx = _utils.import_module('networkx')\n    g = _nx.MultiDiGraph()\n    for root in roots:\n        if abs(root) not in bdd:\n            raise ValueError(root)\n        Q = {root}\n        while Q:\n            u = Q.pop()\n            u = abs(u)\n            i, v, w = bdd._succ[u]\n            if u <= 0:\n                raise AssertionError(u)\n            g.add_node(u, level=i)\n            # terminal ?\n            if v is None or w is None:\n                if v is not None:\n                    raise AssertionError(v)\n                if w is not None:\n                    raise AssertionError(w)\n                continue\n            # non-terminal\n            r = (v < 0)\n            v = abs(v)\n            w = abs(w)\n            if v not in g:\n                Q.add(v)\n            if w not in g:\n                Q.add(w)\n            if v <= 0:\n                raise AssertionError(v)\n            if w <= 0:\n                raise AssertionError(w)\n            g.add_edge(\n                u, v,\n                value=False,\n                complement=r)\n            g.add_edge(\n                u, w,\n                value=True,\n                complement=False)\n    return g\n\n\ndef _to_dot(\n        roots:\n            _abc.Iterable[_Ref] |\n            None,\n        bdd:\n            BDD\n        ) -> _utils.DotGraph:\n    \"\"\"Convert `BDD` to DOT graph.\n\n    Nodes are ordered by variable levels in support.\n    Edges to low successors are dashed.\n    Complemented edges are labeled with \"-1\".\n\n    Nodes not reachable from `roots`\n    are ignored, unless `roots is None`.\n\n    The roots are plotted as external references,\n    with complemented edges where applicable.\n    \"\"\"\n    # all nodes ?\n    if roots is None:\n        nodes = bdd._succ\n        roots = list()\n    else:\n        nodes = bdd.descendants(roots)\n    # show only levels in aggregate support\n    levels = {\n        bdd._succ[abs(u)][0]\n        for u in nodes}\n    if bdd._succ[1][0] not in levels:\n        raise AssertionError(\n            'level of node 1 is missing from computed '\n            'set of BDD nodes reachable from `roots`')\n    g = _utils.DotGraph(\n        graph_type='digraph')\n    skeleton = list()\n    subgraphs = dict()\n    # layer for external BDD references\n    layers = [-1] + sorted(levels)\n    # add nodes for BDD levels\n    for i in layers:\n        h = _utils.DotGraph(\n            rank='same')\n        g.subgraphs.append(h)\n        subgraphs[i] = h\n        # add phantom node\n        u = f'\"L{i}\"'\n        skeleton.append(u)\n        if i == -1:\n            # layer for external BDD references\n            label = 'ref'\n        else:\n            # BDD level\n            label = str(i)\n        h.add_node(\n            u,\n            label=label,\n            shape='none')\n    # auxiliary edges for ranking\n    for i, u in enumerate(skeleton[:-1]):\n        v = skeleton[i + 1]\n        g.add_edge(\n            u, v,\n            style='invis')\n    # add nodes\n    idx2var = {\n        k: v\n        for v, k in bdd.vars.items()}\n    # BDD nodes\n    def f(x):\n        return str(abs(x))\n    for u in nodes:\n        i, v, w = bdd._succ[abs(u)]\n        # terminal ?\n        if v is None:\n            var = str(bool(abs(u)))\n        else:\n            var = idx2var[i]\n        su = f(u)\n        label = f'{var}-{su}'\n        # add node to subgraph for level i\n        h = subgraphs[i]\n        h.add_node(\n            su,\n            label=label)\n        # add edges\n        if v is None:\n            continue\n        sv = f(v)\n        sw = f(w)\n        kw = dict(style='dashed')\n        if v < 0:\n            kw['taillabel'] = '-1'\n        g.add_edge(\n            su, sv,\n            **kw)\n        g.add_edge(\n            su, sw,\n            style='solid')\n    # external references to BDD nodes\n    for u in roots:\n        i, _, _ = bdd._succ[abs(u)]\n        su = f'\"ref{u}\"'\n        label = f'@{u}'\n        # add node to subgraph for level -1\n        h = subgraphs[-1]\n        h.add_node(\n            su,\n            label=label)\n        # add edge from external reference to BDD node\n        if u is None:\n            raise ValueError(f'{u} in `roots`')\n        sv = str(abs(u))\n        kw = dict(style='dashed')\n        if u < 0:\n            kw.update(taillabel='-1')\n        g.add_edge(\n            su, sv,\n            **kw)\n    return g\n"
  },
  {
    "path": "dd/buddy.pyx",
    "content": "# cython: profile=True\n\"\"\"Cython interface to BuDDy.\n\n\nReference\n=========\n    Jorn Lind-Nielsen\n    \"BuDDy: Binary Decision Diagram package\"\n    IT-University of Copenhagen (ITU)\n    v2.4, 2002\n    <https://sourceforge.net/projects/buddy/>\n\"\"\"\n# Copyright 2015 by California Institute of Technology\n# All rights reserved. Licensed under BSD-3.\n#\nimport collections.abc as _abc\nimport logging\nimport pprint\nimport sys\nimport typing as _ty\n\nfrom cpython cimport bool as _py_bool\nfrom cpython.mem cimport PyMem_Malloc, PyMem_Free\nimport cython\nfrom libc.stdio cimport fdopen, fopen\n\nimport dd._abc as _dd_abc\ncimport dd.buddy_ as buddy\n\n\nctypedef cython.int _c_int\n_Yes: _ty.TypeAlias = _py_bool\n_Cardinality: _ty.TypeAlias = _dd_abc.Cardinality\n_VariableName: _ty.TypeAlias = _dd_abc.VariableName\n_Level: _ty.TypeAlias = _dd_abc.Level\n_Renaming: _ty.TypeAlias = _dd_abc.Renaming\n_OperatorSymbol: _ty.TypeAlias = _ty.Literal[\n    '!',\n    'not',\n    '&',\n    'and',\n    '|',\n    'or',\n    '#',\n    '^',\n    'xor']\n_OPERATOR_SYMBOLS: _ty.Final = set(_ty.get_args(\n    _OperatorSymbol))\n\n\nAPPLY_MAP = {\n    'and':\n        0,\n    'xor':\n        1,\n    'or':\n        2,\n    'nand':\n        3,\n    'nor':\n        4,\n    'imp':\n        5,\n    'biimp':\n        6,\n    'diff':\n        7,\n    'less':\n        8,\n    'invimp':\n        9}\nBDD_REORDER_NONE = 0\nBDD_REORDER_WIN2 = 1\nBDD_REORDER_WIN2ITE = 2\n    # \"ite\" = iteratively\nBDD_REORDER_SIFT = 3\nBDD_REORDER_SIFTITE = 4\nBDD_REORDER_WIN3 = 5\nBDD_REORDER_WIN3ITE = 6\nBDD_REORDER_RANDOM = 7\nBDD_REORDER_FREE = 0\nBDD_REORDER_FIXED = 1\n\n\nlogger = logging.getLogger(__name__)\n\n\ncdef class BDD:\n    \"\"\"Wrapper of BuDDy.\n\n    Interface similar to `dd.bdd.BDD`.\n    There is only a single global shared BDD,\n    so use only one instance.\n    \"\"\"\n\n    cdef public object var_to_index\n\n    def __cinit__(\n            self\n            ) -> None:\n        self.var_to_index = dict()\n        if buddy.bdd_isrunning():\n            return\n        n_nodes = 10**2\n        cache = 10**4\n        n_vars = 150\n        buddy.bdd_init(n_nodes, cache)\n        buddy.bdd_setvarnum(n_vars)\n        buddy.bdd_setcacheratio(64)\n        buddy.bdd_autoreorder(BDD_REORDER_SIFT)\n        buddy.bdd_reorder_verbose(1)\n\n    def __dealloc__(\n            self\n            ) -> None:\n        buddy.bdd_done()\n\n    def __str__(\n            self\n            ) -> str:\n        n = buddy.bdd_getnodenum()\n        n_alloc = buddy.bdd_getallocnum()\n        n_vars = buddy.bdd_varnum()\n        s = (\n            'Binary decision diagram (BuDDy wrapper) with:\\n'\n            f'\\t {n} live nodes now\\n'\n            f'\\t {n_alloc} total nodes allocated\\n'\n            f'\\t {n_vars} BDD variables\\n')\n        return s\n\n    def __len__(\n            self\n            ) -> _Cardinality:\n        return buddy.bdd_getnodenum()\n\n    cdef incref(\n            self,\n            u:\n                _c_int):\n        buddy.bdd_addref(u)\n\n    cdef decref(\n            self,\n            u:\n                _c_int):\n        buddy.bdd_delref(u)\n\n    property false:\n\n        def __get__(\n                self\n                ) -> Function:\n            return self._bool(False)\n\n    property true:\n\n        def __get__(\n                self\n                ) -> Function:\n            return self._bool(True)\n\n    cdef Function _bool(\n            self,\n            b:\n                _py_bool):\n        if b:\n            r = buddy.bdd_true()\n        else:\n            r = buddy.bdd_false()\n        return Function(r)\n\n    cpdef int add_var(\n            self,\n            var:\n                _VariableName):\n        \"\"\"Return index for variable `var`.\"\"\"\n        j = self.var_to_index.get(var)\n        if j is not None:\n            return j\n        j = len(self.var_to_index)\n        self.var_to_index[var] = j\n        # new block for reordering\n        buddy.bdd_intaddvarblock(j, j, 0)\n        return j\n\n    cpdef Function var(\n            self,\n            var:\n                _VariableName):\n        \"\"\"Return BDD for variable `var`.\"\"\"\n        if var not in self.var_to_index:\n            raise ValueError(\n                f'\"{var}\" is not a variable (key) in '\n                f'{self.var_to_index = }')\n        j = self.var_to_index[var]\n        r = buddy.bdd_ithvar(j)\n        if r == self.false.node:\n            raise RuntimeError('failed')\n        buddy.bdd_intaddvarblock(j, j, 0)\n        return Function(r)\n\n    cpdef int level_of_var(\n            self,\n            var:\n                _VariableName):\n        \"\"\"Return level of variable `var`.\"\"\"\n        if var not in self.var_to_index:\n            raise ValueError(\n                f'undeclared variable \"{var}\", '\n                'known variables are:\\n'\n                f'{self.var_to_index}')\n        j = self.var_to_index[var]\n        level = buddy.bdd_var2level(j)\n        return level\n\n    cpdef str var_at_level(\n            self,\n            level:\n                _Level):\n        \"\"\"Return variable at `level`.\"\"\"\n        index = buddy.bdd_level2var(level)\n        # unknown variable error ?\n        if index == buddy.BDD_VAR:\n            levels = {\n                var: self.level_of_var(var)\n                for var in self.var_to_index}\n            raise ValueError(\n                f'no variable has level:  {level}, '\n                'the current levels of all variables '\n                f'are:  {levels}')\n        index_to_var = {\n            v: k for k, v in self.var_to_index.items()}\n        var = index_to_var[index]\n        return var\n\n    cpdef Function apply(\n            self,\n            op:\n                _OperatorSymbol,\n            u:\n                Function,\n            v:\n                Function |\n                None=None):\n        \"\"\"Return as `Function` the result of applying `op`.\"\"\"\n        if op not in _OPERATOR_SYMBOLS:\n            raise ValueError(\n                f'unknown operator: \"{op}\"')\n        # unary\n        if op in ('!', 'not'):\n            if v is not None:\n                raise ValueError((op, u, v))\n            r = buddy.bdd_not(u.node)\n        elif v is None:\n            raise ValueError((op, u, v))\n        # binary\n        if op in ('&', 'and'):\n            r = buddy.bdd_and(u.node, v.node)\n        elif op in ('|', 'or'):\n            r = buddy.bdd_or(u.node, v.node)\n        elif op in ('#', '^', 'xor'):\n            r = buddy.bdd_xor(u.node, v.node)\n        return Function(r)\n\n    cpdef Function quantify(\n            self,\n            u:\n                Function,\n            qvars:\n                _abc.Iterable[\n                    _VariableName],\n            forall:\n                _Yes=False):\n        cube = self.cube(qvars)\n        if forall:\n            r = buddy.bdd_forall(u, cube)\n        else:\n            r = buddy.bdd_exist(u, cube)\n        return Function(r)\n\n    cpdef Function cube(\n            self,\n            dvars:\n                _abc.Iterable[\n                    _VariableName]):\n        \"\"\"Return a positive unate cube for `dvars`.\"\"\"\n        n = len(dvars)\n        cdef int *x\n        x = <int *> PyMem_Malloc(n * sizeof(int))\n        for i, var in enumerate(dvars):\n            j = self.add_var(var)\n            x[i] = j\n        try:\n            r = buddy.bdd_makeset(x, n)\n        finally:\n            PyMem_Free(x)\n        return Function(r)\n\n    cpdef assert_consistent(\n            self):\n        raise NotImplementedError('TODO')\n\n\ncpdef Function and_abstract(\n        u:\n            Function,\n        v:\n            Function,\n        qvars:\n            _abc.Iterable[\n                _VariableName],\n        bdd:\n            BDD):\n    \"\"\"Return `? qvars. u & v`.\"\"\"\n    cube = bdd.cube(qvars)\n    op = APPLY_MAP['and']\n    r = buddy.bdd_appex(u.node, v.node, op, cube.node)\n    return Function(r)\n\n\ncpdef Function or_abstract(\n        u:\n            Function,\n        v:\n            Function,\n        qvars:\n            _abc.Iterable[\n                _VariableName],\n        bdd:\n            BDD):\n    \"\"\"Return `! qvars. u | v`.\"\"\"\n    cube = bdd.cube(qvars)\n    op = APPLY_MAP['or']\n    r = buddy.bdd_appall(u.node, v.node, op, cube.node)\n    return Function(r)\n\n\ndef rename(\n        u:\n            Function,\n        bdd:\n            BDD,\n        dvars:\n            _Renaming\n        ) -> Function:\n    n = len(dvars)\n    cdef int *oldvars\n    cdef int *newvars\n    oldvars = <int *> PyMem_Malloc(n * sizeof(int))\n    newvars = <int *> PyMem_Malloc(n * sizeof(int))\n    for i, (a, b) in enumerate(dvars.items()):\n        ja = bdd.add_var(a)\n        jb = bdd.add_var(b)\n        oldvars[i] = ja\n        newvars[i] = jb\n    cdef buddy.bddPair *pair = buddy.bdd_newpair()\n    try:\n        buddy.bdd_setpairs(pair, oldvars, newvars, n)\n        r = buddy.bdd_replace(u.node, pair)\n    finally:\n        buddy.bdd_freepair(pair)\n        PyMem_Free(oldvars)\n        PyMem_Free(newvars)\n    return Function(r)\n\n\ncdef class Function:\n    \"\"\"Wrapper for nodes of `BDD`.\n\n    Takes care of reference counting,\n    using the `weakref`s.\n\n    Use as:\n\n    ```cython\n    bdd = BDD()\n    u = bdd_true()\n    f = Function(u)\n    h = g | ~ f\n    ```\n    \"\"\"\n\n    __weakref__: object\n    cdef public int node\n\n    def __cinit__(\n            self,\n            node:\n                _c_int\n            ) -> None:\n        self.node = node\n        buddy.bdd_addref(node)\n\n    def __dealloc__(\n            self\n            ) -> None:\n        buddy.bdd_delref(self.node)\n        self.node = -1\n\n    def __str__(\n            self\n            ) -> str:\n        n = len(self)\n        return f'Function({self.node}, {n})'\n\n    def __len__(\n            self\n            ) -> _Cardinality:\n        return buddy.bdd_nodecount(self.node)\n\n    def __eq__(\n            self,\n            other:\n                Function |\n                None\n            ) -> _Yes:\n        if other is None:\n            return False\n        other_: Function = other\n        return self.node == other_.node\n\n    def __ne__(\n            self,\n            other:\n                Function |\n                None\n            ) -> _Yes:\n        if other is None:\n            return True\n        other_: Function = other\n        return self.node != other_.node\n\n    def __invert__(\n            self\n            ) -> Function:\n        r = buddy.bdd_not(self.node)\n        return Function(r)\n\n    def __and__(\n            self,\n            other:\n                Function\n            ) -> Function:\n        r = buddy.bdd_and(self.node, other.node)\n        return Function(r)\n\n    def __or__(\n            self,\n            other:\n                Function\n            ) -> Function:\n        r = buddy.bdd_or(self.node, other.node)\n        return Function(r)\n\n    def __xor__(\n            self,\n            other:\n                Function\n            ) -> Function:\n        r = buddy.bdd_xor(self.node, other.node)\n        return Function(r)\n"
  },
  {
    "path": "dd/buddy_.pxd",
    "content": "# cython: profile=True\n\"\"\"Cython extern declarations from BuDDy.\n\n\nReference\n=========\n    Jorn Lind-Nielsen\n    \"BuDDy: Binary Decision Diagram package\"\n    IT-University of Copenhagen (ITU)\n    v2.4, 2002\n    <https://sourceforge.net/projects/buddy/>\n\"\"\"\n# Copyright 2015 by California Institute of Technology\n# All rights reserved. Licensed under BSD-3.\n#\nfrom libc.stdio cimport FILE\n\n\ncdef extern from 'bdd.h':\n    int BDD_VAR\n    # BDD\n    ctypedef int BDD\n    # renaming pair\n    struct s_bddPair:\n        pass\n    ctypedef s_bddPair bddPair\n    int bdd_init(int BDDsize, int cachesize)\n    int bdd_isrunning()\n    void bdd_done()\n    int bdd_setacheratio(int r)\n    # variable creation\n    BDD bdd_ithvar(int var)\n    BDD bdd_nithvar(int var)\n    int bdd_var2level(int var)\n    int bdd_level2var(int level)\n    int bdd_setvarnum(int num)\n    int bdd_extvarnum(int num)\n    int bdd_varnum()\n    # variable manipulation\n    int bdd_var(BDD r)\n    BDD bdd_makeset(int *varset, int varnum)\n    int bdd_scanset(BDD a, int **varset, int *varnum)\n    BDD bdd_ibuildcube(int value, int width, int *var)\n    BDD bdd_buildcube(int value, int width, BDD *var)\n    # BDD elements\n    BDD bdd_true()\n    BDD bdd_false()\n    BDD bdd_low(BDD r)\n    BDD bdd_high(BDD r)\n    BDD bdd_support(BDD r)\n    BDD bdd_satone(BDD r)  # cube\n    BDD bdd_fullsatone(BDD r)  # minterm\n    double bdd_satcount(BDD r)\n    int bdd_nodecount(BDD r)\n    # refs\n    BDD bdd_addref(BDD r)\n    BDD bdd_delref(BDD r)\n    void bdd_gbc()\n    # basic Boolean operators\n    BDD bdd_ite(BDD u, BDD v, BDD w)\n    BDD bdd_apply(BDD u, BDD w, int op)\n    BDD bdd_not(BDD u)\n    BDD bdd_and(BDD u, BDD v)\n    BDD bdd_or(BDD u, BDD v)\n    BDD bdd_xor(BDD u, BDD v)\n    BDD bdd_imp(BDD u, BDD v)\n    BDD bdd_biimp(BDD u, BDD v)\n    # composition operators\n    BDD bdd_restrict(BDD r, BDD var)\n    BDD bdd_constrain(BDD f, BDD c)\n    BDD bdd_compose(BDD f, BDD g, BDD v)\n    BDD bdd_simplify(BDD f, BDD d)\n    # quantification\n    BDD bdd_exist(BDD r, BDD var)\n    BDD bdd_forall(BDD r, BDD var)\n    BDD bdd_appex(BDD u, BDD v, int op, BDD var)\n    BDD bdd_appall(BDD u, BDD v, int op, BDD var)\n    # renaming\n    BDD bdd_replace(BDD r, bddPair *pair)\n    bddPair * bdd_newpair()\n    void bdd_freepair(bddPair *p)\n    int bdd_setpair(bddPair *pair, int oldvar, int newvar)\n    int bdd_setpairs(\n        bddPair *pair, int *oldvar,\n        int *newvar, int size)\n    void bdd_resetpair(bddPair *pair)\n    void bdd_freepair(bddPair *p)\n    # manager config\n    int bdd_setmaxBDDnum(int size)\n    int bdd_setmaxincrease(int size)\n    int bdd_setminfreeBDDs(int mf)\n    int bdd_getnodenum()\n    int bdd_getallocnum()  # both unused and active\n    # reordering\n    int bdd_addvarblock(BDD b, int fixed)\n    int bdd_intaddvarblock(int first, int last, int fixed)\n    void bdd_varblockall()\n    void bdd_reorder(int method)\n    int bdd_autoreorder(int method)\n    int bdd_autoreorder_times(int method, int num)\n    void bdd_enable_reorder()\n    void bdd_disable_reorder()\n    int bdd_reorder_gain()\n    void bdd_setcacheratio(int r)\n    # I/O\n    int bdd_save(FILE *ofile, BDD r)\n    int bdd_load(FILE *ifile, BDD r)\n    # info\n    int bdd_reorder_verbose(int value)\n    void bdd_printorder()\n    void bdd_fprintorder(FILE *ofile)\n    # void bdd_stats(bddStat *stat)\n    # void bdd_cachestats(bddCacheStat *s)\n    void bdd_fprintstat(FILE *f)\n    void bdd_printstat()\n"
  },
  {
    "path": "dd/c_sylvan.pxd",
    "content": "\"\"\"Cython extern declarations from Sylvan.\n\n\nReference\n=========\n    Tom van Dijk, Alfons Laarman, Jaco van de Pol\n    \"Multi-Core BDD Operations for\n     Symbolic Reachability\"\n    PDMC 2012\n    <https://doi.org/10.1016/j.entcs.2013.07.009>\n\"\"\"\n# Copyright 2016 by California Institute of Technology\n# All rights reserved. Licensed under BSD-3.\n#\ncimport libc.stdint as stdint\nfrom libcpp cimport bool\n\n\ncdef extern from 'lace.h':\n    \"\"\"\n    #define LACE_ME_WRAP 0); LACE_ME (0\n    \"\"\"\n    void LACE_ME_WRAP()\n    ctypedef struct WorkerP\n    ctypedef struct Task\ncdef extern from 'sylvan.h':\n    ctypedef stdint.uint64_t BDD\n    ctypedef stdint.uint64_t BDDSET\n    ctypedef stdint.uint32_t BDDVAR\n    ctypedef stdint.uint64_t BDDMAP\n    #\n    stdint.uint64_t sylvan_complement\n    stdint.uint64_t sylvan_false\n    stdint.uint64_t sylvan_true\n    BDD sylvan_invalid\n    # int gc_enabled  # should not be `static`\n    #\n    ctypedef void (*lace_startup_cb)(\n        WorkerP*, Task*, void*)\n    # node elements\n    BDD sylvan_ithvar(BDDVAR var)\n    BDD sylvan_nithvar(BDD var)\n    BDDVAR sylvan_var(BDD bdd)\n    BDD sylvan_low(BDD bdd)\n    BDD sylvan_high(BDD bdd)\n    bool sylvan_isconst(BDD bdd)\n    bool sylvan_isnode(BDD bdd)\n    size_t sylvan_nodecount(BDD a)\n    size_t sylvan_count_refs()\n    # main Boolean operators\n    BDD sylvan_not(BDD a)\n    BDD sylvan_and(BDD a, BDD b)\n    BDD sylvan_xor(BDD a, BDD b)\n    BDD sylvan_ite(BDD a, BDD b, BDD c)\n    # derived operators\n    BDD sylvan_equiv(BDD a, BDD b)\n    BDD sylvan_or(BDD a, BDD b)\n    BDD sylvan_imp(BDD a, BDD b)\n    BDD sylvan_biimp(BDD a, BDD b)\n    BDD sylvan_diff(BDD a, BDD b)\n    # compose\n    BDD sylvan_support(BDD bdd)\n    BDD sylvan_constrain(BDD f, BDD c)\n    BDD sylvan_restrict(BDD f, BDD c)\n    BDD sylvan_compose(BDD f, BDDMAP m)\n    BDDMAP sylvan_map_empty()\n    BDDMAP sylvan_map_add(\n        BDDMAP map, BDDVAR key, BDD value)\n    # enumeration\n    double sylvan_satcount(\n        BDD bdd, BDDSET variables)\n    BDD sylvan_pick_cube(BDD bdd)\n    double sylvan_pathcount(BDD bdd)\n    # refs\n    BDD sylvan_ref(BDD a)\n    void sylvan_deref(BDD a)\n    # logistics\n    void lace_exit()\n    void lace_init(int n_workers, size_t dqsize)\n    void lace_startup(\n        size_t stacksize,\n        lace_startup_cb cb,\n        void* arg)\n    void sylvan_init_package(\n        size_t initial_tablesize,\n        size_t max_tablesize,\n        size_t initial_cachesize,\n        size_t max_cachesize)\n    void sylvan_init_bdd(int granularity)\n    void sylvan_quit()\n    # quantification\n    BDD sylvan_exists(BDD a, BDD qvars)\n    BDD sylvan_forall(BDD a, BDD qvars)\n    BDD sylvan_and_exists(\n        BDD a, BDD b, BDD qvars)\n    BDD sylvan_relprev(BDD a, BDD b, BDD qvars)\n    BDD sylvan_relnext(BDD a, BDD b, BDD qvars)\n    BDD sylvan_closure(BDD a)\n    # TODO: `stats.h`\n"
  },
  {
    "path": "dd/cudd.pyx",
    "content": "\"\"\"Cython interface to CUDD.\n\nVariable `__version__` equals CUDD's version string.\n\n\nReference\n=========\n    Fabio Somenzi\n    \"CUDD: CU Decision Diagram Package\"\n    University of Colorado at Boulder\n    v2.5.1, 2015\n    <http://vlsi.colorado.edu/~fabio/>\n\"\"\"\n# Copyright 2015 by California Institute of Technology\n# All rights reserved. Licensed under BSD-3.\n#\nimport collections.abc as _abc\nimport logging\nimport pickle\nimport pprint\nimport sys\nimport textwrap as _tw\nimport time\nimport typing as _ty\nimport warnings\n\nfrom cpython cimport bool as python_bool\nfrom cpython.mem cimport PyMem_Malloc, PyMem_Free\ncimport libc.stdint as stdint\nfrom libc.stdio cimport FILE, fdopen, fopen, fclose\nfrom libcpp cimport bool\n\nimport dd._abc as _dd_abc\nfrom dd import _copy\nfrom dd import _parser\nfrom dd import _utils\nfrom dd import autoref\nfrom dd import bdd as _bdd\n\n\n_Yes: _ty.TypeAlias = python_bool\n_Nat: _ty.TypeAlias = _dd_abc.Nat\n_Cardinality: _ty.TypeAlias = _dd_abc.Cardinality\n_NumberOfBytes: _ty.TypeAlias = _dd_abc.NumberOfBytes\n_VariableName: _ty.TypeAlias = _dd_abc.VariableName\n_Level: _ty.TypeAlias = _dd_abc.Level\n_VariableLevels: _ty.TypeAlias = _dd_abc.VariableLevels\n_Assignment: _ty.TypeAlias = _dd_abc.Assignment\n_Renaming: _ty.TypeAlias = _dd_abc.Renaming\n_Formula: _ty.TypeAlias = _dd_abc.Formula\n_BDDFileType: _ty.TypeAlias = (\n    _dd_abc.BDDFileType |\n    _ty.Literal['dddmp'])\n\n\ncdef extern from 'mtr.h':\n    struct MtrNode_:\n        pass\n    ctypedef MtrNode_ MtrNode\ncdef MTR_DEFAULT = 0\ncdef MTR_FIXED = 4\ncdef extern from 'cuddInt.h':\n    char* CUDD_VERSION\n    int CUDD_CONST_INDEX\n    # subtable (for a level)\n    struct DdSubtable:\n        unsigned int slots\n        unsigned int keys\n    # manager\n    struct DdManager:\n        DdSubtable *subtables\n        unsigned int keys\n        unsigned int dead\n        double cachecollisions\n        double cacheinserts\n        double cachedeletions\n    DdNode *cuddUniqueInter(\n        DdManager *unique,\n        int index,\n        DdNode *T, DdNode *E)\ncdef extern from 'cudd.h':\n    # node\n    ctypedef unsigned int DdHalfWord\n    struct DdNode:\n        DdHalfWord index\n        DdHalfWord ref\n    ctypedef DdNode DdNode\n\n    ctypedef DdManager DdManager\n    DdManager *Cudd_Init(\n        unsigned int numVars,\n        unsigned int numVarsZ,\n        unsigned int numSlots,\n        unsigned int cacheSize,\n        size_t maxMemory)\n    struct DdGen\n    ctypedef enum Cudd_ReorderingType:\n        pass\n    # node elements\n    DdNode *Cudd_bddNewVar(\n        DdManager *dd)\n    DdNode *Cudd_bddNewVarAtLevel(\n        DdManager *dd, int level)\n    DdNode *Cudd_bddIthVar(\n        DdManager *dd, int index)\n    DdNode *Cudd_ReadLogicZero(\n        DdManager *dd)\n    DdNode *Cudd_ReadOne(\n        DdManager *dd)\n    DdNode *Cudd_Regular(\n        DdNode *u)\n    bool Cudd_IsConstant(\n        DdNode *u)\n    unsigned int Cudd_NodeReadIndex(\n        DdNode *u)\n    DdNode *Cudd_T(\n        DdNode *u)\n    DdNode *Cudd_E(\n        DdNode *u)\n    bool Cudd_IsComplement(\n        DdNode *u)\n    int Cudd_DagSize(\n        DdNode *node)\n    int Cudd_SharingSize(\n        DdNode **nodeArray, int n)\n    # basic Boolean operators\n    DdNode *Cudd_Not(\n        DdNode *dd)\n    DdNode *Cudd_bddIte(\n        DdManager *dd, DdNode *f,\n        DdNode *g, DdNode *h)\n    DdNode *Cudd_bddAnd(\n        DdManager *dd,\n        DdNode *f, DdNode *g)\n    DdNode *Cudd_bddOr(\n        DdManager *dd,\n        DdNode *f, DdNode *g)\n    DdNode *Cudd_bddXor(\n        DdManager *dd,\n        DdNode *f, DdNode *g)\n    DdNode *Cudd_bddXnor(\n        DdManager *dd,\n        DdNode *f, DdNode *g)\n    int Cudd_bddLeq(\n        DdManager *dd,\n        DdNode *f,\n        DdNode *g)\n    DdNode *Cudd_Support(\n        DdManager *dd, DdNode *f)\n    DdNode *Cudd_bddComputeCube(\n        DdManager *dd,\n        DdNode **vars, int *phase, int n)\n    DdNode *Cudd_CubeArrayToBdd(\n        DdManager *dd, int *array)\n    int Cudd_BddToCubeArray(\n        DdManager *dd,\n        DdNode *cube, int *array)\n    int Cudd_PrintMinterm(\n        DdManager *dd, DdNode *f)\n    DdNode *Cudd_Cofactor(\n        DdManager *dd, DdNode *f, DdNode *g)\n    DdNode *Cudd_bddCompose(\n        DdManager *dd,\n        DdNode *f, DdNode *g, int v)\n    DdNode *Cudd_bddVectorCompose(\n        DdManager *dd,\n        DdNode *f, DdNode **vector)\n    DdNode *Cudd_bddRestrict(\n        DdManager *dd, DdNode *f, DdNode *c)\n    # cubes\n    DdGen *Cudd_FirstCube(\n        DdManager *dd, DdNode *f,\n        int **cube, double *value)\n    int Cudd_NextCube(\n        DdGen *gen, int **cube, double *value)\n    int Cudd_IsGenEmpty(\n        DdGen *gen)\n    int Cudd_GenFree(\n        DdGen *gen)\n    double Cudd_CountMinterm(\n        DdManager *dd, DdNode *f, int nvars)\n    # refs\n    void Cudd_Ref(\n        DdNode *n)\n    void Cudd_RecursiveDeref(\n        DdManager *table, DdNode *n)\n    void Cudd_Deref(\n        DdNode *n)\n    # checks\n    int Cudd_CheckZeroRef(\n        DdManager *manager)\n    int Cudd_DebugCheck(\n        DdManager *table)\n    void Cudd_Quit(\n        DdManager *unique)\n    DdNode *Cudd_bddTransfer(\n        DdManager *ddSource,\n        DdManager *ddDestination,\n        DdNode *f)\n    # info\n    int Cudd_PrintInfo(\n        DdManager *dd, FILE *fp)\n    int Cudd_ReadSize(\n        DdManager *dd)\n    long Cudd_ReadNodeCount(\n        DdManager *dd)\n    long Cudd_ReadPeakNodeCount(\n        DdManager *dd)\n    int Cudd_ReadPeakLiveNodeCount(\n        DdManager *dd)\n    size_t Cudd_ReadMemoryInUse(\n        DdManager *dd)\n    unsigned int Cudd_ReadSlots(\n        DdManager *dd)\n    double Cudd_ReadUsedSlots(\n        DdManager *dd)\n    double Cudd_ExpectedUsedSlots(\n        DdManager *dd)\n    unsigned int Cudd_ReadCacheSlots(\n        DdManager *dd)\n    double Cudd_ReadCacheUsedSlots(\n        DdManager *dd)\n    double Cudd_ReadCacheLookUps(\n        DdManager *dd)\n    double Cudd_ReadCacheHits(\n        DdManager *dd)\n    # reordering\n    int Cudd_ReduceHeap(\n        DdManager *table,\n        Cudd_ReorderingType heuristic,\n        int minsize)\n    int Cudd_ShuffleHeap(\n        DdManager *table, int *permutation)\n    void Cudd_AutodynEnable(\n        DdManager *unique,\n        Cudd_ReorderingType method)\n    void Cudd_AutodynDisable(\n        DdManager *unique)\n    int Cudd_ReorderingStatus(\n        DdManager *unique,\n        Cudd_ReorderingType *method)\n    unsigned int Cudd_ReadReorderings(\n        DdManager *dd)\n    long Cudd_ReadReorderingTime(\n        DdManager *dd)\n    int Cudd_ReadPerm(\n        DdManager *dd, int index)\n    int Cudd_ReadInvPerm(\n        DdManager *dd, int level)\n    void Cudd_SetSiftMaxSwap(\n        DdManager *dd, int sms)\n    int Cudd_ReadSiftMaxSwap(\n        DdManager *dd)\n    void Cudd_SetSiftMaxVar(\n        DdManager *dd, int smv)\n    int Cudd_ReadSiftMaxVar(\n        DdManager *dd)\n    # variable grouping\n    extern MtrNode *Cudd_MakeTreeNode(\n        DdManager *dd, unsigned int low,\n        unsigned int size, unsigned int type)\n    extern MtrNode *Cudd_ReadTree(\n        DdManager *dd)\n    extern void Cudd_SetTree(\n        DdManager *dd, MtrNode *tree)\n    extern void Cudd_FreeTree(\n        DdManager *dd)\n    # manager config\n    size_t Cudd_ReadMaxMemory(\n        DdManager *dd)\n    size_t Cudd_SetMaxMemory(\n        DdManager *dd,\n        size_t maxMemory)\n    unsigned int Cudd_ReadMaxCacheHard(\n        DdManager *dd)\n    unsigned int Cudd_ReadMaxCache(\n        DdManager *dd)\n    void Cudd_SetMaxCacheHard(\n        DdManager *dd, unsigned int mc)\n    double Cudd_ReadMaxGrowth(\n        DdManager *dd)\n    void Cudd_SetMaxGrowth(\n        DdManager *dd, double mg)\n    unsigned int Cudd_ReadMinHit(\n        DdManager *dd)\n    void Cudd_SetMinHit(\n        DdManager *dd, unsigned int hr)\n    void Cudd_EnableGarbageCollection(\n        DdManager *dd)\n    void Cudd_DisableGarbageCollection(\n        DdManager *dd)\n    int Cudd_GarbageCollectionEnabled(\n        DdManager * dd)\n    unsigned int Cudd_ReadLooseUpTo(\n        DdManager *dd)\n    void Cudd_SetLooseUpTo(\n        DdManager *dd, unsigned int lut)\n    # quantification\n    DdNode *Cudd_bddExistAbstract(\n        DdManager *manager,\n        DdNode *f,\n        DdNode *cube)\n    DdNode *Cudd_bddUnivAbstract(\n        DdManager *manager,\n        DdNode *f,\n        DdNode *cube)\n    DdNode *Cudd_bddAndAbstract(\n        DdManager *manager,\n        DdNode *f, DdNode *g,\n        DdNode *cube)\n    DdNode *Cudd_bddSwapVariables(\n        DdManager *dd,\n        DdNode *f, DdNode **x, DdNode **y,\n        int n)\ncdef extern from '_cudd_addendum.c':\n    DdNode *Cudd_bddTransferRename(\n        DdManager *ddSource,\n        DdManager *ddDestination,\n        DdNode *f, int *renaming)\nctypedef DdNode *DdRef\ncdef CUDD_UNIQUE_SLOTS = 2**8\ncdef CUDD_CACHE_SLOTS = 2**18\ncdef CUDD_REORDER_GROUP_SIFT = 14\ncdef CUDD_OUT_OF_MEM = -1\ncdef MAX_CACHE = <unsigned int> - 1  # entries\n__version__ = CUDD_VERSION.decode('utf-8')\n\n\n# TODO: replace DDDMP\ncdef extern from 'dddmp.h':\n    ctypedef enum Dddmp_VarInfoType:\n        pass\n    ctypedef enum Dddmp_VarMatchType:\n        pass\n    int Dddmp_cuddBddStore(\n        DdManager *ddMgr,\n        char *ddname,\n        DdNode *f,\n        char **varnames,\n        int *auxids,\n        int mode,\n        Dddmp_VarInfoType varinfo,\n        char *fname,\n        FILE *fp)\n    DdNode *Dddmp_cuddBddLoad(\n        DdManager *ddMgr,\n        Dddmp_VarMatchType varMatchMode,\n        char **varmatchnames,\n        int *varmatchauxids,\n        int *varcomposeids,\n        int mode,\n        char *fname,\n        FILE *fp)\ncdef DDDMP_MODE_TEXT = 65  # <int>'A'\ncdef DDDMP_VARIDS = 0\ncdef DDDMP_VARNAMES = 3\ncdef DDDMP_VAR_MATCHNAMES = 3\ncdef DDDMP_SUCCESS = 1\n\n\n# 2**30 = 1 GiB (gibibyte, read ISO/IEC 80000)\nDEFAULT_MEMORY = 1 * 2**30\nlogger = logging.getLogger(__name__)\n\n\ncdef class BDD:\n    \"\"\"Wrapper of CUDD manager.\n\n    Interface similar to `dd.bdd.BDD`.\n    Variable names are strings.\n    Attributes:\n\n      - `vars`: `set` of bit names as `str`ings\n    \"\"\"\n\n    cdef DdManager *manager\n    cdef public object vars\n    cdef public object _index_of_var\n    cdef public object _var_with_index\n\n    def __cinit__(\n            self,\n            memory_estimate:\n                _NumberOfBytes |\n                None=None,\n            initial_cache_size:\n                _Cardinality |\n                None=None,\n            *arg,\n            **kw\n            ) -> None:\n        \"\"\"Initialize BDD manager.\n\n        @param memory_estimate:\n            maximum allowed memory, in bytes.\n        \"\"\"\n        self.manager = NULL  # prepare for\n            # `__dealloc__`,\n            # in case an exception is raised below.\n            # Including `*arg, **kw` in the\n            # signature of the method `__cinit__`\n            # aims to prevent an exception from\n            # being raised upon instantiation\n            # of the class `BDD` before the\n            # body of the method `__cinit__`\n            # is entered.\n            # In that case, `self.manager`\n            # could in principle have an\n            # arbitrary value when `__dealloc__`\n            # is executed.\n        total_memory = _utils.total_memory()\n        default_memory = DEFAULT_MEMORY\n        if memory_estimate is None:\n            memory_estimate = default_memory\n        if total_memory is None:\n            pass\n        elif memory_estimate >= total_memory:\n            msg = (\n                'Error in `dd.cudd`: '\n                'total physical memory '\n                f'is {total_memory} bytes, '\n                f'but requested {memory_estimate} bytes. '\n                'Please pass an amount of memory to '\n                'the `BDD` constructor to avoid this error. '\n                'For example, by instantiating '\n                'the `BDD` manager as '\n                f'`BDD({round(total_memory / 2)})`.')\n            # The motivation of both printing and\n            # raising an exception was that Cython\n            # failed with a segmentation fault,\n            # without showing the exception message.\n            print(msg)\n            raise ValueError(msg)\n        if initial_cache_size is None:\n            initial_cache_size = CUDD_CACHE_SLOTS\n        initial_subtable_size = CUDD_UNIQUE_SLOTS\n        initial_n_vars_bdd = 0\n        initial_n_vars_zdd = 0\n        mgr = Cudd_Init(\n            initial_n_vars_bdd,\n            initial_n_vars_zdd,\n            initial_subtable_size,\n            initial_cache_size,\n            memory_estimate)\n        if mgr is NULL:\n            raise RuntimeError(\n                'failed to initialize CUDD DdManager')\n        self.manager = mgr\n\n    def __init__(\n            self,\n            memory_estimate:\n                _NumberOfBytes |\n                None=None,\n            initial_cache_size:\n                _Cardinality |\n                None=None\n            ) -> None:\n        logger.info(f'Using CUDD v{__version__}')\n        self.configure(\n            reordering=True,\n            max_cache_hard=MAX_CACHE)\n        self.vars = set()\n        # map: str -> unique fixed int\n        self._index_of_var = dict()\n        self._var_with_index = dict()\n\n    def __dealloc__(\n            self\n            ) -> None:\n        if self.manager is NULL:\n            raise RuntimeError(\n                '`self.manager` is `NULL`, '\n                'which suggests that '\n                'an exception was raised '\n                'inside the method '\n                '`dd.cudd.BDD.__cinit__`.')\n        n = Cudd_CheckZeroRef(self.manager)\n        if n != 0:\n            raise AssertionError(\n                f'Still {n} nodes '\n                'referenced upon shutdown.')\n        # Exceptions raised inside `__dealloc__` will be\n        # ignored. So if the `AssertionError` above is\n        # raised, then Python will continue execution\n        # without calling `Cudd_Quit`.\n        #\n        # Even though this can cause a memory leak,\n        # incorrect reference counts imply that there\n        # already is some issue, and that calling\n        # `Cudd_Quit` might be unsafe.\n        Cudd_Quit(self.manager)\n\n    def __eq__(\n            self:\n                BDD,\n            other:\n                _ty.Optional[BDD]\n            ) -> _Yes:\n        \"\"\"Return `True` if `other` has same manager.\"\"\"\n        if other is None:\n            return False\n        return self.manager == other.manager\n\n    def __ne__(\n            self:\n                BDD,\n            other:\n                _ty.Optional[BDD]\n            ) -> _Yes:\n        if other is None:\n            return True\n        return self.manager != other.manager\n\n    def __len__(\n            self\n            ) -> _Cardinality:\n        \"\"\"Number of nodes with nonzero references.\"\"\"\n        return Cudd_CheckZeroRef(self.manager)\n\n    def __contains__(\n            self,\n            u:\n                Function\n            ) -> _Yes:\n        if u.manager != self.manager:\n            raise ValueError(\n                'undefined containment, because '\n                '`u.manager != self.manager`')\n        try:\n            Cudd_NodeReadIndex(u.node)\n            return True\n        except:\n            return False\n\n    def __str__(\n            self\n            ) -> str:\n        d = self.statistics()\n        s = (\n            'Binary decision diagram '\n                '(CUDD wrapper) with:\\n'\n            '\\t {n} live nodes now\\n'\n            '\\t {peak} live nodes at peak\\n'\n            '\\t {n_vars} BDD variables\\n'\n            '\\t {mem:10.1f} bytes in use\\n'\n            '\\t {reorder_time:10.1f} sec '\n                'spent reordering\\n'\n            '\\t {n_reorderings} reorderings\\n'\n            ).format(\n                n=d['n_nodes'],\n                peak=d['peak_live_nodes'],\n                n_vars=d['n_vars'],\n                reorder_time=d['reordering_time'],\n                n_reorderings=d['n_reorderings'],\n                mem=d['mem'])\n        return s\n\n    def statistics(\n            self:\n                BDD,\n            exact_node_count:\n                _Yes=False\n            ) -> dict[\n                str,\n                _ty.Any]:\n        \"\"\"Return `dict` with CUDD node counts and times.\n\n        If `exact_node_count` is `True`, then the\n        list of dead nodes is cleared.\n\n        Keys with meaning:\n\n          - `n_vars`: number of variables\n          - `n_nodes`: number of live nodes\n          - `peak_nodes`: max number of all nodes\n          - `peak_live_nodes`: max number of live nodes\n\n          - `reordering_time`: sec spent reordering\n          - `n_reorderings`: number of reorderings\n\n          - `mem`: bytes in use\n          - `unique_size`: total number of\n            buckets in unique table\n          - `unique_used_fraction`: buckets that\n            contain >= 1 node\n          - `expected_unique_used_fraction`:\n            if properly working\n\n          - `cache_size`: number of slots in cache\n          - `cache_used_fraction`: slots with data\n          - `cache_lookups`: total number of lookups\n          - `cache_hits`: total number of cache hits\n          - `cache_insertions`\n          - `cache_collisions`\n          - `cache_deletions`\n        \"\"\"\n        warnings.warn(\n            \"Changed in `dd` version 0.5.7: \"\n            \"In the `dict` returned by the method \"\n            \"`dd.cudd.BDD.statistics`, \"\n            \"the value of the key `'mem'` \"\n            \"has changed to bytes (from 10**6 bytes).\",\n            UserWarning)\n        cdef DdManager *mgr\n        mgr = self.manager\n        n_vars = Cudd_ReadSize(mgr)\n        # nodes\n        if exact_node_count:\n            n_nodes = Cudd_ReadNodeCount(mgr)\n        else:\n            n_nodes = mgr.keys - mgr.dead\n        peak_nodes = Cudd_ReadPeakNodeCount(mgr)\n        peak_live_nodes = Cudd_ReadPeakLiveNodeCount(mgr)\n        # reordering\n        t = Cudd_ReadReorderingTime(mgr)\n        reordering_time = t / 1000.0\n        n_reorderings = Cudd_ReadReorderings(mgr)\n        # memory\n        m = Cudd_ReadMemoryInUse(mgr)\n        mem = float(m)\n        # unique table\n        unique_size = Cudd_ReadSlots(mgr)\n        unique_used_fraction = Cudd_ReadUsedSlots(mgr)\n        expected_unique_fraction = (\n            Cudd_ExpectedUsedSlots(mgr))\n        # cache\n        cache_size = Cudd_ReadCacheSlots(mgr)\n        cache_used_fraction = Cudd_ReadCacheUsedSlots(mgr)\n        cache_lookups = Cudd_ReadCacheLookUps(mgr)\n        cache_hits = Cudd_ReadCacheHits(mgr)\n        cache_insertions = mgr.cacheinserts\n        cache_collisions = mgr.cachecollisions\n        cache_deletions = mgr.cachedeletions\n        d = dict(\n            n_vars=n_vars,\n            n_nodes=n_nodes,\n            peak_nodes=peak_nodes,\n            peak_live_nodes=peak_live_nodes,\n            reordering_time=reordering_time,\n            n_reorderings=n_reorderings,\n            mem=mem,\n            unique_size=unique_size,\n            unique_used_fraction=unique_used_fraction,\n            expected_unique_used_fraction=\n                expected_unique_fraction,\n            cache_size=cache_size,\n            cache_used_fraction=cache_used_fraction,\n            cache_lookups=cache_lookups,\n            cache_hits=cache_hits,\n            cache_insertions=cache_insertions,\n            cache_collisions=cache_collisions,\n            cache_deletions=cache_deletions)\n        return d\n\n    def configure(\n            self:\n                BDD,\n            **kw\n            ) -> dict[\n                str,\n                _ty.Any]:\n        \"\"\"Read and apply parameter values.\n\n        First read (returned), then apply `kw`.\n        Available keyword arguments:\n\n          - `'reordering'`:\n                if `True` then enable,\n                else disable\n          - `'garbage_collection'`:\n                if `True` then enable,\n                else disable\n          - `'max_memory'`: in bytes\n          - `'loose_up_to'`:\n                unique table fast growth\n                upper bound\n          - `'max_cache_hard'`:\n                cache entries upper bound\n          - `'min_hit'`:\n                hit ratio for resizing cache\n          - `'max_growth'`:\n                intermediate growth\n                during sifting\n          - `'max_swaps'`:\n                no more level swaps\n                in one sifting\n          - `'max_vars'`:\n                no more variables moved\n                in one sifting\n\n        For more details, read `cuddAPI.c`.\n        Example usage:\n\n        ```python\n        import dd.cudd\n\n        bdd = dd.cudd.BDD()\n        # store old settings, and apply new settings\n        cfg = bdd.configure(\n            max_memory=12 * 1024**3,\n            loose_up_to=5 * 10**6,\n            max_cache_hard=MAX_CACHE,\n            min_hit=20,\n            max_growth=1.5)\n        # something fancy\n        # ...\n        # restore old settings\n        bdd.configure(**cfg)\n        ```\n        \"\"\"\n        cdef int method\n        cdef DdManager *mgr\n        mgr = self.manager\n        # read\n        reordering = Cudd_ReorderingStatus(\n            mgr, <Cudd_ReorderingType *>&method)\n        garbage_collection = (\n            Cudd_GarbageCollectionEnabled(mgr))\n        max_memory = Cudd_ReadMaxMemory(mgr)\n        loose_up_to = Cudd_ReadLooseUpTo(mgr)\n        max_cache_soft = Cudd_ReadMaxCache(mgr)\n        max_cache_hard = Cudd_ReadMaxCacheHard(mgr)\n        min_hit = Cudd_ReadMinHit(mgr)\n        max_growth = Cudd_ReadMaxGrowth(mgr)\n        max_swaps = Cudd_ReadSiftMaxSwap(mgr)\n        max_vars = Cudd_ReadSiftMaxVar(mgr)\n        d = dict(\n            reordering=True if reordering == 1\n                else False,\n            garbage_collection=True\n                if garbage_collection == 1\n                else False,\n            max_memory=max_memory,\n            loose_up_to=loose_up_to,\n            max_cache_soft=max_cache_soft,\n            max_cache_hard=max_cache_hard,\n            min_hit=min_hit,\n            max_growth=max_growth,\n            max_swaps=max_swaps,\n            max_vars=max_vars)\n        # set\n        for k, v in kw.items():\n            if k == 'reordering':\n                if v:\n                    Cudd_AutodynEnable(\n                        mgr, CUDD_REORDER_GROUP_SIFT)\n                else:\n                    Cudd_AutodynDisable(mgr)\n            elif k == 'garbage_collection':\n                if v:\n                    Cudd_EnableGarbageCollection(mgr)\n                else:\n                    Cudd_DisableGarbageCollection(mgr)\n            elif k == 'max_memory':\n                Cudd_SetMaxMemory(mgr, v)\n            elif k == 'loose_up_to':\n                Cudd_SetLooseUpTo(mgr, v)\n            elif k == 'max_cache_hard':\n                Cudd_SetMaxCacheHard(mgr, v)\n            elif k == 'min_hit':\n                Cudd_SetMinHit(mgr, v)\n            elif k == 'max_growth':\n                Cudd_SetMaxGrowth(mgr, v)\n            elif k == 'max_swaps':\n                Cudd_SetSiftMaxSwap(mgr, v)\n            elif k == 'max_vars':\n                Cudd_SetSiftMaxVar(mgr, v)\n            elif k == 'max_cache_soft':\n                logger.warning(\n                    '\"max_cache_soft\" not settable.')\n            else:\n                raise ValueError(\n                    f'Unknown parameter \"{k}\"')\n        return d\n\n    cpdef tuple succ(\n            self,\n            u:\n                Function):\n        \"\"\"Return `(level, low, high)` for `u`.\"\"\"\n        if u.manager != self.manager:\n            raise ValueError(\n                '`u.manager != self.manager`')\n        i = u.level\n        v = u.low\n        w = u.high\n        return i, v, w\n\n    cpdef incref(\n            self,\n            u:\n                Function):\n        \"\"\"Increment the reference count of `u`.\n\n        Raise `RuntimeError` if `u._ref <= 0`.\n        For more details about avoiding this\n        read the docstring of the class `Function`.\n\n        The reference count of the BDD node in CUDD\n        that `u` points to is incremented.\n\n        Also, the attribute `u._ref` is incremented.\n\n        Calling this method is unnecessary,\n        because reference counting is automated.\n        \"\"\"\n        if u.node is NULL:\n            raise RuntimeError(\n                '`u.node` is `NULL` pointer.')\n        if u._ref <= 0:\n            _utils.raise_runtimerror_about_ref_count(\n                u._ref, 'method `dd.cudd.BDD.incref`',\n                '`dd.cudd.Function`')\n        assert u._ref > 0, u._ref\n        u._ref += 1\n        self._incref(u.node)\n\n    cpdef decref(\n            self,\n            u:\n                Function,\n            recursive:\n                _Yes=False,\n            _direct:\n                _Yes=False):\n        \"\"\"Decrement the reference count of `u`.\n\n        Raise `RuntimeError` if `u._ref <= 0`\n        or `u.node is NULL`.\n        For more details about avoiding this\n        read the docstring of the class `Function`.\n\n        The reference count of the BDD node in CUDD\n        that `u` points to is decremented.\n\n        Also, the attribute `u._ref` is decremented.\n        If after this decrement, `u._ref == 0`,\n        then the pointer `u.node` is set to `NULL`.\n\n        Calling this method is unnecessary,\n        because reference counting is automated.\n\n        If early dereferencing of the node is desired\n        in order to allow garbage collection,\n        then write `del u`, instead of calling\n        this method.\n\n        @param recursive:\n            if `True`, then call\n            `Cudd_RecursiveDeref`,\n            else call `Cudd_Deref`\n        @param _direct:\n            use this parameter only after\n            reading the source code of the\n            Cython file `dd/cudd.pyx`.\n            When `_direct == True`, some of the above\n            description does not apply.\n        \"\"\"\n        if u.node is NULL:\n            raise RuntimeError(\n                '`u.node` is `NULL` pointer.')\n        # bypass checks and leave `u._ref` unchanged,\n        # directly call `_decref`\n        if _direct:\n            self._decref(u.node, recursive)\n            return\n        if u._ref <= 0:\n            _utils.raise_runtimerror_about_ref_count(\n                u._ref, 'method `dd.cudd.BDD.decref`',\n                '`dd.cudd.Function`')\n        assert u._ref > 0, u._ref\n        u._ref -= 1\n        self._decref(u.node, recursive)\n        if u._ref == 0:\n            u.node = NULL\n\n    cdef _incref(\n            self,\n            u:\n                DdRef):\n        Cudd_Ref(u)\n\n    cdef _decref(\n            self,\n            u:\n                DdRef,\n            recursive:\n                _Yes=False):\n        # There is little point in checking here\n        # the reference count of `u`, because\n        # doing that relies on the assumption\n        # that `u` still corresponds to a node,\n        # which implies that the reference count\n        # is positive.\n        #\n        # This point should not be reachable\n        # after `u` reaches zero reference count.\n        #\n        # Moreover, if the memory has been deallocated,\n        # then in principle the attribute `ref`\n        # can have any value, so an assertion here\n        # would not be ensuring correctness.\n        if recursive:\n            Cudd_RecursiveDeref(self.manager, u)\n        else:\n            Cudd_Deref(u)\n\n    def declare(\n            self,\n            *variables:\n                _VariableName\n            ) -> None:\n        \"\"\"Add names in `variables` to `self.vars`.\"\"\"\n        for var in variables:\n            self.add_var(var)\n\n    cpdef int add_var(\n            self,\n            var:\n                _VariableName,\n            index:\n                _Nat |\n                None=None):\n        \"\"\"Return index of variable named `var`.\n\n        If a variable named `var` exists,\n        the assert that it has `index`.\n        Otherwise, create a variable named `var`\n        with `index` (if given).\n\n        If no reordering has yet occurred,\n        then the returned index equals the level,\n        provided `add_var` has been used so far.\n        \"\"\"\n        # var already exists ?\n        j = self._index_of_var.get(var)\n        if j is not None:\n            if index is not None and j != index:\n                raise AssertionError(j, index)\n            return j\n        # new var\n        if index is None:\n            j = len(self._index_of_var)\n        else:\n            j = index\n        u = Cudd_bddIthVar(self.manager, j)\n        if u is NULL:\n            raise RuntimeError(\n                f'failed to add var \"{var}\"')\n        self._add_var(var, j)\n        return j\n\n    cpdef int insert_var(\n            self,\n            var:\n                _VariableName,\n            level:\n                _Level):\n        \"\"\"Create new variable at `level`.\n\n        The name of the variable is\n        the string `var`.\n\n        @param var:\n            name of variable\n            that this function\n            will declare\n        @param level:\n            where the new\n            variable will be placed\n            in the variable order\n            of this BDD manager\n        @return:\n            number that CUDD\n            uses to identify the\n            newly created variable.\n            This number is also\n            called an index of\n            the variable.\n        @rtype:\n            `int` >= 0\n        \"\"\"\n        r: DdRef\n        r = Cudd_bddNewVarAtLevel(\n            self.manager, level)\n        if r is NULL:\n            raise RuntimeError(\n                f'failed to create var \"{var}\"')\n        j = r.index\n        self._add_var(var, j)\n        return j\n\n    cdef _add_var(\n            self,\n            var:\n                _VariableName,\n            index:\n                _Nat):\n        \"\"\"Declare new variable `var`.\n\n        Adds to `self` a *new* variable\n        named `var`, identified within\n        CUDD by the number `index`.\n\n        @param var:\n            name of variable that\n            this function will declare\n        @param index:\n            number that\n            will identify within CUDD\n            the newly created variable\n        \"\"\"\n        if var in self.vars:\n            raise ValueError(\n                f'existing variable: \"{var}\"')\n        if var in self._index_of_var:\n            raise ValueError(\n                'variable already has index: {i}'.format(\n                    i=self._index_of_var[var]))\n        if index in self._var_with_index:\n            raise ValueError((\n                'index already corresponds '\n                'to a variable: {v}').format(\n                    v=self._var_with_index[index]))\n        self.vars.add(var)\n        self._index_of_var[var] = index\n        self._var_with_index[index] = var\n        if (len(self._index_of_var) !=\n                len(self._var_with_index)):\n            raise AssertionError(\n                'the attributes '\n                '`_index_of_var` and '\n                '`_var_with_index` '\n                'have different length')\n\n    cpdef Function var(\n            self,\n            var:\n                _VariableName):\n        \"\"\"Return node for variable named `var`.\"\"\"\n        if var not in self._index_of_var:\n            raise ValueError(\n                f'undeclared variable \"{var}\", '\n                'the declared variables are:\\n'\n                f'{self._index_of_var}')\n        j = self._index_of_var[var]\n        r = Cudd_bddIthVar(self.manager, j)\n        return wrap(self, r)\n\n    def var_at_level(\n            self,\n            level:\n                _Level\n            ) -> _VariableName:\n        \"\"\"Return name of variable at `level`.\n\n        Raise `ValueError` if `level` is not\n        the level of any variable declared in\n        `self.vars`.\n        \"\"\"\n        j = Cudd_ReadInvPerm(self.manager, level)\n        if (j == -1 or j == CUDD_CONST_INDEX or\n                j not in self._var_with_index):\n            raise ValueError(_tw.dedent(f'''\n                No declared variable has level: {level}.\n                {_utils.var_counts(self)}\n                '''))\n        var = self._var_with_index[j]\n        return var\n\n    def level_of_var(\n            self,\n            var:\n                _VariableName\n            ) -> _Level:\n        \"\"\"Return level of variable named `var`.\n\n        Raise `ValueError` if `var` is not\n        a variable in `self.vars`.\n        \"\"\"\n        if var not in self._index_of_var:\n            raise ValueError(\n                f'undeclared variable \"{var}\", '\n                'the declared variables are:\\n'\n                f'{self._index_of_var}')\n        j = self._index_of_var[var]\n        level = Cudd_ReadPerm(self.manager, j)\n        if level == -1:\n            raise AssertionError(\n                f'index {j} out of bounds')\n        return level\n\n    @property\n    def var_levels(\n            self\n            ) -> _VariableLevels:\n        return {\n            var: self.level_of_var(var)\n            for var in self.vars}\n\n    def _number_of_cudd_vars(\n            self\n            ) -> _Cardinality:\n        \"\"\"Return number of CUDD indices.\n\n        Can be `> len(self.vars)`.\n        \"\"\"\n        n_cudd_vars = Cudd_ReadSize(self.manager)\n        if 0 <= n_cudd_vars <= CUDD_CONST_INDEX:\n            return n_cudd_vars\n        raise RuntimeError(_tw.dedent(f'''\n            Unexpected value: {n_cudd_vars}\n            returned from `Cudd_ReadSize()`\n            (expected <= {CUDD_CONST_INDEX} =\n             CUDD_CONST_INDEX)\n            '''))\n\n    def reorder(\n            self,\n            var_order:\n                _VariableLevels |\n                None=None\n            ) -> None:\n        \"\"\"Reorder variables to `var_order`.\n\n        If `var_order` is `None`,\n        then invoke sifting.\n        \"\"\"\n        reorder(self, var_order)\n\n    cpdef set support(\n            self,\n            u:\n                Function):\n        \"\"\"Return variables that `u` depends on.\n\n        @return:\n            set of variable names\n        @rtype:\n            `set[str]`\n        \"\"\"\n        if self.manager != u.manager:\n            raise ValueError(\n                '`u.manager != self.manager`')\n        r: DdRef\n        r = Cudd_Support(self.manager, u.node)\n        cube = wrap(self, r)\n        support = self._cube_to_dict(cube)\n        # constant ?\n        if not support:\n            return set()\n        # must be positive unate\n        for value in support.values():\n            if value is True:\n                continue\n            raise AssertionError(support)\n        return set(support)\n\n    def group(\n            self,\n            vrs:\n                _abc.Mapping[\n                    _VariableName,\n                    _Nat]\n            ) -> None:\n        r\"\"\"Couple adjacent variables.\n\n        The variables in `vrs` must be at\n        levels that form a contiguous\n        range.\n\n        ```tla\n        ASSUME\n            \\A value \\in vrs.values():\n                value >= 2\n        ```\n        \"\"\"\n        cdef unsigned int group_low\n        cdef unsigned int group_size\n        for var, group_size in vrs.items():\n            if group_size <= 1:\n                raise ValueError(\n                    'singleton as group '\n                    'has no effect')\n            group_low = self._index_of_var[var]\n            Cudd_MakeTreeNode(\n                self.manager, group_low,\n                group_size, MTR_DEFAULT)\n\n    def copy(\n            self,\n            u:\n                Function,\n            other:\n                'BDD' |\n                autoref.BDD\n            ) -> (\n                Function |\n                autoref.Function):\n        \"\"\"Transfer BDD with root `u` to `other`.\"\"\"\n        if isinstance(other, BDD):\n            return copy_bdd(u, other)\n        else:\n            return _copy.copy_bdd(u, other)\n\n    cpdef Function let(\n            self,\n            definitions:\n                _Renaming |\n                _Assignment |\n                dict[_VariableName, Function],\n            u:\n                Function):\n        r\"\"\"Substitute variables.\n\n        Variables can be substituted with:\n        - other variables (by name)\n        - Boolean constant values\n          (given as Python `bool` values)\n        - binary decision diagrams\n          (given as `Function` instances)\n\n        Variables that are to be substituted\n        are identified by their names,\n        as keys of the argument `definitions`,\n        which is a `dict`.\n\n        Multiple variables can be substituted\n        at once. This means that variables\n        can be swapped too.\n\n        The name of this function originates\n        from TLA+ and languages with\n        \"let\" expressions. A \"let\" expression\n        in TLA+ takes the following form:\n\n        ```tla\n        LET x == TRUE\n        IN x /\\ y\n        ```\n\n        In a context where `y` can take\n        only the values `FALSE` and `TRUE`,\n        the above `LET` expression\n        is equivalent to the expression `y`.\n\n        In comparison, the expression:\n\n        ```tla\n        LET x == FALSE\n        IN x /\\ y\n        ```\n\n        is equivalent to `FALSE`.\n        \"\"\"\n        if not definitions:\n            logger.warning(\n                'Call to `BDD.let` with no effect: '\n                '`defs` is empty.')\n            return u\n        var = next(iter(definitions))\n        value = definitions[var]\n        if isinstance(value, python_bool):\n            return self._cofactor(u, definitions)\n        elif isinstance(value, Function):\n            return self._compose(u, definitions)\n        try:\n            value + 's'\n        except TypeError:\n            raise ValueError(\n                'Value must be variable '\n                'name as `str`, '\n                'or Boolean value as `bool`, '\n                'or BDD node as `int`. '\n                f'Got: {value}')\n        return self._rename(u, definitions)\n\n    cpdef Function _compose(\n            self,\n            f:\n                Function,\n            var_sub:\n                dict):\n        \"\"\"Return the composition f|_(var = g).\n\n        @param var_sub:\n            maps variable names to nodes.\n        \"\"\"\n        n = len(var_sub)\n        if n == 0:\n            logger.warning(\n                'call without any effect')\n            return f\n        if n > 1:\n            return self._multi_compose(f, var_sub)\n        if n != 1:\n            raise ValueError(n)\n        var, g = next(iter(var_sub.items()))\n        return self._unary_compose(f, var, g)\n\n    cdef Function _unary_compose(\n            self,\n            f:\n                Function,\n            var:\n                _VariableName,\n            g:\n                Function):\n        \"\"\"Return single composition.\"\"\"\n        if f.manager != self.manager:\n            raise ValueError(\n                '`f.manager != self.manager`')\n        if g.manager != self.manager:\n            raise ValueError(\n                '`g.manager != self.manager`')\n        r: DdRef\n        index = self._index_of_var[var]\n        r = Cudd_bddCompose(\n            self.manager, f.node, g.node, index)\n        if r is NULL:\n            raise RuntimeError('compose failed')\n        return wrap(self, r)\n\n    cdef Function _multi_compose(\n            self,\n            f:\n                Function,\n            var_sub:\n                dict[\n                    _VariableName,\n                    Function]):\n        \"\"\"Return vector composition.\"\"\"\n        if f.manager != self.manager:\n            raise ValueError(\n                '`f.manager != self.manager`')\n        r: DdRef\n        cdef DdRef *x\n        g: Function\n        n_cudd_vars = self._number_of_cudd_vars()\n        if n_cudd_vars <= 0:\n            raise AssertionError(n_cudd_vars)\n        x = <DdRef *> PyMem_Malloc(\n            n_cudd_vars *sizeof(DdRef))\n        for var in self.vars:\n            j = self._index_of_var[var]\n            if var in var_sub:\n                # substitute\n                g = var_sub[var]\n                if g.manager != self.manager:\n                    raise ValueError((var, g))\n                x[j] = g.node\n            else:\n                # leave var same\n                x[j] = Cudd_bddIthVar(\n                    self.manager, j)\n        try:\n            r = Cudd_bddVectorCompose(\n                self.manager, f.node, x)\n        finally:\n            PyMem_Free(x)\n        return wrap(self, r)\n\n    cpdef Function _cofactor(\n            self,\n            f:\n                Function,\n            values:\n                _Assignment):\n        \"\"\"Substitute Booleans for variables.\n\n        @param values:\n            maps variable names to Boolean constants\n        @return:\n            result of substitution\n        @rtype:\n            `Function`\n        \"\"\"\n        if self.manager != f.manager:\n            raise ValueError(f)\n        r: DdRef\n        cube: Function\n        cube = self.cube(values)\n        r = Cudd_Cofactor(\n            self.manager, f.node, cube.node)\n        if r is NULL:\n            raise RuntimeError(\n                'cofactor failed')\n        return wrap(self, r)\n\n    cpdef Function _rename(\n            self,\n            u:\n                Function,\n            dvars:\n                dict[\n                    _VariableName,\n                    _VariableName]):\n        \"\"\"Return node `u` after renaming variables.\n\n        How to rename the variable is defined\n        in the argument `dvars`,\n        which is a `dict`.\n\n        The argument value `dvars = dict(x='y')`\n        results in variable `'x'` substituted by\n        variable `'y'`.\n\n        The argument value\n        `dvars = dict(x='y', y='x')` results in\n        simultaneous substitution of variable\n        `'x'` by variable `'y'`\n        and of variable `'y'` by variable `'x'`.\n        \"\"\"\n        rename = {\n            k: self.var(v)\n            for k, v in dvars.items()}\n        return self._compose(u, rename)\n\n    cpdef Function _swap(\n            self,\n            u:\n                Function,\n            dvars:\n                dict[\n                    _VariableName,\n                    _VariableName]):\n        \"\"\"Return result from swapping variable pairs.\n\n        The variable pairs are defined in\n        the argument `dvars`, which is a `dict`.\n\n        Asserts that each variable occurs in\n        at most one key-value pair\n        of the dictionary `dvars`.\n\n        The argument value `dvars = dict(x='y')`\n        results in swapping\n        of variables `'x'` and `'y'`,\n        which is equivalent to\n        simultaneous substitution of\n        `'x'` by `'y'` and `'y'` by `'x'`.\n\n        So the argument value\n        `dvars = dict(x='y')` has the same\n        result as calling `_rename`\n        with `dvars = dict(x='y', y='x')`.\n        \"\"\"\n        # assert that each variable\n        # occurs in at most one\n        # key-value pair of the\n        # dictionary `dvars`:\n        # 1) assert keys and values of\n        # `dvars` are disjoint sets\n        common = {\n            var for var in dvars.values()\n            if var in dvars}\n        if common:\n            raise ValueError(common)\n        # 2) assert each value is unique\n        values = set(dvars.values())\n        if len(dvars) != len(values):\n            raise ValueError(dvars)\n        #\n        # call swapping\n        n = len(dvars)\n        cdef DdRef *x = <DdRef *> PyMem_Malloc(\n            n * sizeof(DdRef))\n        cdef DdRef *y = <DdRef *> PyMem_Malloc(\n            n * sizeof(DdRef))\n        r: DdRef\n        cdef DdManager *mgr = u.manager\n        f: Function\n        for i, xvar in enumerate(dvars):\n            yvar = dvars[xvar]\n            f = self.var(xvar)\n            x[i] = f.node\n            f = self.var(yvar)\n            y[i] = f.node\n        try:\n            r = Cudd_bddSwapVariables(\n                mgr, u.node, x, y, n)\n            if r is NULL:\n                raise RuntimeError(\n                    'variable swap failed')\n        finally:\n            PyMem_Free(x)\n            PyMem_Free(y)\n        return wrap(self, r)\n\n    cpdef Function ite(\n            self,\n            g:\n                Function,\n            u:\n                Function,\n            v:\n                Function):\n        \"\"\"Ternary conditional.\n\n        In other words, the root of\n        the BDD that represents\n        the expression:\n\n        ```tla\n        IF g THEN u ELSE v\n        ```\n        \"\"\"\n        if g.manager != self.manager:\n            raise ValueError(\n                '`g.manager != self.manager`')\n        if u.manager != self.manager:\n            raise ValueError(\n                '`u.manager != self.manager`')\n        if v.manager != self.manager:\n            raise ValueError(\n                '`v.manager != self.manager`')\n        r: DdRef\n        r = Cudd_bddIte(\n            self.manager,\n            g.node, u.node, v.node)\n        return wrap(self, r)\n\n    cpdef Function find_or_add(\n            self,\n            var:\n                _VariableName,\n            low:\n                Function,\n            high:\n                Function):\n        \"\"\"Return node `IF var THEN high ELSE low`.\"\"\"\n        if low.manager != self.manager:\n            raise ValueError(\n                '`low.manager != self.manager`')\n        if high.manager != self.manager:\n            raise ValueError(\n                '`high.manager != self.manager`')\n        if var not in self.vars:\n            raise ValueError(\n                f'undeclared variable: {var}, '\n                'the declared variables '\n                f'are: {self.vars}')\n        level = self.level_of_var(var)\n        if level >= low.level:\n            raise ValueError(\n                level, low.level, 'low.level')\n        if level >= high.level:\n            raise ValueError(\n                level, high.level, 'high.level')\n        r: DdRef\n        index = self._index_of_var[var]\n        r = cuddUniqueInter(\n            self.manager, index,\n            high.node, low.node)\n        return wrap(self, r)\n\n    def count(\n            self,\n            u:\n                Function,\n            nvars:\n                _Cardinality |\n                None=None\n            ) -> _Cardinality:\n        \"\"\"Return number of models of node `u`.\n\n        @param nvars:\n            regard `u` as\n            an operator that depends on\n            `nvars`-many variables.\n\n            If omitted, then assume\n            those variables in `support(u)`.\n        \"\"\"\n        if u.manager != self.manager:\n            raise ValueError(\n                '`u.manager != self.manager`')\n        n = len(self.support(u))\n        if nvars is None:\n            nvars = n\n        if nvars < n:\n            raise ValueError(nvars, n)\n        r = Cudd_CountMinterm(\n            self.manager, u.node, nvars)\n        if r == CUDD_OUT_OF_MEM:\n            raise RuntimeError(\n                'CUDD out of memory')\n        if r == float('inf'):\n            raise RuntimeError(\n                'overflow of integer '\n                'type double')\n        return r\n\n    def pick(\n            self,\n            u:\n                Function,\n            care_vars:\n                _abc.Set[\n                    _VariableName] |\n                None=None\n            ) -> _Assignment:\n        \"\"\"Return a single assignment.\n\n        @return:\n            assignment of values to\n            variables\n        \"\"\"\n        return next(\n            self.pick_iter(u, care_vars),\n            None)\n\n    def _pick_iter(\n            self,\n            u:\n                Function,\n            care_vars:\n                _abc.Set[\n                    _VariableName] |\n                None=None\n            ) -> _abc.Iterable[\n                _Assignment]:\n        \"\"\"Return iterator over assignments.\n\n        The returned iterator is generator-based.\n        \"\"\"\n        if u.manager != self.manager:\n            raise ValueError(\n                '`u.manager != self.manager`')\n        cdef DdGen *gen\n        cdef int *cube\n        cdef double value\n        support = self.support(u)\n        if care_vars is None:\n            care_vars = support\n        missing = {\n            v for v in support\n            if v not in care_vars}\n        if missing:\n            logger.warning(\n                'Missing bits:  '\n                f'support - care_vars = {missing}')\n        config = self.configure(\n            reordering=False)\n        gen = Cudd_FirstCube(\n            self.manager, u.node,\n            &cube, &value)\n        if gen is NULL:\n            raise RuntimeError(\n                'first cube failed')\n        try:\n            r = 1\n            while Cudd_IsGenEmpty(gen) == 0:\n                if r != 1:\n                    raise RuntimeError(\n                        'gen not empty but '\n                        'no next cube', r)\n                d = _cube_array_to_dict(\n                    cube, self._index_of_var)\n                if not set(d).issubset(support):\n                    raise AssertionError(\n                        set(d).difference(support))\n                for m in _bdd._enumerate_minterms(\n                        d, care_vars):\n                    yield m\n                r = Cudd_NextCube(\n                    gen, &cube, &value)\n        finally:\n            Cudd_GenFree(gen)\n        self.configure(\n            reordering=config['reordering'])\n\n    def pick_iter(\n            self,\n            u:\n                Function,\n            care_vars:\n                _abc.Set[\n                    _VariableName] |\n                None=None\n            ) -> _abc.Iterable[\n                _Assignment]:\n        \"\"\"Return iterator over assignments.\n\n        The returned iterator is generator-based.\n        \"\"\"\n        if self.manager != u.manager:\n            raise ValueError(\n                '`u.manager != self.manager`')\n        support = self.support(u)\n        if care_vars is None:\n            care_vars = support\n        missing = {\n            v for v in support\n            if v not in care_vars}\n        if missing:\n            logger.warning(\n                'Missing bits:  '\n                f'support - care_vars = {missing}')\n        cube = dict()\n        value = True\n        config = self.configure(\n            reordering=False)\n        for cube in self._sat_iter(\n                u, cube, value, support):\n            for m in _bdd._enumerate_minterms(\n                    cube, care_vars):\n                yield m\n        self.configure(\n            reordering=config['reordering'])\n\n    def _sat_iter(\n            self,\n            u:\n                Function,\n            cube:\n                _Assignment,\n            value:\n                python_bool,\n            support\n            ) -> _abc.Iterable[\n                _Assignment]:\n        \"\"\"Recurse to enumerate models.\"\"\"\n        if u.negated:\n            value = not value\n        # terminal ?\n        if u.var is None:\n            if value:\n                if not set(cube).issubset(support):\n                    raise ValueError(set(\n                        cube).difference(support))\n                yield cube\n            return\n        # non-terminal\n        i, v, w = self.succ(u)\n        var = self.var_at_level(i)\n        d0 = dict(cube)\n        d0[var] = False\n        d1 = dict(cube)\n        d1[var] = True\n        for x in self._sat_iter(\n                v, d0, value, support):\n            yield x\n        for x in self._sat_iter(\n                w, d1, value, support):\n            yield x\n\n    cpdef Function apply(\n            self,\n            op:\n                _dd_abc.OperatorSymbol,\n            u:\n                Function,\n            v:\n                _ty.Optional[Function]\n                =None,\n            w:\n                _ty.Optional[Function]\n                =None):\n        \"\"\"Return the result of applying `op`.\"\"\"\n        _utils.assert_operator_arity(op, v, w, 'bdd')\n        if self.manager != u.manager:\n            raise ValueError(\n                '`u.manager != self.manager`')\n        if v is not None and self.manager != v.manager:\n            raise ValueError(\n                '`v.manager != self.manager`')\n        if w is not None and self.manager != w.manager:\n            raise ValueError(\n                '`w.manager != self.manager`')\n        r: DdRef\n        cdef DdManager *mgr\n        mgr = u.manager\n        # unary\n        r = NULL\n        if op in ('~', 'not', '!'):\n            r = Cudd_Not(u.node)\n        # binary\n        elif op in ('and', '/\\\\', '&', '&&'):\n            r = Cudd_bddAnd(mgr, u.node, v.node)\n        elif op in ('or', r'\\/', '|', '||'):\n            r = Cudd_bddOr(mgr, u.node, v.node)\n        elif op in ('#', 'xor', '^'):\n            r = Cudd_bddXor(mgr, u.node, v.node)\n        elif op in ('=>', '->', 'implies'):\n            r = Cudd_bddIte(\n                mgr, u.node, v.node,\n                Cudd_ReadOne(mgr))\n        elif op in ('<=>', '<->', 'equiv'):\n            r = Cudd_bddXnor(mgr, u.node, v.node)\n        elif op in ('diff', '-'):\n            r = Cudd_bddIte(\n                mgr, u.node, Cudd_Not(v.node),\n                Cudd_ReadLogicZero(mgr))\n        elif op in (r'\\A', 'forall'):\n            r = Cudd_bddUnivAbstract(\n                mgr, v.node, u.node)\n        elif op in (r'\\E', 'exists'):\n            r = Cudd_bddExistAbstract(\n                mgr, v.node, u.node)\n        # ternary\n        elif op == 'ite':\n            r = Cudd_bddIte(\n                mgr, u.node, v.node, w.node)\n        else:\n            raise ValueError(\n                f'unknown operator: \"{op}\"')\n        if r is NULL:\n            config = self.configure()\n            raise RuntimeError((\n                'CUDD appears to have '\n                'run out of memory.\\n'\n                'Current settings for '\n                'upper bounds are:\\n'\n                '    max memory = {max_memory} bytes\\n'\n                '    max cache = {max_cache} entries'\n                ).format(\n                    max_memory=config['max_memory'],\n                    max_cache=config['max_cache_hard']))\n        return wrap(self, r)\n\n    cpdef Function _add_int(\n            self,\n            i:\n                int):\n        \"\"\"Return node from `i`.\n\n        Inverse of `Function.__int__()`.\n        \"\"\"\n        u: DdRef = _int_to_ddref(i)\n        return wrap(self, u)\n\n    cpdef Function cube(\n            self,\n            dvars:\n                _abc.Collection[\n                    _VariableName]):\n        \"\"\"Return node for cube over `dvars`.\"\"\"\n        n_cudd_vars = self._number_of_cudd_vars()\n        # make cube\n        cube: DdRef\n        cdef int *x\n        x = <int *> PyMem_Malloc(\n            n_cudd_vars * sizeof(int))\n        _dict_to_cube_array(\n            dvars, x, self._index_of_var)\n        try:\n            cube = Cudd_CubeArrayToBdd(\n                self.manager, x)\n        finally:\n            PyMem_Free(x)\n        return wrap(self, cube)\n\n    cdef Function _cube_from_bdds(\n            self,\n            dvars:\n                _abc.Iterable[\n                    _VariableName]):\n        \"\"\"Return node for cube over `dvars`.\n\n        Only positive unate cubes implemented for now.\n        \"\"\"\n        n = len(dvars)\n        # make cube\n        cube: DdRef\n        cdef DdRef *x\n        x = <DdRef *> PyMem_Malloc(\n            n * sizeof(DdRef))\n        for i, var in enumerate(dvars):\n            f = self.var(var)\n            x[i] = f.node\n        try:\n            cube = Cudd_bddComputeCube(\n                self.manager, x, NULL, n)\n        finally:\n            PyMem_Free(x)\n        return wrap(self, cube)\n\n    cpdef dict _cube_to_dict(\n            self,\n            f:\n                Function):\n        \"\"\"Collect indices of support variables.\"\"\"\n        if f.manager != self.manager:\n            raise ValueError(\n                '`f.manager != self.manager`')\n        n_cudd_vars = self._number_of_cudd_vars()\n        cdef int *x\n        x = <int *> PyMem_Malloc(\n            n_cudd_vars * sizeof(DdRef))\n        try:\n            Cudd_BddToCubeArray(\n                self.manager, f.node, x)\n            d = _cube_array_to_dict(\n                x, self._index_of_var)\n        finally:\n            PyMem_Free(x)\n        return d\n\n    cpdef Function quantify(\n            self,\n            u:\n                Function,\n            qvars:\n                _abc.Iterable[\n                    _VariableName],\n            forall:\n                _Yes=False):\n        \"\"\"Abstract variables `qvars` from node `u`.\"\"\"\n        if u.manager != self.manager:\n            raise ValueError(\n                '`u.manager != self.manager`')\n        cdef DdManager *mgr = u.manager\n        c = set(qvars)\n        cube = self.cube(c)\n        # quantify\n        if forall:\n            r = Cudd_bddUnivAbstract(\n                mgr, u.node, cube.node)\n        else:\n            r = Cudd_bddExistAbstract(\n                mgr, u.node, cube.node)\n        return wrap(self, r)\n\n    cpdef Function forall(\n            self,\n            variables:\n                _abc.Iterable[\n                    _VariableName],\n            u:\n                Function):\n        \"\"\"Quantify `variables` in `u` universally.\n\n        Wraps method `quantify` to be more readable.\n        \"\"\"\n        return self.quantify(\n            u, variables, forall=True)\n\n    cpdef Function exist(\n            self,\n            variables:\n                _abc.Iterable[\n                    _VariableName],\n            u:\n                Function):\n        \"\"\"Quantify `variables` in `u` existentially.\n\n        Wraps method `quantify` to be more readable.\n        \"\"\"\n        return self.quantify(\n            u, variables, forall=False)\n\n    cpdef assert_consistent(\n            self):\n        \"\"\"Raise `AssertionError` if not consistent.\"\"\"\n        if Cudd_DebugCheck(self.manager) != 0:\n            raise AssertionError(\n                '`Cudd_DebugCheck` errored')\n        n = len(self.vars)\n        m = len(self._var_with_index)\n        k = len(self._index_of_var)\n        if n != m:\n            raise AssertionError(n, m)\n        if m != k:\n            raise AssertionError(m, k)\n\n    def add_expr(\n            self,\n            expr:\n                _Formula\n            ) -> Function:\n        \"\"\"Return node for expression `e`.\"\"\"\n        return _parser.add_expr(expr, self)\n\n    cpdef str to_expr(\n            self,\n            u:\n                Function):\n        \"\"\"Return a Boolean expression for node `u`.\"\"\"\n        if u.manager != self.manager:\n            raise ValueError(\n                '`u.manager != self.manager`')\n        cache = dict()\n        return self._to_expr(u.node, cache)\n\n    cdef str _to_expr(\n            self,\n            u:\n                DdRef,\n            cache:\n                dict[int, str]):\n        if u == Cudd_ReadLogicZero(self.manager):\n            return 'FALSE'\n        if u == Cudd_ReadOne(self.manager):\n            return 'TRUE'\n        u_index = _ddref_to_int(u)\n        if u_index in cache:\n            return cache[u_index]\n        v = Cudd_E(u)\n        w = Cudd_T(u)\n        p = self._to_expr(v, cache)\n        q = self._to_expr(w, cache)\n        r = Cudd_Regular(u)\n        var = self._var_with_index[r.index]\n        # pure var ?\n        if p == 'FALSE' and q == 'TRUE':\n            expr = var\n        else:\n            expr = f'ite({var}, {q}, {p})'\n        # complemented ?\n        if Cudd_IsComplement(u):\n            expr = f'(~ {expr})'\n        cache[u_index] = expr\n        return expr\n\n    def dump(\n            self,\n            filename:\n                str,\n            roots:\n                dict[str, Function] |\n                list[Function],\n            filetype:\n                _BDDFileType |\n                None=None\n            ) -> None:\n        \"\"\"Write BDDs to `filename`.\n\n        The file type is inferred from the\n        extension (case insensitive),\n        unless a `filetype` is explicitly given.\n\n        `filetype` can have the values:\n\n        - `'pdf'` for PDF\n        - `'png'` for PNG\n        - `'svg'` for SVG\n        - `'json'` for JSON\n        - `'dddmp'` for DDDMP (of CUDD)\n\n        If `filetype is None`, then `filename`\n        must have an extension that matches\n        one of the file types listed above.\n\n        Dump nodes reachable from `roots`.\n\n        Dumping a JSON file requires that `roots`\n        be nonempty.\n\n        Dumping a DDDMP file requires that `roots`\n        contain a single node.\n\n        @param roots:\n            For JSON: a mapping from\n            names to nodes.\n        \"\"\"\n        if filetype is None:\n            name = filename.lower()\n            if name.endswith('.pdf'):\n                filetype = 'pdf'\n            elif name.endswith('.png'):\n                filetype = 'png'\n            elif name.endswith('.svg'):\n                filetype = 'svg'\n            elif name.endswith('.dot'):\n                filetype = 'dot'\n            elif name.endswith('.p'):\n                raise ValueError(\n                    'pickling unsupported '\n                    'by this class, use JSON')\n            elif name.endswith('.json'):\n                filetype = 'json'\n            elif name.endswith('.dddmp'):\n                filetype = 'dddmp'\n            else:\n                raise ValueError(\n                    'cannot infer file type '\n                    'from extension of file '\n                    f'name \"{filename}\"')\n        if filetype == 'dddmp':\n            # single root supported for now\n            u, = roots\n            self._dump_dddmp(u, filename)\n            return\n        elif filetype == 'json':\n            if roots is None:\n                raise ValueError(roots)\n            _copy.dump_json(roots, filename)\n            return\n        elif (filetype != 'pickle' and\n                filetype not in _utils.DOT_FILE_TYPES):\n            raise ValueError(filetype)\n        bdd = autoref.BDD()\n        _copy.copy_vars(self, bdd)\n            # preserve levels\n        if roots is None:\n            root_nodes = None\n        else:\n            cache = dict()\n            def mapper(u):\n                return _copy.copy_bdd(\n                    u, bdd, cache)\n            root_nodes = _utils.map_container(\n                mapper, roots)\n        bdd.dump(\n            filename, root_nodes,\n            filetype=filetype)\n\n    cpdef _dump_dddmp(\n            self,\n            u:\n                Function,\n            fname:\n                str):\n        \"\"\"Dump BDD as DDDMP file named `fname`.\"\"\"\n        if u.manager != self.manager:\n            raise ValueError(\n                '`u.manager != self.manager`')\n        n_declared_vars = len(self._var_with_index)\n        n_cudd_vars = self._number_of_cudd_vars()\n        if n_declared_vars != n_cudd_vars:\n            counts = _utils.var_counts(self)\n            contiguous = _utils.contiguous_levels(\n                '_dump_dddmp', self)\n            raise AssertionError(\n                f'{counts}\\n{contiguous}')\n        cdef FILE *f\n        cdef char **names\n        cdef bytes py_bytes\n        names = <char **> PyMem_Malloc(\n            n_cudd_vars * sizeof(char *))\n        str_mem = list()\n        for index, var in self._var_with_index.items():\n            py_bytes = var.encode()\n            str_mem.append(py_bytes)\n                # prevent garbage collection\n            names[index] = py_bytes\n        try:\n            f = fopen(fname.encode(), 'w')\n            i = Dddmp_cuddBddStore(\n                self.manager,\n                NULL,\n                u.node,\n                names,\n                NULL,\n                DDDMP_MODE_TEXT,\n                DDDMP_VARNAMES,\n                NULL,\n                f)\n        finally:\n            fclose(f)\n            PyMem_Free(names)\n        if i != DDDMP_SUCCESS:\n            raise RuntimeError(\n                'failed to write to DDDMP file')\n\n    cpdef load(\n            self,\n            filename:\n                str):\n        \"\"\"Return `Function` loaded from `filename`.\n\n        @param filename:\n            name of file from\n            where the BDD is loaded\n        @return:\n            roots of loaded BDDs\n        @rtype:\n            depends on the contents of the file:\n            | `dict[str, Function]`\n            | `list[Function]`\n        \"\"\"\n        if filename.lower().endswith('.dddmp'):\n            r = self._load_dddmp(filename)\n            return [r]\n        elif filename.lower().endswith('.json'):\n            return _copy.load_json(filename, self)\n        else:\n            raise ValueError(\n                f'Unknown file type \"{filename}\"')\n\n    cpdef Function _load_dddmp(\n            self,\n            filename:\n                str):\n        n_declared_vars = len(self._var_with_index)\n        n_cudd_vars = self._number_of_cudd_vars()\n        if n_declared_vars != n_cudd_vars:\n            counts = _utils.var_counts(self)\n            contiguous = _utils.contiguous_levels(\n                '_load_dddmp', self)\n            raise AssertionError(f'{counts}\\n{contiguous}')\n        r: DdRef\n        cdef FILE *f\n        cdef char **names\n        cdef bytes py_bytes\n        names = <char **> PyMem_Malloc(\n            n_cudd_vars * sizeof(char *))\n        str_mem = list()\n        for index, var in self._var_with_index.items():\n            py_bytes = var.encode()\n            str_mem.append(py_bytes)\n            names[index] = py_bytes\n        try:\n            f = fopen(filename.encode(), 'r')\n            r = Dddmp_cuddBddLoad(\n                self.manager,\n                DDDMP_VAR_MATCHNAMES,\n                names,\n                NULL,\n                NULL,\n                DDDMP_MODE_TEXT,\n                NULL,\n                f)\n        except:\n            raise Exception(\n                'A malformed DDDMP file '\n                'can cause segmentation '\n                'faults to `cudd/dddmp`.')\n        finally:\n            fclose(f)\n            PyMem_Free(names)\n        if r is NULL:\n            raise RuntimeError(\n                'failed to load DDDMP file.')\n        h = wrap(self, r)\n        # `Dddmp_cuddBddArrayLoad` references `r`\n        Cudd_RecursiveDeref(self.manager, r)\n        return h\n\n    @property\n    def false(\n            self\n            ) -> Function:\n        \"\"\"Boolean value false.\"\"\"\n        return self._bool(False)\n\n    @property\n    def true(\n            self\n            ) -> Function:\n        \"\"\"Boolean value true.\"\"\"\n        return self._bool(True)\n\n    cdef Function _bool(\n            self,\n            v:\n                python_bool):\n        \"\"\"Return leaf node for Boolean `v`.\"\"\"\n        r: DdRef\n        if v:\n            r = Cudd_ReadOne(self.manager)\n        else:\n            r = Cudd_ReadLogicZero(self.manager)\n        return wrap(self, r)\n\n\ncpdef Function restrict(\n        u:\n            Function,\n        care_set:\n            Function):\n    \"\"\"Restrict `u` to `care_set`.\n\n    The operator \"restrict\" is defined in\n    1990 Coudert ICCAD.\n    \"\"\"\n    if u.manager != care_set.manager:\n        raise ValueError(\n            '`u.manager != care_set.manager`')\n    r: DdRef\n    r = Cudd_bddRestrict(\n        u.manager, u.node, care_set.node)\n    return wrap(u.bdd, r)\n\n\ncpdef Function and_exists(\n        u:\n            Function,\n        v:\n            Function,\n        qvars:\n            _abc.Iterable[\n                _VariableName]):\n    r\"\"\"Return `\\E qvars:  u /\\ v`.\"\"\"\n    if u.manager != v.manager:\n        raise ValueError(\n            '`u.manager != v.manager`')\n    qvars = set(qvars)\n    cube = u.bdd.cube(qvars)\n    r = Cudd_bddAndAbstract(\n        u.manager, u.node, v.node, cube.node)\n    return wrap(u.bdd, r)\n\n\ncpdef Function or_forall(\n        u:\n            Function,\n        v:\n            Function,\n        qvars:\n            _abc.Iterable[\n                _VariableName]):\n    r\"\"\"Return `\\A qvars:  u \\/ v`.\"\"\"\n    if u.manager != v.manager:\n        raise ValueError(\n            '`u.manager != v.manager`')\n    qvars = set(qvars)\n    cube = u.bdd.cube(qvars)\n    r = Cudd_bddAndAbstract(\n        u.manager,\n        Cudd_Not(u.node),\n        Cudd_Not(v.node),\n        cube.node)\n    r = Cudd_Not(r)\n    return wrap(u.bdd, r)\n\n\ncpdef reorder(\n        bdd:\n            BDD,\n        dvars:\n            _VariableLevels |\n            None=None):\n    \"\"\"Reorder `bdd` to order in `dvars`.\n\n    If `dvars` is `None`, then invoke group sifting.\n    \"\"\"\n    # invoke sifting ?\n    if dvars is None:\n        Cudd_ReduceHeap(\n            bdd.manager,\n            CUDD_REORDER_GROUP_SIFT, 1)\n        return\n    n_declared_vars = len(bdd.vars)\n    n_cudd_vars = bdd._number_of_cudd_vars()\n    if n_declared_vars != n_cudd_vars:\n        counts = _utils.var_counts(bdd)\n        contiguous = _utils.contiguous_levels(\n            'reorder', bdd)\n        raise AssertionError(\n            f'{counts}\\n{contiguous}')\n    # partial reorderings not supported for now\n    if len(dvars) != n_cudd_vars:\n        raise ValueError(\n            'Mismatch of variable numbers:\\n'\n            'the number of declared variables '\n            f'is: {n_cudd_vars}\\n'\n            f'new variable order: {len(dvars)}')\n    cdef int *p\n    p = <int *> PyMem_Malloc(\n        n_cudd_vars * sizeof(int *))\n    level_to_var = {v: k for k, v in dvars.items()}\n    for level in range(n_cudd_vars):\n        var = level_to_var[level]\n        index = bdd._index_of_var[var]\n        p[level] = index\n    try:\n        r = Cudd_ShuffleHeap(bdd.manager, p)\n    finally:\n        PyMem_Free(p)\n    if r != 1:\n        raise RuntimeError(\n            'Failed to reorder. '\n            'Variable groups that are incompatible to '\n            'the given order can cause this.')\n\n\ndef copy_vars(\n        source:\n            BDD,\n        target:\n            BDD\n        ) -> None:\n    \"\"\"Copy variables, preserving CUDD indices.\"\"\"\n    for var, index in source._index_of_var.items():\n        target.add_var(var, index=index)\n\n\ncpdef Function copy_bdd(\n        u:\n            Function,\n        target:\n            BDD):\n    \"\"\"Copy BDD of node `u` to manager `target`.\n\n    Turns off reordering in `source`\n    when checking for missing vars in `target`.\n\n    ```tla\n    ASSUME\n        u in source\n    ```\n    \"\"\"\n    logger.debug('++ transfer bdd')\n    source = u.bdd\n    if u.manager == target.manager:\n        logger.warning(\n            'copying node to same manager')\n        return u\n    # target missing vars ?\n    cfg = source.configure(reordering=False)\n    supp = source.support(u)\n    source.configure(reordering=cfg['reordering'])\n    missing = {\n        var for var in supp\n        if var not in target.vars}\n    if missing:\n        raise ValueError(\n            '`target` BDD is missing the variables:\\n'\n            f'{missing}\\n'\n            'the declared variables in `target` are:\\n'\n            f'{target.vars}\\n')\n    # mapping of indices\n    n_cudd_vars = source._number_of_cudd_vars()\n    cdef int *renaming\n    renaming = <int *> PyMem_Malloc(\n        n_cudd_vars * sizeof(int))\n    # only support will show up during BDD traversal\n    for var in supp:\n        i = source._index_of_var[var]\n        j = target._index_of_var[var]\n        renaming[i] = j\n    try:\n        r = Cudd_bddTransferRename(\n            source.manager,\n            target.manager, u.node, renaming)\n    finally:\n        PyMem_Free(renaming)\n    logger.debug(\n        '-- done transferring bdd')\n    return wrap(target, r)\n\n\ncpdef int count_nodes(\n        functions:\n            list[Function]):\n    \"\"\"Return total nodes used by `functions`.\n\n    Sharing is taken into account.\n    \"\"\"\n    cdef DdRef *x\n    f: Function\n    n = len(functions)\n    x = <DdRef *> PyMem_Malloc(\n        n * sizeof(DdRef))\n    for i, f in enumerate(functions):\n        x[i] = f.node\n    try:\n        k = Cudd_SharingSize(x, n)\n    finally:\n        PyMem_Free(x)\n    return k\n\n\ncpdef dict count_nodes_per_level(\n        bdd:\n            BDD):\n    \"\"\"Return mapping of each var to a node count.\"\"\"\n    d = dict()\n    for var in bdd.vars:\n        level = bdd.level_of_var(var)\n        n = bdd.manager.subtables[level].keys\n        d[var] = n\n    return d\n\n\ndef dump(\n        u:\n            Function,\n        file_name:\n            str\n        ) -> None:\n    \"\"\"Pickle variable order and dump dddmp file.\"\"\"\n    bdd = u.bdd\n    pickle_fname = f'{file_name}.pickle'\n    dddmp_fname = f'{file_name}.dddmp'\n    order = {\n        var: bdd.level_of_var(var)\n        for var in bdd.vars}\n    d = dict(variable_order=order)\n    with open(pickle_fname, 'wb') as f:\n        pickle.dump(d, f, protocol=2)\n    bdd.dump(u, dddmp_fname)\n\n\ndef load(\n        file_name:\n            str,\n        bdd:\n            BDD,\n        reordering:\n            _Yes=False\n        ) -> Function:\n    \"\"\"Unpickle variable order and load dddmp file.\n\n    Loads the variable order,\n    reorders `bdd` to match that order,\n    turns off reordering,\n    then loads the BDD,\n    restores reordering.\n    Assumes that:\n\n      - `file_name` has no extension\n      - pickle file name: `file_name.pickle`\n      - dddmp file name: `file_name.dddmp`\n\n    @param reordering:\n        if `True`,\n        then enable reordering during DDDMP load.\n    \"\"\"\n    t0 = time.time()\n    pickle_fname = f'{file_name}.pickle'\n    dddmp_fname = f'{file_name}.dddmp'\n    with open(pickle_fname, 'rb') as f:\n        d = pickle.load(f)\n    order = d['variable_order']\n    for var in order:\n        bdd.add_var(var)\n    reorder(bdd, order)\n    cfg = bdd.configure(reordering=False)\n    u = bdd.load(dddmp_fname)\n    bdd.configure(reordering=cfg['reordering'])\n    t1 = time.time()\n    dt = t1 - t0\n    logger.info(\n        f'BDD load time from file: {dt}')\n    return u\n\n\ncdef _dict_to_cube_array(\n        d:\n            _Assignment,\n        int *x,\n        index_of_var:\n            _Assignment |\n            set[_VariableName]):\n    \"\"\"Assign array of literals `x` from assignment `d`.\n\n    @param x:\n        array of literals\n        0: negated, 1: positive, 2: don't care\n        read `Cudd_FirstCube`\n    @param index_of_var:\n        `dict` from variables to `bool`\n        or `set` of variable names.\n    \"\"\"\n    for var in d:\n        if var not in index_of_var:\n            raise ValueError(var)\n    for var, j in index_of_var.items():\n        if var not in d:\n            x[j] = 2\n            continue\n        # var in `d`\n        if isinstance(d, dict):\n            b = d[var]\n        else:\n            b = True\n        if b is False:\n            x[j] = 0\n        elif b is True:\n            x[j] = 1\n        else:\n            raise ValueError(\n                f'unknown value: {b}')\n\n\ncdef dict _cube_array_to_dict(\n        int *x,\n        index_of_var:\n            dict):\n    \"\"\"Return assignment from array of literals `x`.\n\n    @param x:\n        read `_dict_to_cube_array`\n    \"\"\"\n    d = dict()\n    for var, j in index_of_var.items():\n        b = x[j]\n        if b == 2:\n            continue\n        elif b == 1:\n            d[var] = True\n        elif b == 0:\n            d[var] = False\n        else:\n            raise Exception(\n                f'unknown polarity: {b}, '\n                f'for variable \"{var}\"')\n    return d\n\n\ncdef Function wrap(\n        bdd:\n            BDD,\n        node:\n            DdRef):\n    \"\"\"Return a `Function` that wraps `node`.\"\"\"\n    # because `@classmethod` unsupported\n    f = Function()\n    f.init(node, bdd)\n    return f\n\n\ncdef class Function:\n    r\"\"\"Wrapper of `DdNode` from CUDD.\n\n    Attributes (those that are properties are\n    described in their docstrings):\n\n      - `_index`\n      - `_ref`: safe lower bound on reference count\n        of the CUDD BDD node pointed to by this\n        `Function` instance. Do not modify this value.\n      - `var`\n      - `level`\n      - `ref`\n      - `low`\n      - `high`\n      - `negated`\n      - `support`\n      - `dag_size`\n\n    In Python, use as:\n\n    ```python\n    from dd.cudd import BDD\n\n    bdd = BDD()\n    u = bdd.true\n    v = bdd.false\n    w = u | ~ v\n    ```\n\n    In Cython, use as:\n\n    ```cython\n    bdd = BDD()\n    cdef DdNode *u\n    u = Cudd_ReadOne(bdd.manager)\n    f = Function()\n    f.init(bdd, u)\n    ```\n\n\n    About reference counting\n    ========================\n\n    Nothing needs to be done for reference counting\n    by the user: reference counting is automated.\n\n    \"Early\" dereferencing of a CUDD BDD node is\n    possible by using the statement:\n\n    ```python\n    del u\n    ```\n\n    where `u` is an instance of the class `Function`.\n    \"Early\" here means that the CUDD BDD node will\n    be dereferenced before it would have otherwise\n    been dereferenced.\n\n    That (possibly) later time would have been when\n    Python exited the scope where `u` was defined,\n    or even later, in case other references to the\n    object with `id(u)` existed.\n\n    The method `dd.cudd.BDD.decref` should not be\n    called for \"early\" dereferencing. Instead,\n    write `del u` as above.\n\n    However, if the user decides to call any\n    of the methods:\n\n    - `dd.cudd.BDD.incref(u)`\n    - `dd.cudd.BDD.decref(u)`\n\n    then the user needs to ensure that `u._ref > 0`\n    before each call to these methods,\n    taking into account that:\n\n    - `dd.cudd.BDD.incref(u)` increments `u._ref`\n    - `dd.cudd.BDD.decref(u)` decrements `u._ref` and\n      sets `u.node` to `NULL` when `u._ref` becomes `0`.\n\n    The attribute `u._ref` is *not* the\n    reference count of the BDD node in CUDD\n    that the C attribute `u.node`\n    points to. The value of `u._ref` is\n    a lower bound on the reference count of\n    the BDD node that `u.node` points to.\n\n    This is a safe approach for accessing\n    memory in CUDD. The following example\n    demonstrates this approach.\n\n    We start with:\n\n    ```python\n    from dd.cudd import BDD\n\n    bdd = BDD()\n    bdd.declare('x', 'y')\n    u = bdd.add_expr(r'x /\\ ~ y')\n\n    w = u\n    assert w is u\n    ```\n\n    i.e., `u` and `w` are different Python variables\n    that point to the *same* instance of `Function`.\n    This `Function` instance points to\n    a BDD node in CUDD.\n    We will refer to this `Function` instance as\n    \"the object with `id(u)`\".\n\n    ```python\n    v = bdd.add_expr(r'x /\\ ~ y')\n    assert v is not u\n    ```\n\n    i.e., the Python variable `v` points to an\n    instance of `Function` different from the\n    `Function` instance that `u` points to.\n    We will refer to the `Function` instance that\n    `v` points to as \"the object with `id(v)`\".\n\n    The object with `id(v)` and the object with `id(u)`\n    point to the same BDD node in CUDD.\n\n    The statement\n\n    ```python\n    bdd.decref(v, recursive=True)\n    ```\n\n    decrements:\n\n    - the reference count of the BDD node in CUDD\n      that the object with `id(v)` points to\n\n    - the lower bound `v._ref`\n\n    - the reference counts of CUDD BDD nodes that\n      are successors, recursively, when a node's\n      reference count becomes 0. For more details\n      read the docstring of the CUDD function\n      `Cudd_RecursiveDeref`.\n\n    Setting the parameter `recursive` to `True`\n    here has no effect, because due to `u` the\n    reference count of the CUDD BDD node corresponding\n    to `v` remains positive after the decrement.\n\n    But in general this is not the case,\n    so `recursive=True` is then necessary,\n    because afterwards it is impossible to\n    dereference the successors of the CUDD BDD node\n    that corresponds to `v`. The reason is\n    described next.\n\n    The object with `id(v)` *cannot* be used after\n    this point, because the call to the method `decref`\n    resulted in `v._ref == 0`, so it also set the\n    pointer `v.node` to `NULL`.\n\n    Setting `v.node` to `NULL` guards from further\n    use of the object with `id(v)` to\n    access CUDD BDD nodes.\n    The object with `id(v)` should *not* be used\n    after this point.\n\n    In this specific example,\n    if the method `decref` did not\n    set `v.node` to `NULL`, then using `v` beyond\n    this point would actually not have caused problems,\n    because the CUDD BDD node's reference count\n    is still positive (due to the increment\n    when the object with `id(u)` was instantiated).\n\n    But in general this is not the case.\n\n    Also, after the attribute `v._ref` becomes `0`,\n    there is no safe way for the object with `id(v)`\n    to read the reference count of the CUDD BDD node\n    that this object points to, even though that\n    reference count is positive and the BDD node\n    is still accessible via `u` and `w`.\n\n    From the perspective of the object with `id(v)`,\n    further access to that CUDD BDD node is unsafe.\n\n    Had the method `decref` not set `u.node` to `NULL`,\n    then if we had continued by doing:\n\n    ```python\n    bdd.decref(u, recursive=True)\n    ```\n\n    then both variables `u` and `w`\n    should *not* had been used any further.\n    These variables refer to the\n    same Python object, and `u._ref == 0`\n    (thus `w._ref == 0`). So the same observations\n    apply to `u` and `w` as for `v` above.\n    \"\"\"\n\n    __weakref__: object\n    cdef public BDD bdd\n    cdef DdManager *manager\n    node: DdRef\n    cdef public int _ref\n\n    cdef init(\n            self,\n            node:\n                DdRef,\n            bdd:\n                BDD):\n        if node is NULL:\n            raise ValueError(\n                '`DdNode *node` is `NULL` pointer.')\n        self.bdd = bdd\n        self.manager = bdd.manager\n        self.node = node\n        self._ref = 1  # lower bound on\n            # reference count\n            #\n            # Assumed invariant:\n            # this instance participates in\n            # computation only as long as\n            # `self._ref > 0`.\n            # The user is responsible for\n            # implementing this invariant.\n        Cudd_Ref(node)\n\n    def __hash__(\n            self\n            ) -> int:\n        return int(self)\n\n    @property\n    def _index(\n            self\n            ) -> _Nat:\n        \"\"\"Index of `self.node`.\"\"\"\n        return Cudd_NodeReadIndex(self.node)\n\n    @property\n    def var(\n            self\n            ) -> (\n                _VariableName |\n                None):\n        \"\"\"Variable at level where this node is.\n\n        If node is constant, return `None`.\n        \"\"\"\n        if Cudd_IsConstant(self.node):\n            return None\n        return self.bdd._var_with_index[self._index]\n\n    @property\n    def level(\n            self\n            ) -> _Level:\n        \"\"\"Level where this node currently is.\"\"\"\n        i = self._index\n        return Cudd_ReadPerm(self.manager, i)\n\n    @property\n    def ref(\n            self\n            ) -> _Cardinality:\n        \"\"\"Reference count of node.\n\n        Returns the sum of the reference count\n        of this BDD root, and of the reference\n        count of the root of the negated BDD.\n        \"\"\"\n        u: DdRef\n        u = Cudd_Regular(self.node)\n        return u.ref\n\n    @property\n    def low(\n            self\n            ) -> '''\n                Function |\n                None\n                ''':\n        \"\"\"Return \"else\" node as `Function`.\"\"\"\n        u: DdRef\n        if Cudd_IsConstant(self.node):\n            return None\n        u = Cudd_E(self.node)\n        return wrap(self.bdd, u)\n\n    @property\n    def high(\n            self\n            ) -> '''\n                Function |\n                None\n                ''':\n        \"\"\"Return \"then\" node as `Function`.\"\"\"\n        u: DdRef\n        if Cudd_IsConstant(self.node):\n            return None\n        u = Cudd_T(self.node)\n        return wrap(self.bdd, u)\n\n    @property\n    def negated(\n            self\n            ) -> _Yes:\n        \"\"\"`True` if this is a complemented edge.\n\n        Returns `True` if `self` is\n        a complemented edge.\n        \"\"\"\n        return Cudd_IsComplement(self.node)\n\n    @property\n    def support(\n            self:\n                BDD\n            ) -> set[_VariableName]:\n        \"\"\"Return `set` of variables in support.\"\"\"\n        return self.bdd.support(self)\n\n    def __dealloc__(\n            self\n            ) -> None:\n        # when changing this method,\n        # update also the function\n        # `_test_call_dealloc` below\n        if self._ref < 0:\n            raise AssertionError(\n                \"The lower bound `_ref` \"\n                \"on the node's \"\n                'reference count has '\n                f'value {self._ref}, '\n                'which is unexpected and '\n                'should never happen. '\n                'Was the value of `_ref` '\n                'changed from outside '\n                'this instance?')\n        assert self._ref >= 0, self._ref\n        if self._ref == 0:\n            return\n        if self.node is NULL:\n            raise AssertionError(\n                'The attribute `node` is '\n                'a `NULL` pointer. '\n                'This is unexpected and '\n                'should never happen. '\n                'Was the value of `_ref` '\n                'changed from outside '\n                'this instance?')\n        # anticipate multiple calls to `__dealloc__`\n        self._ref -= 1\n        # deref\n        Cudd_RecursiveDeref(\n            self.manager, self.node)\n        # avoid future access\n        # to deallocated memory\n        self.node = NULL\n\n    def __int__(\n            self\n            ) -> int:\n        \"\"\"Inverse of `BDD._add_int()`.\"\"\"\n        return _ddref_to_int(self.node)\n\n    def __repr__(\n            self\n            ) -> str:\n        u: DdRef\n        u = Cudd_Regular(self.node)\n        return (\n            f'<dd.cudd.Function at {hex(id(self))}, '\n            'wrapping a DdNode with '\n            f'var index: {u.index}, '\n            f'ref count: {u.ref}, '\n            f'int repr: {int(self)}>')\n\n    def __str__(\n            self\n            ) -> str:\n        return f'@{int(self)}'\n\n    def __len__(\n            self\n            ) -> _Cardinality:\n        return Cudd_DagSize(self.node)\n\n    @property\n    def dag_size(\n            self\n            ) -> _Cardinality:\n        \"\"\"Return number of BDD nodes.\n\n        This is the number of BDD nodes that\n        are reachable from this BDD reference,\n        i.e., with `self` as root.\n        \"\"\"\n        return len(self)\n\n    def __eq__(\n            self:\n                Function,\n            other:\n                _ty.Optional[Function]\n            ) -> _Yes:\n        if other is None:\n            return False\n        # guard against mixing managers\n        if self.manager != other.manager:\n            raise ValueError(\n                '`self.manager != other.manager`')\n        return self.node == other.node\n\n    def __ne__(\n            self:\n                Function,\n            other:\n                _ty.Optional[Function]\n            ) -> _Yes:\n        if other is None:\n            return True\n        if self.manager != other.manager:\n            raise ValueError(\n                '`self.manager != other.manager`')\n        return self.node != other.node\n\n    def __le__(\n            self:\n                Function,\n            other:\n                Function\n            ) -> _Yes:\n        if self.manager != other.manager:\n            raise ValueError(\n                '`self.manager != other.manager`')\n        leq = Cudd_bddLeq(\n            self.manager, self.node, other.node)\n        return (leq == 1)\n\n    def __lt__(\n            self:\n                Function,\n            other:\n                Function\n            ) -> _Yes:\n        if self.manager != other.manager:\n            raise ValueError(\n                '`self.manager != other.manager`')\n        leq = Cudd_bddLeq(\n            self.manager, self.node, other.node)\n        return (\n            self.node != other.node and\n            leq == 1)\n\n    def __ge__(\n            self:\n                Function,\n            other:\n                Function\n            ) -> _Yes:\n        if self.manager != other.manager:\n            raise ValueError(\n                '`self.manager != other.manager`')\n        geq = Cudd_bddLeq(\n            self.manager, other.node, self.node)\n        return (geq == 1)\n\n    def __gt__(\n            self:\n                Function,\n            other:\n                Function\n            ) -> _Yes:\n        if self.manager != other.manager:\n            raise ValueError(\n                '`self.manager != other.manager`')\n        geq = Cudd_bddLeq(\n            self.manager, other.node, self.node)\n        return (\n            self.node != other.node and\n            geq == 1)\n\n    def __invert__(\n            self\n            ) -> Function:\n        r: DdRef\n        r = Cudd_Not(self.node)\n        return wrap(self.bdd, r)\n\n    def __and__(\n            self:\n                Function,\n            other:\n                Function\n            ) -> Function:\n        if self.manager != other.manager:\n            raise ValueError(\n                '`self.manager != other.manager`')\n        r = Cudd_bddAnd(\n            self.manager, self.node, other.node)\n        return wrap(self.bdd, r)\n\n    def __or__(\n            self:\n                Function,\n            other:\n                Function\n            ) -> Function:\n        if self.manager != other.manager:\n            raise ValueError(\n                '`self.manager != other.manager`')\n        r = Cudd_bddOr(\n            self.manager, self.node, other.node)\n        return wrap(self.bdd, r)\n\n    def __xor__(\n            self:\n                Function,\n            other:\n                Function\n            ) -> Function:\n        if self.manager != other.manager:\n            raise ValueError(\n                '`self.manager != other.manager`')\n        r = Cudd_bddXor(\n            self.manager, self.node, other.node)\n        return wrap(self.bdd, r)\n\n    def implies(\n            self:\n                Function,\n            other:\n                Function\n            ) -> Function:\n        if self.manager != other.manager:\n            raise ValueError(\n                '`self.manager != other.manager`')\n        r = Cudd_bddIte(\n            self.manager, self.node,\n            other.node, Cudd_ReadOne(self.manager))\n        return wrap(self.bdd, r)\n\n    def equiv(\n            self:\n                Function,\n            other:\n                Function\n            ) -> Function:\n        if self.manager != other.manager:\n            raise ValueError(\n                '`self.manager != other.manager`')\n        r = Cudd_bddIte(\n            self.manager, self.node,\n            other.node, Cudd_Not(other.node))\n        return wrap(self.bdd, r)\n\n    def let(\n            self:\n                Function,\n            **definitions:\n                _VariableName |\n                python_bool |\n                Function\n            ) -> Function:\n        return self.bdd.let(definitions, self)\n\n    def exist(\n            self:\n                Function,\n            *variables:\n                _VariableName\n            ) -> Function:\n        return self.bdd.exist(variables, self)\n\n    def forall(\n            self:\n                Function,\n            *variables:\n                _VariableName\n            ) -> Function:\n        return self.bdd.forall(variables, self)\n\n    def pick(\n            self:\n                Function,\n            care_vars:\n                _abc.Set[\n                    _VariableName] |\n                None=None\n            ) -> _Assignment:\n        return self.bdd.pick(self, care_vars)\n\n    def count(\n            self:\n                Function,\n            nvars:\n                _Cardinality |\n                None=None\n            ) -> _Cardinality:\n        return self.bdd.count(self, nvars)\n\n\ncdef _ddref_to_int(\n        node:\n            DdRef):\n    \"\"\"Convert node pointer to numeric index.\n\n    Inverse of `_int_to_ddref()`.\n    \"\"\"\n    if sizeof(stdint.uintptr_t) != sizeof(DdRef):\n        raise AssertionError(\n            'mismatch of sizes')\n    index = <stdint.uintptr_t>node\n    # 0, 1 used to represent TRUE and FALSE\n    # in syntax of expressions\n    if 0 <= index:\n        index += 2\n    if index in (0, 1):\n        raise AssertionError(index)\n    return index\n\n\ncdef DdRef _int_to_ddref(\n        index:\n            int):\n    \"\"\"Convert numeric index to node pointer.\n\n    Inverse of `_ddref_to_int()`.\n    \"\"\"\n    if index in (0, 1):\n        raise ValueError(index)\n    if 2 <= index:\n        index -= 2\n    u: DdRef = <DdRef><stdint.uintptr_t>index\n    return u\n\n\n\"\"\"Tests and test wrappers for C functions.\"\"\"\n\n\ncpdef _test_incref():\n    bdd = BDD()\n    f: Function\n    f = bdd.true\n    i = f.ref\n    bdd._incref(f.node)\n    j = f.ref\n    if j != i + 1:\n        raise AssertionError((j, i))\n    # avoid errors in `BDD.__dealloc__`\n    bdd._decref(f.node, recursive=True)\n    del f\n\n\ncpdef _test_decref():\n    bdd = BDD()\n    f: Function\n    f = bdd.true\n    i = f.ref\n    if i != 2:\n        raise AssertionError(i)\n    bdd._incref(f.node)\n    i = f.ref\n    if i != 3:\n        raise AssertionError(i)\n    bdd._decref(f.node, recursive=True)\n    j = f.ref\n    if j != i - 1:\n        raise AssertionError((j, i))\n    del f\n\n\ncpdef _test_dict_to_cube_array():\n    cdef int *x\n    n = 3\n    x = <int *> PyMem_Malloc(\n        n * sizeof(int))\n    index_of_var = dict(x=0, y=1, z=2)\n    d = dict(y=True, z=False)\n    _dict_to_cube_array(\n        d, x, index_of_var)\n    r = [j for j in x[:n]]\n    r_ = [2, 1, 0]\n    if r != r_:\n        raise AssertionError((r, r_))\n    PyMem_Free(x)\n\n\ncpdef _test_cube_array_to_dict():\n    cdef int *x\n    n = 3\n    x = <int *> PyMem_Malloc(\n        n * sizeof(int))\n    x[0] = 2\n    x[1] = 1\n    x[2] = 0\n    index_of_var = dict(x=0, y=1, z=2)\n    d = _cube_array_to_dict(\n        x, index_of_var)\n    d_ = dict(y=True, z=False)\n    if d != d_:\n        raise AssertionError((d, d_))\n    PyMem_Free(x)\n\n\ncpdef _test_call_dealloc(\n        u:\n            Function):\n    \"\"\"Duplicates the code of `Function.__dealloc__`.\n\n    The main purpose of this function is to test the\n    exceptions raised in the method `Function.__dealloc__`.\n\n    Exceptions raised in `__dealloc__` are ignored\n    (they become messages), and it seems impossible to\n    call `__dealloc__` directly (unlike `__del__`),\n    so there is no way to assert what exceptions\n    are raised in `__dealloc__`.\n\n    This function is the closest thing to testing\n    those exceptions.\n    \"\"\"\n    self = u\n    # the code of `Function.__dealloc__` follows:\n    if self._ref < 0:\n        raise AssertionError(\n            \"The lower bound `_ref` on the node's \"\n            'reference count has value {self._ref}, '\n            'which is unexpected and should never happen. '\n            'Was the value of `_ref` changed from outside '\n            'this instance?')\n    assert self._ref >= 0, self._ref\n    if self._ref == 0:\n        return\n    if self.node is NULL:\n        raise AssertionError(\n            'The attribute `node` is a `NULL` pointer. '\n            'This is unexpected and should never happen. '\n            'Was the value of `_ref` changed from outside '\n            'this instance?')\n    # anticipate multiple calls to `__dealloc__`\n    self._ref -= 1\n    # deref\n    Cudd_RecursiveDeref(self.manager, self.node)\n    # avoid future access to deallocated memory\n    self.node = NULL\n"
  },
  {
    "path": "dd/cudd_zdd.pyx",
    "content": "\"\"\"Cython interface to ZDD implementation in CUDD.\n\nZDDs are represented without complemented edges in CUDD (unlike BDDs).\nSo rectifying a node to \"regular\" is unnecessary.\nVariable `__version__` equals CUDD's version string.\n\n\nReference\n=========\n    Fabio Somenzi\n    \"CUDD: CU Decision Diagram Package\"\n    University of Colorado at Boulder\n    v2.5.1, 2015\n    <http://vlsi.colorado.edu/~fabio/>\n\"\"\"\n# Copyright 2015-2020 by California Institute of Technology\n# All rights reserved. Licensed under 3-clause BSD.\n#\n#\n# Copyright (c) 1995-2015, Regents of the University of Colorado\n#\n# All rights reserved.\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions\n# are met:\n#\n# Redistributions of source code must retain the above copyright\n# notice, this list of conditions and the following disclaimer.\n#\n# Redistributions in binary form must reproduce the above copyright\n# notice, this list of conditions and the following disclaimer in the\n# documentation and/or other materials provided with the distribution.\n#\n# Neither the name of the University of Colorado nor the names of its\n# contributors may be used to endorse or promote products derived from\n# this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS\n# \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT\n# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS\n# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE\n# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,\n# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,\n# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\n# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\n# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT\n# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN\n# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\n# POSSIBILITY OF SUCH DAMAGE.\n#\nimport collections.abc as _abc\nimport itertools as _itr\nimport logging\nimport textwrap as _tw\nimport typing as _ty\nimport warnings\n\nfrom cpython cimport bool as python_bool\nfrom cpython.mem cimport PyMem_Malloc, PyMem_Free\nimport cython\ncimport libc.stdint as stdint\nfrom libc.stdio cimport FILE, fdopen, fopen, fclose\nfrom libcpp cimport bool\n\nimport dd._abc as _dd_abc\nfrom dd import _copy\nfrom dd import _parser\nfrom dd import _utils\nfrom dd import bdd as _bdd\n\n\nctypedef cython.int _c_level\nctypedef cython.int _c_int\n_Yes: _ty.TypeAlias = python_bool\n_Nat: _ty.TypeAlias = _dd_abc.Nat\n_Cardinality: _ty.TypeAlias = _dd_abc.Cardinality\n_NumberOfBytes: _ty.TypeAlias = _dd_abc.NumberOfBytes\n_VariableName: _ty.TypeAlias = _dd_abc.VariableName\n_Level: _ty.TypeAlias = _dd_abc.Level\n_VariableLevels: _ty.TypeAlias = _dd_abc.VariableLevels\n_Assignment: _ty.TypeAlias = _dd_abc.Assignment\n_Renaming: _ty.TypeAlias = _dd_abc.Renaming\n_Formula: _ty.TypeAlias = _dd_abc.Formula\n\n\ncdef extern from 'cuddInt.h':\n    char* CUDD_VERSION\n    int CUDD_CONST_INDEX\n    # subtable (for a level)\n    struct DdSubtable:\n        unsigned int slots\n        unsigned int keys\n    # manager\n    struct DdManager:\n        DdSubtable *subtables\n        unsigned int keys\n        unsigned int dead\n        double cachecollisions\n        double cacheinserts\n        double cachedeletions\n        DdNode **univ\n        int reordered\n    # local hash tables\n    ctypedef stdint.intptr_t ptrint\n    struct DdHashItem:\n        DdHashItem *next\n        DdNode *value\n    struct DdHashTable:\n        DdHashItem **bucket\n        DdHashItem **memoryList\n        unsigned int numBuckets\n        DdManager *manager\n    DdHashTable * cuddHashTableInit(\n        DdManager *manager,\n        unsigned int keySize,\n        unsigned int initSize)\n    void cuddHashTableQuit(\n        DdHashTable *hash)\n    int cuddHashTableInsert1(\n        DdHashTable *hash, DdNode *f,\n        DdNode *value, ptrint count)\n    DdNode * cuddHashTableLookup1(\n        DdHashTable *hash, DdNode *f)\n    # cache\n    DdNode * cuddCacheLookup2Zdd(\n        DdManager *table,\n        DdNode * (*)(\n            DdManager *, DdNode *, DdNode *),\n        DdNode *f,\n        DdNode *g)\n    void cuddCacheInsert2(\n        DdManager *table,\n        DdNode * (*)(\n            DdManager *, DdNode *, DdNode *),\n        DdNode *f,\n        DdNode *g,\n        DdNode *data)\n    # node elements\n    DdNode *cuddUniqueInter(\n        DdManager *unique, int index,\n        DdNode *T, DdNode *E)\n    DdNode * cuddUniqueInterZdd(\n        DdManager *unique, int index,\n        DdNode *T, DdNode *E)\n    bool cuddIsConstant(\n        DdNode *u)\n    DdNode *DD_ZERO(\n        DdManager *mgr)\n    DdNode *DD_ONE(\n        DdManager *mgr)\n    DdNode *cuddT(\n        DdNode *u)  # top cofactors\n    DdNode *cuddE(\n        DdNode *u)\n    # BDD node elements\n    DdNode *Cudd_Not(\n        DdNode *dd)\n    DdNode *Cudd_Regular(\n        DdNode *u)\n    bool Cudd_IsComplement(\n        DdNode *u)\n    # reference counting\n    void cuddRef(\n        DdNode *u)\n    void cuddDeref(\n        DdNode *u)\n    # recursive ITE\n    DdNode * cuddZddIte(\n        DdManager *dd,\n        DdNode *f, DdNode *g, DdNode *h)\n    # realignment\n    int Cudd_zddRealignmentEnabled(\n        DdManager *unique)\n    void Cudd_zddRealignEnable(\n        DdManager *unique)\n    void Cudd_zddRealignDisable(\n        DdManager *unique)\n    int Cudd_bddRealignmentEnabled(\n        DdManager *unique)\n    void Cudd_bddRealignEnable(\n        DdManager *unique)\n    void Cudd_bddRealignDisable(\n        DdManager *unique)\ncdef extern from 'cudd.h':\n    # node\n    ctypedef unsigned int DdHalfWord\n    struct DdNode:\n        DdHalfWord index\n        DdHalfWord ref\n        DdNode *next\n    # manager\n    DdManager *Cudd_Init(\n        unsigned int numVars,\n        unsigned int numVarsZ,\n        unsigned int numSlots,\n        unsigned int cacheSize,\n        size_t maxMemory)\n    # generator\n    struct DdGen\n    # variables\n    DdNode *Cudd_zddIthVar(\n        DdManager *dd, int index)\n    DdNode *Cudd_bddIthVar(\n        DdManager *dd, int index)\n    DdNode *Cudd_zddSupport(\n        DdManager *dd, DdNode *f)\n    int Cudd_ReadPermZdd(\n        DdManager *dd, int index)\n    int Cudd_ReadInvPermZdd(\n        DdManager *dd, int level)\n    unsigned int Cudd_NodeReadIndex(\n        DdNode *u)\n    # cofactors (of any given `var`, not only top)\n    DdNode *Cudd_zddSubset1(\n        DdManager *dd, DdNode *P, int var)\n    DdNode *Cudd_zddSubset0(\n        DdManager *dd, DdNode *P, int var)\n    # conversions between BDDs and ZDDs\n    int Cudd_zddVarsFromBddVars(\n        DdManager *dd, int multiplicity)\n    DdNode *Cudd_zddPortFromBdd(\n        DdManager *dd, DdNode *B)\n    DdNode *Cudd_zddPortToBdd(\n        DdManager *dd, DdNode *f)\n    # propositional operators\n    DdNode *Cudd_zddIte(\n        DdManager *dd,\n        DdNode *f, DdNode *g, DdNode *h)\n    DdNode *Cudd_zddUnion(\n        DdManager *dd, DdNode *P, DdNode *Q)\n    DdNode *Cudd_zddIntersect(\n        DdManager *dd, DdNode *P, DdNode *Q)\n    DdNode *Cudd_zddDiff(\n        DdManager *dd, DdNode *P, DdNode *Q)\n    # constants\n    DdNode *Cudd_ReadZddOne(\n        DdManager *dd, int i)\n        # `i` is the index of the topmost variable\n    DdNode *Cudd_ReadZero(DdManager *dd)\n    # counting\n    int Cudd_zddDagSize(\n        DdNode *p_node)\n    double Cudd_zddCountMinterm(\n        DdManager *zdd, DdNode *node, int path)\n    int Cudd_zddCount(\n        DdManager *zdd, DdNode *P)\n    int Cudd_BddToCubeArray(\n        DdManager *dd, DdNode *cube, int *array)\n    # pick\n    DdGen *Cudd_zddFirstPath(\n        DdManager *zdd, DdNode *f, int **path)\n    int Cudd_zddNextPath(\n        DdGen *gen, int **path)\n    int Cudd_IsGenEmpty(\n        DdGen *gen)\n    int Cudd_GenFree(\n        DdGen *gen)\n    # info\n    int Cudd_PrintInfo(\n        DdManager *dd, FILE *fp)\n    int Cudd_ReadZddSize(\n        DdManager *dd)\n    long Cudd_zddReadNodeCount(\n        DdManager *dd)\n    long Cudd_ReadPeakNodeCount(\n        DdManager *dd)\n    int Cudd_ReadPeakLiveNodeCount(\n        DdManager *dd)\n    size_t Cudd_ReadMemoryInUse(\n        DdManager *dd)\n    unsigned int Cudd_ReadSlots(\n        DdManager *dd)\n    double Cudd_ReadUsedSlots(\n        DdManager *dd)\n    double Cudd_ExpectedUsedSlots(\n        DdManager *dd)\n    unsigned int Cudd_ReadCacheSlots(\n        DdManager *dd)\n    double Cudd_ReadCacheUsedSlots(\n        DdManager *dd)\n    double Cudd_ReadCacheLookUps(\n        DdManager *dd)\n    double Cudd_ReadCacheHits(\n        DdManager *dd)\n    # reordering\n    ctypedef enum Cudd_ReorderingType:\n        pass\n    void Cudd_AutodynEnableZdd(\n        DdManager *unique,\n        Cudd_ReorderingType method)\n    void Cudd_AutodynDisableZdd(\n        DdManager *unique)\n    int Cudd_ReorderingStatusZdd(\n        DdManager *unique,\n        Cudd_ReorderingType *method)\n    int Cudd_zddReduceHeap(\n        DdManager *table,\n        Cudd_ReorderingType heuristic,\n        int minsize)\n    int Cudd_zddShuffleHeap(\n        DdManager *table, int *permutation)\n    void Cudd_SetSiftMaxSwap(\n        DdManager *dd, int sms)\n    int Cudd_ReadSiftMaxSwap(\n        DdManager *dd)\n    void Cudd_SetSiftMaxVar(\n        DdManager *dd, int smv)\n    int Cudd_ReadSiftMaxVar(\n        DdManager *dd)\n    # The function `Cudd_zddReduceHeap` increments the\n    # counter `dd->reorderings`. The function `Cudd_ReadReorderings`\n    # reads this counter.\n    unsigned int Cudd_ReadReorderings(\n        DdManager *dd)\n    # The function `Cudd_zddReduceHeap` adds to the attribute\n    # `dd->reordTime`. The function `Cudd_ReadReorderingTime`\n    # reads this attribute.\n    long Cudd_ReadReorderingTime(\n        DdManager *dd)\n    # manager config\n    size_t Cudd_ReadMaxMemory(\n        DdManager *dd)\n    size_t Cudd_SetMaxMemory(\n        DdManager *dd, size_t maxMemory)\n    unsigned int Cudd_ReadMaxCacheHard(\n        DdManager *dd)\n    unsigned int Cudd_ReadMaxCache(\n        DdManager *dd)\n    void Cudd_SetMaxCacheHard(\n        DdManager *dd, unsigned int mc)\n    double Cudd_ReadMaxGrowth(\n        DdManager *dd)\n    void Cudd_SetMaxGrowth(\n        DdManager *dd, double mg)\n    unsigned int Cudd_ReadMinHit(\n        DdManager *dd)\n    void Cudd_SetMinHit(\n        DdManager *dd, unsigned int hr)\n    void Cudd_EnableGarbageCollection(\n        DdManager *dd)\n    void Cudd_DisableGarbageCollection(\n        DdManager *dd)\n    int Cudd_GarbageCollectionEnabled(\n        DdManager * dd)\n    unsigned int Cudd_ReadLooseUpTo(\n        DdManager *dd)\n    void Cudd_SetLooseUpTo(\n        DdManager *dd, unsigned int lut)\n    # reference counting\n    void Cudd_Ref(\n        DdNode *n)\n    void Cudd_Deref(\n        DdNode *n)\n    void Cudd_RecursiveDerefZdd(\n        DdManager *table, DdNode *n)\n    int Cudd_CheckZeroRef(\n        DdManager *manager)\n    # checks\n    int Cudd_DebugCheck(\n        DdManager *table)\n    void Cudd_Quit(\n        DdManager *unique)\n    # manager config\n    void Cudd_EnableGarbageCollection(\n        DdManager *dd)\n    void Cudd_DisableGarbageCollection(\n        DdManager *dd)\n    int Cudd_GarbageCollectionEnabled(\n        DdManager * dd)\n    # BDD functions\n    DdNode *Cudd_bddIte(\n        DdManager *dd,\n        DdNode *f, DdNode *g, DdNode *h)\ncdef extern from 'util.h':\n    void FREE(void *ptr)\n\n    # node elements\nctypedef DdNode *DdRef\ncdef CUDD_UNIQUE_SLOTS = 2**8\ncdef CUDD_CACHE_SLOTS = 2**18\ncdef CUDD_REORDER_SIFT = 4\ncdef CUDD_OUT_OF_MEM = -1\ncdef MAX_CACHE = <unsigned int> - 1  # entries\n__version__ = CUDD_VERSION.decode('utf-8')\n\n\n# 2**30 = 1 GiB (gibibyte, read ISO/IEC 80000)\nDEFAULT_MEMORY = 1 * 2**30\nlogger = logging.getLogger(__name__)\n\n\nclass CouldNotCreateNode(Exception):\n    pass\n\n\ncdef class ZDD:\n    \"\"\"Wrapper of CUDD manager.\n\n    Interface similar to `dd._abc.BDD` and `dd.cudd.BDD`.\n    Variable names are strings.\n    Attributes:\n\n      - `vars`: `set` of bit names as `str`ings\n    \"\"\"\n\n    cdef DdManager *manager\n    cdef public object vars\n    cdef public object _index_of_var\n    cdef public object _var_with_index\n\n    def __cinit__(\n            self,\n            memory_estimate:\n                _NumberOfBytes |\n                None=None,\n            initial_cache_size:\n                _Cardinality |\n                None=None,\n            *arg,\n            **kw\n            ) -> None:\n        \"\"\"Initialize ZDD manager.\n\n        @param memory_estimate:\n            maximum allowed memory, in bytes.\n        \"\"\"\n        self.manager = NULL  # prepare for `__dealloc__`\n        total_memory = _utils.total_memory()\n        default_memory = DEFAULT_MEMORY\n        if memory_estimate is None:\n            memory_estimate = default_memory\n        if total_memory is None:\n            pass\n        elif memory_estimate >= total_memory:\n            memory_example = round(total_memory / 2)\n            msg = (\n                'Error in `dd.cudd_zdd.ZDD`: '\n                f'total physical memory is {total_memory} bytes, '\n                f'but requested {memory_estimate} bytes. '\n                'To avoid this error, pass an amount of memory, '\n                f'for example: `ZDD({memory_example})`.')\n            # Motivation is described in\n            # comments inside `dd.cudd.BDD.__cinit__`.\n            print(msg)\n            raise ValueError(msg)\n        if initial_cache_size is None:\n            initial_cache_size = CUDD_CACHE_SLOTS\n        initial_subtable_size = CUDD_UNIQUE_SLOTS\n        initial_n_vars_bdd = 0\n        initial_n_vars_zdd = 0\n        mgr = Cudd_Init(\n            initial_n_vars_bdd,\n            initial_n_vars_zdd,\n            initial_subtable_size,\n            initial_cache_size,\n            memory_estimate)\n        if mgr is NULL:\n            raise RuntimeError(\n                'failed to initialize CUDD DdManager')\n        self.manager = mgr\n\n    def __init__(\n            self,\n            memory_estimate:\n                _NumberOfBytes |\n                None=None,\n            initial_cache_size:\n                _Cardinality |\n                None=None\n            ) -> None:\n        logger.info(f'Using CUDD v{__version__}')\n        self.configure(reordering=True, max_cache_hard=MAX_CACHE)\n        self.vars = set()\n        self._index_of_var = dict()  # map: str -> unique fixed int\n        self._var_with_index = dict()\n\n    def __dealloc__(\n            self\n            ) -> None:\n        if self.manager is NULL:\n            raise RuntimeError(\n                '`self.manager` is `NULL`, which suggests that '\n                'an exception was raised inside the method '\n                '`dd.cudd_zdd.ZDD.__cinit__`.')\n        n = Cudd_CheckZeroRef(self.manager)\n        if n != 0:\n            raise AssertionError(\n                f'Still {n} nodes '\n                'referenced upon shutdown.')\n        Cudd_Quit(self.manager)\n\n    def __eq__(\n            self:\n                ZDD,\n            other:\n                _ty.Optional[ZDD]\n            ) -> _Yes:\n        \"\"\"Return `True` if `other` has same manager.\n\n        If `other is None`, then return `False`.\n        \"\"\"\n        if other is None:\n            return False\n        return self.manager == other.manager\n\n    def __ne__(\n            self:\n                ZDD,\n            other:\n                _ty.Optional[ZDD]\n            ) -> _Yes:\n        \"\"\"Return `True` if `other` has different manager.\n\n        If `other is None`, then return `True`.\n        \"\"\"\n        if other is None:\n            return True\n        return self.manager != other.manager\n\n    def __len__(\n            self\n            ) -> _Cardinality:\n        \"\"\"Return number of nodes with non-zero references.\"\"\"\n        return Cudd_CheckZeroRef(self.manager)\n\n    def __contains__(\n            self,\n            u:\n                Function\n            ) -> _Yes:\n        \"\"\"Return `True` if `u.node` in `self.manager`.\"\"\"\n        if u.manager != self.manager:\n            raise ValueError(\n                'undefined containment, because '\n                '`u.manager != self.manager`')\n        try:\n            Cudd_NodeReadIndex(u.node)\n            return True\n        except:\n            return False\n\n    # This method is similar to\n    # the method `dd.cudd.BDD.__str__`.\n    def __str__(\n            self\n            ) -> str:\n        d = self.statistics()\n        s = (\n            'Zero-omitted binary decision diagram (CUDD wrapper).\\n'\n            '\\t {n} live nodes now\\n'\n            '\\t {peak} live nodes at peak\\n'\n            '\\t {n_vars} ZDD variables\\n'\n            '\\t {mem:10.1f} bytes in use\\n'\n            '\\t {reorder_time:10.1f} sec spent reordering\\n'\n            '\\t {n_reorderings} reorderings\\n').format(\n                n=d['n_nodes'],\n                peak=d['peak_live_nodes'],\n                n_vars=d['n_vars'],\n                reorder_time=d['reordering_time'],\n                n_reorderings=d['n_reorderings'],\n                mem=d['mem'])\n        return s\n\n    # This method is similar to\n    # the method `dd.cudd.BDD.statistics`.\n    def statistics(\n            self:\n                ZDD,\n            exact_node_count:\n                _Yes=False\n            ) -> dict[\n                str,\n                _ty.Any]:\n        \"\"\"Return `dict` with CUDD node counts and times.\n\n        For details read the docstring of the method\n        `dd.cudd.BDD.statistics`.\n        \"\"\"\n        warnings.warn(\n            \"Changed in `dd` version 0.5.7: \"\n            \"In the `dict` returned by the method \"\n            '`dd.cudd_zdd.ZDD.statistics`, '\n            \"the value of the key `'mem'` \"\n            \"has changed to bytes (from 10**6 bytes).\",\n            UserWarning)\n        cdef DdManager *mgr\n        mgr = self.manager\n        n_vars = Cudd_ReadZddSize(mgr)\n        # nodes\n        if exact_node_count:\n            n_nodes = Cudd_zddReadNodeCount(mgr)\n        else:\n            n_nodes = mgr.keys - mgr.dead\n        peak_nodes = Cudd_ReadPeakNodeCount(mgr)\n        peak_live_nodes = Cudd_ReadPeakLiveNodeCount(mgr)\n        # reordering\n        t = Cudd_ReadReorderingTime(mgr)\n        reordering_time = t / 1000.0\n        n_reorderings = Cudd_ReadReorderings(mgr)\n        # memory\n        m = Cudd_ReadMemoryInUse(mgr)\n        mem = float(m)\n        # unique table\n        unique_size = Cudd_ReadSlots(mgr)\n        unique_used_fraction = Cudd_ReadUsedSlots(mgr)\n        expected_unique_fraction = Cudd_ExpectedUsedSlots(mgr)\n        # cache\n        cache_size = Cudd_ReadCacheSlots(mgr)\n        cache_used_fraction = Cudd_ReadCacheUsedSlots(mgr)\n        cache_lookups = Cudd_ReadCacheLookUps(mgr)\n        cache_hits = Cudd_ReadCacheHits(mgr)\n        cache_insertions = mgr.cacheinserts\n        cache_collisions = mgr.cachecollisions\n        cache_deletions = mgr.cachedeletions\n        d = dict(\n            n_vars=n_vars,\n            n_nodes=n_nodes,\n            peak_nodes=peak_nodes,\n            peak_live_nodes=peak_live_nodes,\n            reordering_time=reordering_time,\n            n_reorderings=n_reorderings,\n            mem=mem,\n            unique_size=unique_size,\n            unique_used_fraction=unique_used_fraction,\n            expected_unique_used_fraction=expected_unique_fraction,\n            cache_size=cache_size,\n            cache_used_fraction=cache_used_fraction,\n            cache_lookups=cache_lookups,\n            cache_hits=cache_hits,\n            cache_insertions=cache_insertions,\n            cache_collisions=cache_collisions,\n            cache_deletions=cache_deletions)\n        return d\n\n    # This method is similar to the method\n    # `dd.cudd.BDD.configure`.\n    def configure(\n            self,\n            **kw\n            ) -> dict[\n                str,\n                _ty.Any]:\n        \"\"\"Read and apply parameter values.\n\n        For details read the docstring of the method\n        `dd.cudd.BDD.configure`.\n        \"\"\"\n        cdef int method\n        cdef DdManager *mgr\n        mgr = self.manager\n        # read\n        reordering = Cudd_ReorderingStatusZdd(\n            self.manager, <Cudd_ReorderingType *>&method)\n        garbage_collection = Cudd_GarbageCollectionEnabled(self.manager)\n        max_memory = Cudd_ReadMaxMemory(mgr)\n        loose_up_to = Cudd_ReadLooseUpTo(mgr)\n        max_cache_soft = Cudd_ReadMaxCache(mgr)\n        max_cache_hard = Cudd_ReadMaxCacheHard(mgr)\n        min_hit = Cudd_ReadMinHit(mgr)\n        max_growth = Cudd_ReadMaxGrowth(mgr)\n        max_swaps = Cudd_ReadSiftMaxSwap(mgr)\n        max_vars = Cudd_ReadSiftMaxVar(mgr)\n        d = dict(\n            reordering=\n                True if reordering == 1 else False,\n            garbage_collection=\n                True if garbage_collection == 1 else False,\n            max_memory=max_memory,\n            loose_up_to=loose_up_to,\n            max_cache_soft=max_cache_soft,\n            max_cache_hard=max_cache_hard,\n            min_hit=min_hit,\n            max_growth=max_growth,\n            max_swaps=max_swaps,\n            max_vars=max_vars)\n        # set\n        for k, v in kw.items():\n            if k == 'reordering':\n                if v:\n                    self._enable_reordering()\n                else:\n                    self._disable_reordering()\n            elif k == 'garbage_collection':\n                if v:\n                    Cudd_EnableGarbageCollection(self.manager)\n                else:\n                    Cudd_DisableGarbageCollection(self.manager)\n            elif k == 'max_memory':\n                Cudd_SetMaxMemory(mgr, v)\n            elif k == 'loose_up_to':\n                Cudd_SetLooseUpTo(mgr, v)\n            elif k == 'max_cache_hard':\n                Cudd_SetMaxCacheHard(mgr, v)\n            elif k == 'min_hit':\n                Cudd_SetMinHit(mgr, v)\n            elif k == 'max_growth':\n                Cudd_SetMaxGrowth(mgr, v)\n            elif k == 'max_swaps':\n                Cudd_SetSiftMaxSwap(mgr, v)\n            elif k == 'max_vars':\n                Cudd_SetSiftMaxVar(mgr, v)\n            elif k == 'max_cache_soft':\n                logger.warning('\"max_cache_soft\" not settable.')\n            else:\n                raise ValueError(\n                    f'Unknown parameter \"{k}\"')\n        return d\n\n    cpdef tuple succ(\n            self,\n            u:\n                Function):\n        \"\"\"Return `(level, low, high)` for `u`.\"\"\"\n        if u.manager != self.manager:\n            raise ValueError('`u.manager != self.manager`')\n        i = u.level\n        v = u.low\n        w = u.high\n        if v is not None and i >= v.level:\n            raise AssertionError('v.level')\n        if w is not None and i >= w.level:\n            raise AssertionError('w.level')\n        return i, v, w\n\n    cpdef incref(\n            self,\n            u:\n                Function):\n        \"\"\"Increment the reference count of `u`.\n\n        For details read the docstring of the\n        method `dd.cudd.BDD.incref`.\n        \"\"\"\n        if u.node is NULL:\n            raise RuntimeError('`u.node` is `NULL` pointer.')\n        if u._ref <= 0:\n            _utils.raise_runtimerror_about_ref_count(\n                u._ref, 'method `dd.cudd_zdd.ZDD.incref`',\n                '`dd.cudd_zdd.Function`')\n        assert u._ref > 0, u._ref\n        u._ref += 1\n        self._incref(u.node)\n\n    cpdef decref(\n            self,\n            u:\n                Function,\n            recursive:\n                _Yes=False,\n            _direct:\n                _Yes=False):\n        \"\"\"Decrement the reference count of `u`.\n\n        For details read the docstring of the\n        method `dd.cudd.BDD.decref`.\n\n        @param recursive:\n            if `True`, then call\n            `Cudd_RecursiveDerefZdd`,\n            else call `Cudd_Deref`\n        @param _direct:\n            use this parameter only after\n            reading the source code of the\n            Cython file `dd/cudd_zdd.pyx`.\n            When `_direct == True`, some of the above\n            description does not apply.\n        \"\"\"\n        if u.node is NULL:\n            raise RuntimeError('`u.node` is `NULL` pointer.')\n        # bypass checks and leave `u._ref` unchanged,\n        # directly call `_decref`\n        if _direct:\n            self._decref(u.node, recursive)\n            return\n        if u._ref <= 0:\n            _utils.raise_runtimerror_about_ref_count(\n                u._ref, 'method `dd.cudd_zdd.ZDD.decref`',\n                '`dd.cudd_zdd.Function`')\n        assert u._ref > 0, u._ref\n        u._ref -= 1\n        self._decref(u.node, recursive)\n        if u._ref == 0:\n            u.node = NULL\n\n    cdef _incref(\n            self,\n            u:\n                DdRef):\n        Cudd_Ref(u)\n\n    cdef _decref(\n            self,\n            u:\n                DdRef,\n            recursive:\n                _Yes=False):\n        if recursive:\n            Cudd_RecursiveDerefZdd(self.manager, u)\n        else:\n            Cudd_Deref(u)\n\n    def declare(\n            self,\n            *variables:\n                _VariableName\n            ) -> None:\n        \"\"\"Add names in `variables` to `self.vars`.\"\"\"\n        for var in variables:\n            self.add_var(var)\n\n    cpdef int add_var(\n            self,\n            var:\n                _VariableName,\n            index:\n                _Nat |\n                None=None):\n        \"\"\"Return index of variable named `var`.\n\n        If a variable named `var` exists,\n        then assert that it has `index`.\n        Otherwise, create a variable named `var`\n        with `index` (if given).\n\n        If no reordering has yet occurred,\n        then the returned index equals the level,\n        provided `add_var` has been used so far.\n        \"\"\"\n        # var already exists ?\n        j = self._index_of_var.get(var)\n        if j is not None:\n            if index is not None and j != index:\n                raise AssertionError(j, index)\n            return j\n        # new var\n        if index is None:\n            j = len(self._index_of_var)\n        else:\n            j = index\n        u = Cudd_zddIthVar(self.manager, j)\n        # ref and recursive deref,\n        # in order to cancel refs out\n        Cudd_Ref(u)\n        Cudd_RecursiveDerefZdd(self.manager, u)\n        if u is NULL:\n            raise RuntimeError(\n                f'failed to add var \"{var}\"')\n        self._add_var(var, j)\n        return j\n\n    cdef _add_var(\n            self,\n            var:\n                _VariableName,\n            index:\n                _Nat):\n        \"\"\"Add to `self` a *new* variable named `var`.\"\"\"\n        if var in self.vars:\n            raise ValueError(\n                f'existing variable: {var}')\n        if var in self._index_of_var:\n            raise ValueError(\n                'variable already has '\n                f'index: {self._index_of_var[var]}')\n        if index in self._var_with_index:\n            raise ValueError(\n                'index already corresponds '\n                'to a variable: '\n                f'{self._var_with_index[index]}')\n        self.vars.add(var)\n        self._index_of_var[var] = index\n        self._var_with_index[index] = var\n        if (len(self._index_of_var) !=\n                len(self._var_with_index)):\n            raise AssertionError(\n                'the attributes `_index_of_var` and '\n                '`_var_with_index` have different length')\n\n    cpdef Function var(\n            self,\n            var:\n                _VariableName):\n        \"\"\"Return node for variable named `var`.\"\"\"\n        if var not in self._index_of_var:\n            raise ValueError(\n                f'undeclared variable \"{var}\", '\n                'the declared variables are:\\n'\n                f'{self._index_of_var}')\n        j = self._index_of_var[var]\n        r = _ith_var(var, self)\n        return r\n\n    # CUDD implementation of `var`\n    cpdef Function _var_cudd(\n            self,\n            var:\n                _VariableName):\n        \"\"\"Return node for variable named `var`.\"\"\"\n        if var not in self._index_of_var:\n            raise ValueError(\n                f'undeclared variable \"{var}\", '\n                'the declared variables are:\\n'\n                f'{self._index_of_var}')\n        j = self._index_of_var[var]\n        r = Cudd_zddIthVar(self.manager, j)\n        return wrap(self, r)\n\n    def _add_bdd_var(\n            self,\n            j:\n                _Nat\n            ) -> None:\n        \"\"\"Declare a BDD variable with index `j`.\"\"\"\n        Cudd_bddIthVar(self.manager, j)\n\n    def var_at_level(\n            self,\n            level:\n                _Level\n            ) -> _VariableName:\n        \"\"\"Return name of variable at `level`.\n\n        Raise `ValueError` if `level` is not\n        the level of any variable declared in\n        `self.vars`.\n        \"\"\"\n        j = Cudd_ReadInvPermZdd(self.manager, level)\n        if (j == -1 or j == CUDD_CONST_INDEX or\n                j not in self._var_with_index):\n            raise ValueError(_tw.dedent(f'''\n                No declared variable has level: {level}.\n                {_utils.var_counts(self)}\n                '''))\n        var = self._var_with_index[j]\n        return var\n\n    def level_of_var(\n            self,\n            var:\n                _VariableName\n            ) -> _Level:\n        \"\"\"Return level of variable named `var`.\n\n        Raise `ValueError` if `var` is not\n        a variable in `self.vars`.\n        \"\"\"\n        if var not in self._index_of_var:\n            raise ValueError(\n                f'undeclared variable \"{var}\", '\n                'the declared variables are:\\n'\n                f'{self._index_of_var}')\n        j = self._index_of_var[var]\n        level = Cudd_ReadPermZdd(self.manager, j)\n        if level == -1:\n            raise AssertionError(\n                f'index {j} out of bounds')\n        return level\n\n    @property\n    def var_levels(\n            self\n            ) -> _VariableLevels:\n        \"\"\"Return `dict` that maps variables to levels.\"\"\"\n        return {var: self.level_of_var(var)\n                for var in self.vars}\n\n    def _index_at_level(\n            self,\n            level:\n                _Level\n            ) -> (\n                int |\n                None):\n        \"\"\"Return index of CUDD variable at `level`.\n\n        The presence of such an index does not mean that\n        a variable has been declared for that index.\n\n        Declaring variables with indices over a set of\n        noncontiguous integers creates these intermediate\n        indices. For example:\n\n        ```python\n        import dd.cudd_zdd as _zdd\n        import pytest\n\n        zdd = _zdd.ZDD()\n        zdd.add_var('x', 1)\n        assert zdd.level_of_var('x') == 1\n        # no `ZDD` declared variable at level 0\n        # CUDD does return an index for level 0\n        with pytest.raises(ValueError):\n            zdd.level_of_var(0)\n        # no CUDD variable at level 2\n        with pytest.raises(ValueError):\n            zdd.level_of_var(2)\n        ```\n\n        The usefulness of this method is during\n        recursion over BDDs. If this method returns\n        `None`, then this means that `level`\n        is below the bottommost CUDD variable,\n        therefore also below the bottommost\n        `dd.cudd.ZDD` variable.\n\n        So a return value of `None` means that\n        we have reached the constant nodes\n        (even though it can be\n        `level > CUDD_CONST_INDEX`).\n\n        Checking `len(zdd.vars) == level` is\n        not equivalent, for example:\n\n        ```python\n        import dd.cudd_zdd as _zdd\n\n        zdd = _zdd.ZDD()\n        zdd.add_var('x', 2)\n        n_declared_vars = len(zdd.vars)\n        max_var_level = max(\n            zdd.level_of_var(var)\n            for var in zdd.vars)\n        n_cudd_vars = zdd._number_of_cudd_vars()\n        print(f'''\n            {n_vars = }\n            {max_var_level = }\n            {n_cudd_vars = }\n            ''')\n        ```\n\n        So if the recursion stopped at level `n_vars`,\n        it would not reach the nodes at `max_var_level`.\n\n        Any computation with noncontiguous levels of\n        declared variables is equivalent to a computation\n        with contiguous levels.\n\n        @param level:\n            level >= 0 for which the corresponding\n            CUDD variable index will be returned,\n            if it exists\n        \"\"\"\n        # `Cudd_ReadInvPermZdd()`:\n        # - returns `CUDD_CONST_INDEX` if\n        #   `level == CUDD_CONST_INDEX`\n        # - returns `invperm[self.manager.level]` if\n        #   `0 <= level < self.manager.sizeZ`\n        # - otherwise returns `-1`\n        j = Cudd_ReadInvPermZdd(self.manager, level)\n        if j == -1:\n            return None\n        if j < 0:\n            raise RuntimeError(\n                f'unexpected value: {j}, returned from '\n                'CUDD function `Cudd_ReadInvPermZdd()`')\n        return j\n\n    cpdef python_bool _gt_var_levels(\n            self,\n            level:\n                _Level):\n        \"\"\"Return `True` if `level` > any variable level.\n\n        Raise `ValueError` if `level < 0`.\n        Similar to how `Cudd_ReadInvPermZdd()` works.\n\n        Note that the constant nodes are below all\n        variable levels:\n\n        ```python\n        import dd.cudd_zdd\n\n        zdd = dd.cudd_zdd.ZDD()\n        assert zdd._gt_var_levels(zdd.false.level)\n        ```\n\n        Read also `_index_at_level()`\n\n        @param level:\n            >= 0\n        @return:\n            `True` if any CUDD variable has\n            level < of given `level`\n        \"\"\"\n        if level < 0:\n            raise ValueError(\n                f'requires `level >= 0`, got:  {level}')\n        n_cudd_vars = Cudd_ReadZddSize(self.manager)\n        if 0 <= n_cudd_vars <= CUDD_CONST_INDEX:\n            return level >= n_cudd_vars\n        raise RuntimeError(_tw.dedent(f'''\n            Unexpected value: {n_cudd_vars} returned\n            from CUDD function `Cudd_ReadZddSize()`\n            (0 <= expected <= {CUDD_CONST_INDEX} =\n             CUDD_CONST_INDEX)\n            '''))\n\n    cpdef int _number_of_cudd_vars(\n            self):\n        \"\"\"Return number of CUDD indices.\n\n        Can be `> len(self.vars)`, because CUDD creates\n        variable indices also for levels in between\n        those of variables declared using `ZDD.declare()`.\n\n        Read also `_index_at_level()`.\n\n        @rtype:\n            `int` >= 0\n        \"\"\"\n        n_cudd_vars = Cudd_ReadZddSize(self.manager)\n        if 0 <= n_cudd_vars <= CUDD_CONST_INDEX:\n            return n_cudd_vars\n        raise RuntimeError(_tw.dedent(f'''\n            Unexpected value: {n_cudd_vars} returned\n            from CUDD function `Cudd_ReadZddSize()`\n            (0 <= expected <= {CUDD_CONST_INDEX} =\n             CUDD_CONST_INDEX)\n            '''))\n\n    def reorder(\n            self,\n            var_order:\n                _VariableLevels |\n                None=None\n            ) -> None:\n        \"\"\"Reorder variables to `var_order`.\n\n        If `var_order` is `None`, then invoke sifting.\n        \"\"\"\n        if var_order is None:\n            Cudd_zddReduceHeap(self.manager, CUDD_REORDER_SIFT, 1)\n            return\n        n = len(var_order)\n        if n != len(self.vars):\n            raise ValueError(\n                'Mismatch of variable numbers:\\n'\n                'the number of declared variables is:  '\n                f'{len(self.vars)}\\n'\n                f'new variable order has length:  {n}')\n        cdef int *p\n        p = <int *> PyMem_Malloc(n * sizeof(int))\n        for var, level in var_order.items():\n            index = self._index_of_var[var]\n            p[level] = index\n        try:\n            r = Cudd_zddShuffleHeap(self.manager, p)\n        finally:\n            PyMem_Free(p)\n        if r != 1:\n            raise RuntimeError('Failed to reorder.')\n\n    def _enable_reordering(\n            self\n            ) -> None:\n        \"\"\"Enable dynamic reordering of ZDDs.\"\"\"\n        Cudd_AutodynEnableZdd(self.manager, CUDD_REORDER_SIFT)\n\n    def _disable_reordering(\n            self\n            ) -> None:\n        \"\"\"Disable dynamic reordering of ZDDs.\"\"\"\n        Cudd_AutodynDisableZdd(self.manager)\n\n    cpdef set support(\n            self,\n            u:\n                Function):\n        \"\"\"Return `set` of variables that `u` depends on.\n\n        These are the variables that the Boolean function\n        represented by the ZDD with root `u` depends on.\n        \"\"\"\n        if self.manager != u.manager:\n            raise ValueError('`u.manager != self.manager`')\n        return _c_support(u)\n\n    cpdef set _support_py(\n            self,\n            u:\n                Function):\n        # Python implementation\n        if self.manager != u.manager:\n            raise ValueError('`u.manager != self.manager`')\n        visited = set()\n        support = set()\n        level = 0\n        self._support(level, u, support, visited)\n        return support\n\n    cdef _support(\n            self,\n            level:\n                _c_level,\n            u:\n                Function,\n            support:\n                set[_VariableName],\n            visited:\n                set[Function]):\n        \"\"\"Recurse to compute the support of `u`.\"\"\"\n        # terminal ?\n        if u == self.false or self._gt_var_levels(level):\n            return\n        if u in visited:\n            return\n        var = self.var_at_level(level)\n        u_level, v, w = self.succ(u)\n        if level > u_level:\n            raise ValueError(level, u_level)\n        if level < u_level:\n            support.add(var)\n            self._support(level + 1, u, support, visited)\n        elif v == w:\n            self._support(level + 1, v, support, visited)\n        else:\n            support.add(var)\n            self._support(level + 1, v, support, visited)\n            self._support(level + 1, w, support, visited)\n        visited.add(u)\n\n    cpdef set _support_cudd(\n            self,\n            f:\n                Function):\n        \"\"\"Return `set` of variables that node `f` depends on.\"\"\"\n        if self.manager != f.manager:\n            raise ValueError('`f.manager != self.manager`')\n        r: DdRef\n        r = Cudd_zddSupport(self.manager, f.node)\n        f = wrap(self, r)\n        supp = self._cube_to_dict(f)\n        # constant ?\n        if not supp:\n            return set()\n        # must be positive unate\n        if set(supp.values()) != {True}:\n            raise AssertionError(supp)\n        return set(supp)\n\n    def _copy_bdd_vars(\n            self,\n            bdd\n            ) -> None:\n        \"\"\"Copy BDD to ZDD variables.\"\"\"\n        Cudd_zddVarsFromBddVars(self.manager, 1)\n\n    def _bdd_to_zdd(\n            self,\n            u:\n                Function\n            ) -> Function:\n        \"\"\"Copy BDD `u` to a ZDD in `self`.\n\n        @param u:\n            node in a `dd.cudd.BDD` manager\n        @type u:\n            `dd.cudd.Function`\n        \"\"\"\n        r: DdRef\n        bdd = u.bdd\n        u_ = bdd.copy(u, self)\n        r = <DdRef>u_.node\n        r = Cudd_zddPortFromBdd(self.manager, r)\n        return wrap(self, r)\n\n    def copy(\n            self,\n            u:\n                Function,\n            other):\n        \"\"\"Transfer ZDD with root `u` to `other`.\n\n        @param other:\n            `ZDD` or `BDD` manager\n        @type other:\n            | `dd.cudd_zdd.ZDD`\n            | `dd.cudd.BDD`\n            | `dd.autoref.BDD`\n        @rtype:\n            | `dd.cudd_zdd.Function`\n            | `dd.cudd.Function`\n            | `dd.autoref.Function`\n        \"\"\"\n        return _copy.copy_zdd(u, other)\n\n    cpdef Function let(\n            self,\n            definitions:\n                _Renaming |\n                _Assignment |\n                dict[_VariableName, Function],\n            u:\n                Function):\n        \"\"\"Replace variables with `definitions` in `u`.\n\n        @param definitions:\n            maps variable names\n            to either:\n            - Boolean values, or\n            - variable names, or\n            - ZDD nodes\n        @rtype:\n            `Function`\n        \"\"\"\n        if self.manager != u.manager:\n            raise ValueError('`u.manager != self.manager`')\n        d = definitions\n        if not d:\n            logger.warning(\n                'Call to `ZDD.let` with no effect, '\n                'because the dictionary `definitions` '\n                'is empty.')\n            return u\n        var = next(iter(d))\n        value = d[var]\n        if isinstance(value, python_bool):\n            return self._cofactor_root(u, d)\n        elif isinstance(value, Function):\n            # return self._compose_root(u, d)\n            return _c_compose(u, d)\n        try:\n            value + 's'\n        except TypeError:\n            raise ValueError(\n                'Value must be variable name as `str`, '\n                'or Boolean value as `bool`, '\n                f'or ZDD node as `int`. Got: {value}')\n        return self._rename(u, d)\n\n    cpdef Function _cofactor_root(\n            self,\n            u:\n                Function,\n            d:\n                _Assignment):\n        \"\"\"Return cofactor of `u` as defined in `d`.\n\n        @param d:\n            maps variable names to Boolean values\n        \"\"\"\n        if self.manager != u.manager:\n            raise ValueError('`u.manager != self.manager`')\n        level = 0\n        self.manager.reordered = 1\n        while self.manager.reordered == 1:\n            cache = dict()\n            self.manager.reordered = 0\n            try:\n                r = self._cofactor(level, u, d, cache)\n            except CouldNotCreateNode:\n                r = None\n        return r\n\n    cpdef Function _cofactor(\n            self,\n            level:\n                _Level,\n            u:\n                Function,\n            d:\n                _Assignment,\n            cache:\n                dict[\n                    tuple[Function, _Level],\n                    Function]):\n        \"\"\"Recursively compute the cofactor of `u`.\"\"\"\n        # terminal ?\n        if u == self.false or self._gt_var_levels(level):\n            return u\n        t = (u, level)\n        if t in cache:\n            return cache[t]\n        var = self.var_at_level(level)\n        i = u.level\n        if level > i:\n            raise ValueError((level, i))\n        if level < i:\n            if var in d:\n                value = d[var]\n                if value:\n                    r = self.false\n                else:\n                    r = self._cofactor(level + 1, u, d, cache)\n                r = self.find_or_add(var, r, r)\n            else:\n                r = self._cofactor(level + 1, u, d, cache)\n        else:\n            if level != i:\n                raise AssertionError((level, i))\n            _, v, w = self.succ(u)\n            if var in d:\n                value = d[var]\n                if value:\n                    r = self._cofactor(level + 1, w, d, cache)\n                else:\n                    r = self._cofactor(level + 1, v, d, cache)\n                r = self.find_or_add(var, r, r)\n            else:\n                p = self._cofactor(level + 1, v, d, cache)\n                q = self._cofactor(level + 1, w, d, cache)\n                r = self.find_or_add(var, p, q)\n        cache[t] = r\n        return r\n\n    cpdef Function _cofactor_cudd(\n            self,\n            u:\n                Function,\n            var:\n                _VariableName,\n            value:\n                python_bool):\n        \"\"\"CUDD implementation of cofactor.\"\"\"\n        if self.manager != u.manager:\n            raise ValueError('`u.manager != self.manager`')\n        r: DdRef\n        index = self._index_of_var[var]\n        if value:\n            r = Cudd_zddSubset1(self.manager, u.node, index)\n        else:\n            r = Cudd_zddSubset0(self.manager, u.node, index)\n        return wrap(self, r)\n\n    cpdef Function _compose_root(\n            self,\n            u:\n                Function,\n            d:\n                dict[\n                    _VariableName,\n                    Function]):\n        \"\"\"Return the composition defined in `d`.\n\n        @param d:\n            `dict` from variable names (`str`)\n            to ZDD nodes (`Function`).\n        \"\"\"\n        if self.manager != u.manager:\n            raise ValueError('`u.manager != self.manager`')\n        self.manager.reordered = 1\n        while self.manager.reordered == 1:\n            cache = dict()\n            self.manager.reordered = 0\n            try:\n                r = self._compose(0, u, d, cache)\n            except CouldNotCreateNode:\n                r = None\n        return r\n\n    cpdef Function _compose(\n            self,\n            level:\n                _Level,\n            u:\n                Function,\n            d:\n                dict[\n                    _VariableName,\n                    Function],\n            cache:\n                dict[\n                    tuple[Function, _Level],\n                    Function]):\n        \"\"\"Recursively compute composition of `u`.\"\"\"\n        if level > u.level:\n            raise ValueError(\n                'requires `level <= u.level`, but: '\n                f'{level = } > {u.level = }')\n        # terminal ?\n        if u == self.false:\n            return u\n        if self._gt_var_levels(level):\n            return self.true  # full ZDDs at output\n        t = (u, level)\n        if t in cache:\n            return cache[t]\n        var = self.var_at_level(level)\n        u_level = u.level\n        if var in d:\n            g = d[var]\n        else:\n            g = self.var(var)\n        if level < u_level:\n            if level + 1 > u.level:\n                raise ValueError(\n                    'expected `level + 1 <= u.level`, but '\n                    f'{level + 1} = level + 1 > '\n                    f'u.level = {u.level}')\n            r = self._compose(level + 1, u, d, cache)\n            r = self._ite_recursive(g, self.false, r)\n        else:\n            if level != u.level:\n                raise AssertionError(\n                    'expected `level == u.level`, but '\n                    f'{level} = level != '\n                    f'u.level = {u.level}')\n            _, v, w = self.succ(u)\n            if level + 1 > v.level:\n                raise AssertionError(\n                    'expected `level + 1 <= v.level`, but '\n                    f'{level + 1} = level + 1 > '\n                    f'v.level = {v.level}')\n            if level + 1 > w.level:\n                raise AssertionError(\n                    'expected `level + 1 <= w.level`, but '\n                    f'{level + 1} = level + 1 > '\n                    f'w.level = {w.level}')\n            p = self._compose(level + 1, v, d, cache)\n            if level + 1 > w.level:\n                raise AssertionError(\n                    'expected `level + 1 <= w.level`, but '\n                    f'{level + 1} = level + 1 > '\n                    f'w.level = {w.level}')\n            q = self._compose(level + 1, w, d, cache)\n            r = self._ite_recursive(g, q, p)\n        cache[t] = r\n        return r\n\n    cpdef Function _rename(\n            self,\n            u:\n                Function,\n            d:\n                _Renaming):\n        \"\"\"Return node from renaming in `u` the variables in `d`.\"\"\"\n        if self.manager != u.manager:\n            raise ValueError('`u.manager != self.manager`')\n        rename = {k: self.var(v) for k, v in d.items()}\n        # return self._compose_root(u, rename)\n        return _c_compose(u, rename)\n\n    cpdef Function ite(\n            self,\n            g:\n                Function,\n            u:\n                Function,\n            v:\n                Function):\n        \"\"\"Ternary conditional `IF g THEN u ELSE v` for ZDDs.\n\n        Calls `Cudd_zddIte`.\n        \"\"\"\n        # for calling `cuddZddIte`\n        # read the method `_ite_recursive`\n        if g.manager != self.manager:\n            raise ValueError('`g.manager != self.manager`')\n        if u.manager != self.manager:\n            raise ValueError('`u.manager != self.manager`')\n        if v.manager != self.manager:\n            raise ValueError('`v.manager != self.manager`')\n        r: DdRef\n        r = Cudd_zddIte(self.manager, g.node, u.node, v.node)\n        if r is NULL:\n            raise CouldNotCreateNode()\n        return wrap(self, r)\n\n    cpdef Function _ite_recursive(\n            self,\n            g:\n                Function,\n            u:\n                Function,\n            v:\n                Function):\n        \"\"\"Recursive call to ternary conditional.\n\n        Raises `CouldNotCreateNode` if reordering occurred.\n        Calls `cuddZddIte`.\n        \"\"\"\n        if g.manager != self.manager:\n            raise ValueError('`g.manager != self.manager`')\n        if u.manager != self.manager:\n            raise ValueError('`u.manager != self.manager`')\n        if v.manager != self.manager:\n            raise ValueError('`v.manager != self.manager`')\n        r: DdRef\n        r = cuddZddIte(self.manager, g.node, u.node, v.node)\n        if r is NULL:\n            raise CouldNotCreateNode()\n        return wrap(self, r)\n\n    cpdef Function find_or_add(\n            self,\n            var:\n                _VariableName,\n            low:\n                Function,\n            high:\n                Function):\n        \"\"\"Return node `IF var THEN high ELSE low`.\"\"\"\n        if low.manager != self.manager:\n            raise ValueError('`low.manager != self.manager`')\n        if high.manager != self.manager:\n            raise ValueError('`high.manager != self.manager`')\n        if var not in self.vars:\n            raise ValueError(\n                f'undeclared variable: {var}, '\n                f'not in: {self.vars}')\n        level = self.level_of_var(var)\n        if level >= low.level:\n            raise ValueError(level, low.level, 'low.level')\n        if level >= high.level:\n            raise ValueError(level, high.level, 'high.level')\n        r: DdRef\n        index = self._index_of_var[var]\n        if high == self.false:\n            r = low.node\n        else:\n            r = cuddUniqueInterZdd(\n                self.manager, index, high.node, low.node)\n            if r is NULL:\n                raise CouldNotCreateNode()\n        f = wrap(self, r)\n        if level > f.level:\n            raise AssertionError(level, f.level, 'f.level')\n        return f\n\n    cdef DdRef _find_or_add(\n            self,\n            index:\n                int,\n            low:\n                DdRef,\n            high:\n                DdRef\n            ) except NULL:\n        \"\"\"Implementation of method `find_or_add` in C.\"\"\"\n        r: DdRef\n        if low is NULL:\n            raise AssertionError('`low is NULL`')\n        if high is NULL:\n            raise AssertionError('`high is NULL`')\n        if high == Cudd_ReadZero(self.manager):\n            return low\n        r = cuddUniqueInterZdd(\n            self.manager, index, high, low)\n        if r is NULL:\n            raise AssertionError('r is NULL')\n        return r\n\n    cpdef tuple _top_cofactor(\n            self,\n            u:\n                Function,\n            level:\n                _Level):\n        \"\"\"Return cofactor at `level`.\"\"\"\n        u_level = u.level\n        if level > u_level:\n            raise ValueError((level, u_level))\n        if level < u_level:\n            return (u, self.false)\n        v, w = u.low, u.high\n        if v is None:\n            raise AssertionError('`v is None`')\n        if w is None:\n            raise AssertionError('`w is None`')\n        return (v, w)\n\n    def count(\n            self,\n            u:\n                Function,\n            nvars:\n                _Cardinality |\n                None=None\n            ) -> _Cardinality:\n        \"\"\"Return nuber of models of node `u`.\n\n        @param nvars:\n            regard `u` as an operator that\n            depends on `nvars` many variables.\n\n            If omitted, then assume those in `support(u)`.\n        \"\"\"\n        if u.manager != self.manager:\n            raise ValueError('nodes from different managers')\n        support = self.support(u)\n        r = self._count(0, u, support, cache=dict())\n        n_support = len(support)\n        if nvars == None:\n            nvars = n_support\n        if nvars < n_support:\n            raise ValueError((nvars, n_support))\n        return r * 2**(nvars - n_support)\n\n    def _count(\n            self,\n            level:\n                _Level,\n            u:\n                Function,\n            support:\n                set[_VariableName],\n            cache:\n                dict[Function, Function]\n            ) -> _Cardinality:\n        \"\"\"Recurse to count satisfying assignments.\"\"\"\n        if u == self.false:\n            return 0\n        if self._gt_var_levels(level):\n            return 1\n        if u in cache:\n            return cache[u]\n        v, w = self._top_cofactor(u, level)\n        var = self.var_at_level(level)\n        if var in support:\n            nv = self._count(level + 1, v, support, cache)\n            nw = self._count(level + 1, w, support, cache)\n            r = nv + nw\n        else:\n            r = self._count(level + 1, v, support, cache)\n        cache[u] = r\n        return r\n\n    def _count_cudd(\n            self,\n            u:\n                Function,\n            nvars:\n                _Cardinality\n            ) -> _Cardinality:\n        \"\"\"CUDD implementation of `self.count`.\"\"\"\n        # returns different results\n        if u.manager != self.manager:\n            raise ValueError('nodes from different managers')\n        n = len(self.support(u))\n        if nvars < n:\n            raise ValueError((nvars, n))\n        r = Cudd_zddCountMinterm(self.manager, u.node, nvars)\n        if r == CUDD_OUT_OF_MEM:\n            raise RuntimeError('CUDD out of memory')\n        if r == float('inf'):\n            raise RuntimeError('overflow of integer type double')\n        return r\n\n    def pick(\n            self,\n            u:\n                Function,\n            care_vars:\n                _abc.Set[\n                    _VariableName] |\n                None=None\n            ) -> (\n                _Assignment |\n                None):\n        \"\"\"Return a single satisfying assignment as `dict`.\"\"\"\n        return next(self.pick_iter(u, care_vars), None)\n\n    def pick_iter(\n            self,\n            u:\n                Function,\n            care_vars:\n                _abc.Set[\n                    _VariableName] |\n                None=None\n            ) -> _abc.Iterable[\n                _Assignment]:\n        \"\"\"Return iterator over satisfying assignments.\n\n        The returned iterator is generator-based.\n        \"\"\"\n        if self.manager != u.manager:\n            raise ValueError('nodes from different managers')\n        support = self.support(u)\n        if care_vars is None:\n            care_vars = support\n        missing = support.difference(care_vars)\n        if missing:\n            logger.warning(\n                'Missing bits:  '\n                rf'support \\ care_vars = {missing}')\n        cube = dict()\n        value = True\n        config = self.configure(reordering=False)\n        level = 0\n        for cube in self._sat_iter(level, u, cube, value, support):\n            for m in _bdd._enumerate_minterms(cube, care_vars):\n                yield m\n        self.configure(reordering=config['reordering'])\n\n    def _pick_iter_cudd(\n            self,\n            u:\n                Function,\n            care_vars:\n                _abc.Set[\n                    _VariableName] |\n                None=None\n            ) -> _abc.Iterable[\n                _Assignment]:\n        \"\"\"CUDD implementation of `self.pick_iter`.\"\"\"\n        # assigns also to variables outside the support\n        if self.manager != u.manager:\n            raise ValueError('nodes from different ZDD managers')\n        cdef DdGen *gen\n        cdef int *path\n        cdef double value\n        support = self.support(u)\n        if care_vars is None:\n            care_vars = support\n        missing = support.difference(care_vars)\n        if missing:\n            logger.warning(\n                'Missing bits:  '\n                rf'support \\ care_vars = {missing}')\n        config = self.configure(reordering=False)\n        gen = Cudd_zddFirstPath(self.manager, u.node, &path)\n        if gen is NULL:\n            raise RuntimeError('first path failed')\n        try:\n            r = 1\n            while Cudd_IsGenEmpty(gen) == 0:\n                if r != 1:\n                    raise RuntimeError(\n                        f'gen not empty but no next path: {r}')\n                d = _path_array_to_dict(path, self._index_of_var)\n                if not set(d).issubset(support):\n                    raise AssertionError(\n                        set(d).difference(support))\n                for m in _bdd._enumerate_minterms(d, care_vars):\n                    yield m\n                r = Cudd_zddNextPath(gen, &path)\n        finally:\n            Cudd_GenFree(gen)\n        self.configure(reordering=config['reordering'])\n\n    def _sat_iter(\n            self,\n            level:\n                _Level,\n            u:\n                Function,\n            cube:\n                _Assignment,\n            value:\n                python_bool,\n            support:\n                set[_VariableName]\n            ) -> _abc.Iterable[\n                _Assignment]:\n        \"\"\"Recurse to enumerate models.\"\"\"\n        # terminal ?\n        if u == self.false or self._gt_var_levels(level):\n            if u != self.false:\n                if not set(cube).issubset(support):\n                    raise ValueError(\n                        set(cube).difference(support))\n                yield cube\n            return\n        # non-terminal\n        i, v, w = self.succ(u)\n        var = self.var_at_level(level)\n        if level > i:\n            raise ValueError((level, i))\n        if level < i:\n            cube[var] = False\n            if var not in support:\n                raise ValueError((var, support, '<'))\n            for x in self._sat_iter(\n                    level + 1, u, cube, value, support):\n                yield x\n        elif v != w:\n            if level != i:\n                raise AssertionError((level, i))\n            if var not in support:\n                raise ValueError((var, support, '=='))\n            d0 = dict(cube)\n            d0[var] = False\n            d1 = dict(cube)\n            d1[var] = True\n            for x in self._sat_iter(\n                    level + 1, v, d0, value, support):\n                yield x\n            for x in self._sat_iter(\n                    level + 1, w, d1, value, support):\n                yield x\n        else:\n            if level != i:\n                raise AssertionError((level, i))\n            if not (v == w):\n                raise AssertionError((v, w))\n            for x in self._sat_iter(\n                    level + 1, v, cube, value, support):\n                yield x\n\n    cpdef Function apply(\n            self,\n            op:\n                _dd_abc.OperatorSymbol,\n            u:\n                Function,\n            v:\n                _ty.Optional[Function]\n                =None,\n            w:\n                _ty.Optional[Function]\n                =None):\n        r\"\"\"Return as `Function` the result of applying `op`.\n\n        @type op:\n            `str` in\n            - `'~'`, `'not'`, `'!'`\n              (logical negation)\n            - `'/\\\\'`, `'and'`, `'&'`, `'&&'`\n              (conjunction)\n            - `'or'`, `r'\\/'`, `'|'`, `'||'`\n              (disjunction)\n            - `'#'`, `'xor'`, `'^'`\n              (different values)\n            - `'=>'`, `'implies'`, `'->'`\n              (logical implication)\n            - `'<=>'`, `'equiv'`, `'<->'`\n              (logical equivalence)\n            - `'ite'`\n              (ternary conditional)\n            - `r'\\A'`, `'forall'`\n              (universal quantification)\n            - `r'\\E'`, `'exists'`\n              (existential quantification)\n            - `'-'`\n              (`a - b` means `a /\\ ~ b`)\n        \"\"\"\n        _utils.assert_operator_arity(op, v, w, 'bdd')\n        if self.manager != u.manager:\n            raise ValueError(\n                'node `u` is from different ZDD manager')\n        if v is not None and self.manager != v.manager:\n            raise ValueError(\n                'node `v` is from different ZDD manager')\n        if w is not None and self.manager != w.manager:\n            raise ValueError(\n                'node `w` is from different ZDD manager')\n        r: DdRef\n        neg_node: DdRef\n        t: Function\n        cdef DdManager *mgr\n        mgr = u.manager\n        # unary\n        r = NULL\n        if op in ('~', 'not', '!'):\n            r = Cudd_zddDiff(\n                mgr, Cudd_ReadZddOne(mgr, 0), u.node)\n        # binary\n        elif op in ('and', '/\\\\', '&', '&&'):\n            r = Cudd_zddIntersect(mgr, u.node, v.node)\n        elif op in ('or', r'\\/', '|', '||'):\n            r = Cudd_zddUnion(mgr, u.node, v.node)\n        elif op in ('#', 'xor', '^'):\n            neg_node = Cudd_zddDiff(\n                mgr, Cudd_ReadZddOne(mgr, 0), v.node)\n            neg = wrap(self, neg_node)\n            r = Cudd_zddIte(mgr, u.node, neg.node, v.node)\n        elif op in ('=>', '->', 'implies'):\n            r = Cudd_zddIte(\n                mgr,\n                u.node, v.node, Cudd_ReadZddOne(mgr, 0))\n        elif op in ('<=>', '<->', 'equiv'):\n            neg_node = Cudd_zddDiff(\n                mgr, Cudd_ReadZddOne(mgr, 0), v.node)\n            neg = wrap(self, neg_node)\n            r = Cudd_zddIte(mgr, u.node, v.node, neg.node)\n        elif op in ('diff', '-'):\n            r = Cudd_zddDiff(mgr, u.node, v.node)\n        elif op in (r'\\A', 'forall'):\n            qvars = self.support(u)\n            cube = _dict_to_zdd(qvars, v.zdd)\n            r = _forall_root(\n                mgr, v.node, cube.node)\n        elif op in (r'\\E', 'exists'):\n            qvars = self.support(u)\n            cube = _dict_to_zdd(qvars, v.zdd)\n            r = _exist_root(\n                mgr, v.node, cube.node)\n        # ternary\n        elif op == 'ite':\n            r = Cudd_zddIte(mgr, u.node, v.node, w.node)\n        else:\n            raise ValueError(\n                f'unknown operator: \"{op}\"')\n        if r is NULL:\n            config = self.configure()\n            raise RuntimeError(\n                'CUDD appears to have run out of memory.\\n'\n                f'Computing the operator {op}\\n.'\n                'Current settings for upper bounds:\\n'\n                f'    max memory = {config[\"max_memory\"]} bytes\\n'\n                f'    max cache = {config[\"max_cache_hard\"]} entries')\n        res = wrap(self, r)\n        return res\n\n    cpdef Function _add_int(\n            self,\n            i:\n                int):\n        \"\"\"Return node from integer `i`.\"\"\"\n        u: DdRef\n        if i in (0, 1):\n            raise ValueError(\n                rf'{i} \\in {{0, 1}}')\n        # invert `Function.__int__`\n        if 2 <= i:\n            i -= 2\n        u = <DdRef><stdint.uintptr_t>i\n        return wrap(self, u)\n\n    cpdef Function cube(\n            self,\n            dvars:\n                _abc.Collection[\n                    _VariableName]):\n        \"\"\"Return conjunction of variables in `dvars`.\n\n        If `dvars` is a `dict`, then a Boolean value\n        `False` results in a negated variable.\n\n        @param dvars:\n            `dict` or container of variables\n            as `str`\n        @rtype:\n            `Function`\n        \"\"\"\n        r = self.true\n        for var in dvars:\n            u = self.var(var)\n            if isinstance(dvars, dict):\n                value = dvars[var]\n            else:\n                value = True\n            if value is True:\n                r &= u\n            elif value is False:\n                r &= ~ u\n            else:\n                raise ValueError(\n                    f'value not Boolean: {value}')\n        return r\n\n    cpdef Function _disjoin_root(\n            self,\n            u:\n                Function,\n            v:\n                Function):\n        \"\"\"Disjoin `u` and `v`.\"\"\"\n        level = 0\n        self.manager.reordered = 1\n        while self.manager.reordered == 1:\n            try:\n                self.manager.reordered = 0\n                cache = dict()\n                r = self._disjoin(level, u, v, cache)\n            except CouldNotCreateNode:\n                r = None\n        return r\n\n    cdef Function _disjoin(\n            self,\n            level:\n                _c_level,\n            u:\n                Function,\n            v:\n                Function,\n            cache:\n                dict[\n                    tuple[Function, Function],\n                    Function]):\n        \"\"\"Recursively disjoin `u` and `v`.\n\n        The recursion starts at `level`.\n        \"\"\"\n        if u == self.false:\n            return v\n        if v == self.false:\n            return u\n        if self._gt_var_levels(level):\n            if u.low is not None:\n                raise ValueError(\n                    (u, u.low, level, self.var_levels))\n            if v.low is not None:\n                raise ValueError(\n                    (v, v.low, level, self.var_levels))\n            if u != v:\n                raise AssertionError(\n                    (level, u, v))\n            return u\n        t = (u, v)\n        if t in cache:\n            return cache[t]\n        pu, qu = self._top_cofactor(u, level)\n        pv, qv = self._top_cofactor(v, level)\n        p = self._disjoin(level + 1, pu, pv, cache)\n        q = self._disjoin(level + 1, qu, qv, cache)\n        var = self.var_at_level(level)\n        r = self.find_or_add(var, p, q)\n        cache[t] = r\n        return r\n\n    cpdef Function _conjoin_root(\n            self,\n            u:\n                Function,\n            v:\n                Function):\n        \"\"\"Conjoin `u` and `v`.\"\"\"\n        level = 0\n        self.manager.reordered = 1\n        while self.manager.reordered == 1:\n            try:\n                self.manager.reordered = 0\n                cache = dict()\n                r = self._conjoin(level, u, v, cache)\n            except CouldNotCreateNode:\n                r = None\n        return r\n\n    cdef Function _conjoin(\n            self,\n            level:\n                _c_level,\n            u:\n                Function,\n            v:\n                Function,\n            cache):\n        \"\"\"Recursively conjoin `u` and `v`.\n\n        The recursion starts at `level`.\n        \"\"\"\n        if u == self.false:\n            return u\n        if v == self.false:\n            return v\n        if self._gt_var_levels(level):\n            if u.low is not None:\n                raise ValueError(\n                    (u, u.low, level, self.var_levels))\n            if v.low is not None:\n                raise ValueError(\n                    (v, v.low, level, self.var_levels))\n            if u != v:\n                raise AssertionError(\n                    (level, u, v))\n            return u\n        t = (u, v)\n        if t in cache:\n            return cache[t]\n        pu, qu = self._top_cofactor(u, level)\n        pv, qv = self._top_cofactor(v, level)\n        p = self._conjoin(level + 1, pu, pv, cache)\n        q = self._conjoin(level + 1, qu, qv, cache)\n        var = self.var_at_level(level)\n        r = self.find_or_add(var, p, q)\n        cache[t] = r\n        return r\n\n    cpdef Function quantify(\n            self,\n            u:\n                Function,\n            qvars:\n                _abc.Iterable[\n                    _VariableName],\n            forall:\n                _Yes=False):\n        \"\"\"Abstract variables `qvars` from node `u`.\"\"\"\n        if u.manager != self.manager:\n            raise ValueError('`u.manager != self.manager`')\n        # similar to the C implementation\n        # return self._quantify_using_cube_root(\n        #     u, qvars, forall)\n\n        # implementation that uses a `dict`\n        # return self._quantify_root(\n        #     u, qvars, forall)\n\n        # C implementation\n        r: Function\n        if forall:\n            r = _c_forall(qvars, u)\n        else:\n            r = _c_exist(qvars, u)\n        return r\n\n    cpdef Function _quantify_root(\n            self,\n            u:\n                Function,\n            qvars:\n                _abc.Container[\n                    _VariableName],\n            forall:\n                _Yes=False):\n        \"\"\"Abstract variables `qvars` in `u`.\n\n        @param forall:\n            if `True`,\n            then quantify `qvars` universally,\n            else existentially.\n        \"\"\"\n        level = 0\n        self.manager.reordered = 1\n        while self.manager.reordered == 1:\n            self.manager.reordered = 0\n            cache = dict()\n            r = self._quantify(\n                level, u, qvars, forall, cache)\n        return r\n\n    cpdef Function _quantify_using_cube_root(\n            self,\n            u:\n                Function,\n            qvars:\n                _abc.Iterable[\n                    _VariableName],\n            forall:\n                _Yes=False):\n        \"\"\"Abstract variables `qvars` in `u`.\n\n        This implementation usses a ZDD to represent\n        the set of variables to quantify.\n        \"\"\"\n        level = 0\n        cube = _dict_to_zdd(qvars, self)\n        self.manager.reordered = 1\n        while self.manager.reordered == 1:\n            self.manager.reordered = 0\n            cache = dict()\n            r = self._quantify_using_cube(\n                level, u, cube, forall, cache)\n        return r\n\n    def _quantify_using_cube(\n            self,\n            level:\n                _Level,\n            u:\n                Function,\n            cube:\n                Function,\n            forall:\n                _Yes,\n            cache:\n                dict[\n                    tuple[Function, _Level],\n                    Function]\n            ) -> Function:\n        \"\"\"Recurse to quantify variables.\n\n        This implementation uses a ZDD to represent\n        the set of variables to quantify.\n        \"\"\"\n        if u == self.false or self._gt_var_levels(level):\n            return u\n        t = (u, level)\n        if t in cache:\n            return cache[t]\n        v, w = self._top_cofactor(u, level)\n        if cube.low != cube.high:\n            raise ValueError((cube, cube.low, cube.high))\n        new_cube, _ = self._top_cofactor(cube, level)\n        p = self._quantify_using_cube(level + 1, v, new_cube, forall, cache)\n        q = self._quantify_using_cube(level + 1, w, new_cube, forall, cache)\n        var = self.var_at_level(level)\n        if level > cube.level:\n            raise ValueError((level, cube, cube.level))\n        if level == cube.level:\n            if forall:\n                r = self._conjoin(\n                    level + 1, p, q, dict())\n            else:\n                r = self._disjoin(\n                    level + 1, p, q, dict())\n            r = self.find_or_add(var, r, r)\n        else:\n            r = self.find_or_add(var, p, q)\n        cache[t] = r\n        return r\n\n    def _quantify(\n            self,\n            level:\n                _Level,\n            u:\n                Function,\n            qvars:\n                _abc.Container[\n                    _VariableName],\n            forall:\n                _Yes,\n            cache:\n                dict[\n                    tuple[Function, _Level],\n                    Function]\n            ) -> Function:\n        \"\"\"Recurse to quantify variables.\"\"\"\n        if (level > u.level):\n            raise ValueError(\n                (level, u.level, u))\n        # terminal ?\n        if u == self.false or self._gt_var_levels(level):\n            return u\n        t = (u, level)\n        if t in cache:\n            r = cache[t]\n            if level > r.level:\n                raise AssertionError(\n                    (level, r.level, r, '= cache[(u, level)]'))\n            return r\n        v, w = self._top_cofactor(u, level)\n        if level >= v.level:\n            raise AssertionError((level, v.level, v))\n        if level >= w.level:\n            raise AssertionError((level, w.level, w))\n        p = self._quantify(level + 1, v, qvars, forall, cache)\n        q = self._quantify(level + 1, w, qvars, forall, cache)\n        if level >= p.level:\n            raise AssertionError((level, p.level, p))\n        if level >= q.level:\n            raise AssertionError((level, q.level, q))\n        var = self.var_at_level(level)\n        if var in qvars:\n            if forall:\n                r = self._conjoin(\n                    level + 1, p, q, dict())\n            else:\n                r = self._disjoin(\n                    level + 1, p, q, dict())\n            r = self.find_or_add(var, r, r)\n        else:\n            r = self.find_or_add(var, p, q)\n        if min(level, u.level) > r.level:\n            raise AssertionError(\n                (level, u.level, r.level, 'u, r'))\n        cache[t] = r\n        if level > r.level:\n            raise AssertionError((level, r.level, 'r'))\n        return r\n\n    def _quantify_optimized(\n            self,\n            level:\n                _Level,\n            u:\n                Function,\n            qvars:\n                _abc.Container[\n                    _VariableName],\n            forall:\n                _Yes,\n            cache:\n                dict[Function, Function]\n            ) -> Function:\n        \"\"\"Recurse to quantify variables.\"\"\"\n        # terminal ?\n        if u == self.false or self._gt_var_levels(level):\n            return u\n        if u in cache:\n            return cache[u]\n        u_level = u.level\n        var = self.var_at_level(level)\n        if level < u_level:\n            r = self._quantify(\n                level + 1, u, qvars, forall, cache)\n            if var in qvars:\n                r = self.find_or_add(var, r, r)\n        else:\n            _, v, w = self.succ(u)\n            p = self._quantify(\n                level + 1, v, qvars, forall, cache)\n            q = self._quantify(\n                level + 1, w, qvars, forall, cache)\n            if var in qvars:\n                if forall:\n                    r = self._conjoin(\n                        level + 1, p, q, dict())\n                else:\n                    r = self._disjoin(\n                        level + 1, p, q, dict())\n                r = self.find_or_add(var, r, r)\n            else:\n                r = self.find_or_add(var, p, q)\n        cache[u] = r\n        return r\n\n    cpdef Function forall(\n            self,\n            variables:\n                _abc.Iterable[\n                    _VariableName],\n            u:\n                Function):\n        \"\"\"Quantify `variables` in `u` universally.\n\n        Wraps method `quantify` to be more readable.\n        \"\"\"\n        return self.quantify(u, variables, forall=True)\n\n    cpdef Function exist(\n            self,\n            variables:\n                _abc.Iterable[\n                    _VariableName],\n            u:\n                Function):\n        \"\"\"Quantify `variables` in `u` existentially.\n\n        Wraps method `quantify` to be more readable.\n        \"\"\"\n        return self.quantify(u, variables, forall=False)\n\n    cpdef assert_consistent(\n            self):\n        \"\"\"Raise `AssertionError` if not consistent.\"\"\"\n        if Cudd_DebugCheck(self.manager) != 0:\n            raise AssertionError('`Cudd_DebugCheck` errored')\n        n = len(self.vars)\n        m = len(self._var_with_index)\n        k = len(self._index_of_var)\n        if n != m:\n            raise AssertionError(\n                f'`len(self.vars) == {n}` but '\n                f'`len(self._var_with_index) == {m}`\\n'\n                f'{self.vars = }\\n'\n                f'{self._var_with_index = }')\n        if m != k:\n            raise AssertionError(\n                f'`len(self._var_with_index) == {m}` but '\n                f'`len(self._index_of_var) == {k}`\\n'\n                f'{self._var_with_index = }\\n'\n                f'{self._index_of_var = }')\n        if set(self.vars) != set(self._index_of_var):\n            raise AssertionError(\n                '`set(self.vars) != '\n                'set(self._index_of_var)`\\n'\n                f'{self.vars = }\\n'\n                f'{self._index_of_var = }')\n        if set(self._var_with_index) != set(\n                self._index_of_var.values()):\n            raise AssertionError(\n                '`set(self._var_with_index) != '\n                'set(self._index_of_var.values())`\\n'\n                f'{self._var_with_index = }\\n'\n                f'{self._index_of_var = }')\n\n    def add_expr(\n            self,\n            expr:\n                _Formula\n            ) -> Function:\n        \"\"\"Return node for expression `e`.\"\"\"\n        return _parser.add_expr(expr, self)\n\n    cpdef str to_expr(\n            self,\n            u:\n                Function):\n        \"\"\"Return a Boolean expression for node `u`.\"\"\"\n        if u.manager != self.manager:\n            raise ValueError('`u.manager != self.manager`')\n        cache = dict()\n        level = 0\n        return self._to_expr(level, u, cache)\n\n    cpdef str _to_expr(\n            self,\n            level:\n                _Level,\n            u:\n                Function,\n            cache:\n                dict[Function, _Formula]):\n        \"\"\"Recursively compute an expression.\"\"\"\n        if u == self.false:\n            return 'FALSE'\n        if self._gt_var_levels(level):\n            return 'TRUE'\n        if u in cache:\n            return cache[u]\n        v, w = self._top_cofactor(u, level)\n        p = self._to_expr(level + 1, v, cache)\n        q = self._to_expr(level + 1, w, cache)\n        var = self.var_at_level(level)\n        if p == 'FALSE' and q == 'TRUE':\n            s = var\n        elif p == q:\n            s = p\n        else:\n            s = f'ite({var}, {q}, {p})'\n        cache[u] = s\n        return s\n\n    cpdef dump(\n            self,\n            filename:\n                str,\n            roots:\n                list[Function],\n            filetype:\n                _dd_abc.ImageFileType |\n                None=None):\n        \"\"\"Write ZDD as a diagram to file `filename`.\n\n        The file type is inferred from the\n        extension (case insensitive),\n        unless a `filetype` is explicitly given.\n\n        `filetype` can have the values:\n\n        - `'pdf'` for PDF\n        - `'png'` for PNG\n        - `'svg'` for SVG\n\n        If `filetype is None`, then `filename`\n        must have an extension that matches\n        one of the file types listed above.\n\n        Only the ZDD manager nodes that are reachable from the\n        ZDD references in `roots` are included in the diagram.\n\n        @param filename:\n            file name,\n            e.g., `\"diagram.pdf\"`\n        @param roots:\n            container of nodes\n        \"\"\"\n        if filetype is None:\n            name = filename.lower()\n            if name.endswith('.pdf'):\n                filetype = 'pdf'\n            elif name.endswith('.png'):\n                filetype = 'png'\n            elif name.endswith('.svg'):\n                filetype = 'svg'\n            elif name.endswith('.dot'):\n                filetype = 'dot'\n            else:\n                raise ValueError(\n                    'cannot infer file type '\n                    'from extension of file '\n                    f'name \"{filename}\"')\n        if filetype in _utils.DOT_FILE_TYPES:\n            self._dump_figure(\n                roots, filename, filetype)\n        else:\n            raise ValueError(\n                f'unknown file type \"{filetype}\", '\n                'the method `dd.cudd_zdd.ZDD.dump` '\n                'supports writing diagrams as '\n                'PDF, PNG, or SVG files.')\n\n    def _dump_figure(\n            self,\n            roots:\n                _abc.Collection[Function],\n            filename:\n                str,\n            filetype:\n                _dd_abc.ImageFileType,\n            **kw\n            ) -> None:\n        \"\"\"Write BDDs to `filename` as figure.\"\"\"\n        g = _to_dot(roots)\n        g.dump(\n            filename,\n            filetype=filetype,\n            **kw)\n\n    cpdef load(\n            self,\n            filename:\n                str):\n        raise NotImplementedError()\n\n    # Same with the method `dd.cudd.BDD._cube_to_dict`.\n    cpdef dict _cube_to_dict(\n            self,\n            f:\n                Function):\n        \"\"\"Recurse to collect indices of support variables.\"\"\"\n        if f.manager != self.manager:\n            raise ValueError('`f.manager != self.manager`')\n        n = self._number_of_cudd_vars()\n        cdef int *x\n        x = <int *> PyMem_Malloc(n * sizeof(DdRef))\n        try:\n            Cudd_BddToCubeArray(self.manager, f.node, x)\n            d = _cube_array_to_dict(x, self._index_of_var)\n        finally:\n            PyMem_Free(x)\n        return d\n\n    @property\n    def false(\n            self\n            ) -> Function:\n        \"\"\"`Function` for Boolean value FALSE.\n\n        Relevant properties:\n            `ZDD.true` and `ZDD.true_node`.\n        \"\"\"\n        return self._bool(False)\n\n    @property\n    def true(\n            self\n            ) -> Function:\n        \"\"\"`Function` for Boolean value TRUE.\n\n        Read also the docstring of the\n        property `ZDD.true_node`.\n\n        Relevant properties:\n            `ZDD.false`, `ZDD.true_node`\n        \"\"\"\n        return self._bool(True)\n\n    @property\n    def true_node(\n            self\n            ) -> Function:\n        \"\"\"Return the constant ZDD node for TRUE.\n\n        Compare with the property `true`,\n        for example the value of `ZDD.true.level`\n        with the value of `ZDD.true_node.level`,\n        after at least one variable has been\n        declared:\n\n        ```python\n        import dd.cudd_zdd as _zdd\n\n        zdd = _zdd.ZDD()\n        zdd.declare('x')\n        a = zdd.true.level\n        b = zdd.true_node.level\n        print(f'level of `zdd.true`:  {a}')\n        print(f'level of `zdd.true_node`:  {b}')\n        ```\n\n        Relevant properties:\n            `ZDD.false`, `ZDD.true`\n        \"\"\"\n        r: DdRef\n        r = DD_ONE(self.manager)\n        return wrap(self, r)\n\n    cdef Function _bool(\n            self,\n            v:\n                python_bool):\n        \"\"\"Return terminal node for Boolean `v`.\"\"\"\n        r: DdRef\n        if v:\n            r = Cudd_ReadZddOne(self.manager, 0)\n        else:\n            r = Cudd_ReadZero(self.manager)\n        return wrap(self, r)\n\n\ncdef Function wrap(\n        bdd:\n            ZDD,\n        node:\n            DdRef):\n    \"\"\"Return a `Function` that wraps `node`.\"\"\"\n    # because `@classmethod` unsupported\n    f = Function()\n    f.init(node, bdd)\n    return f\n\n\ncdef class Function:\n    \"\"\"Wrapper of ZDD `DdNode` from CUDD.\n\n    For details, read the docstring of the\n    class `dd.cudd.Function`.\n    \"\"\"\n\n    __weakref__: object\n    cdef public ZDD bdd\n    cdef public ZDD zdd\n    cdef DdManager *manager\n    node: DdRef\n    cdef public int _ref\n\n    cdef init(\n            self,\n            node:\n                DdRef,\n            zdd:\n                ZDD):\n        if node is NULL:\n            raise ValueError('`DdNode *node` is `NULL` pointer.')\n        self.zdd = zdd\n        self.bdd = zdd  # keep this attribute for writing\n            # common algorithms for BDDs and ZDDs where possible\n        self.manager = zdd.manager\n        self.node = node\n        self._ref = 1  # lower bound on reference count\n        Cudd_Ref(node)\n\n    def __hash__(\n            self\n            ) -> int:\n        return int(self)\n\n    @property\n    def _index(\n            self\n            ) -> int:\n        \"\"\"Index of `self.node`.\"\"\"\n        return Cudd_NodeReadIndex(self.node)\n\n    @property\n    def var(\n            self\n            ) -> (\n                _VariableName |\n                None):\n        \"\"\"Variable at level where this node is.\n\n        If node is constant, return `None`.\n        \"\"\"\n        if cuddIsConstant(self.node):\n            return None\n        return self.zdd._var_with_index[self._index]\n\n    @property\n    def level(\n            self\n            ) -> _Level:\n        \"\"\"Level where this node currently is.\"\"\"\n        i = self._index\n        return Cudd_ReadPermZdd(self.manager, i)\n\n    @property\n    def ref(\n            self\n            ) -> _Cardinality:\n        \"\"\"Reference count of node.\"\"\"\n        return self.node.ref\n\n    @property\n    def low(\n            self\n            ) -> (\n                Function |\n                None):\n        \"\"\"Return \"else\" node.\"\"\"\n        if cuddIsConstant(self.node):\n            return None\n        u: DdRef\n        # refer to the lines:\n        #\n        # else if (top_var == level) {\n        #     res = cuddE(P);\n        #\n        # inside the function `zdd_subset0_aux`,\n        # in the file `cudd/cuddZddSetop.c`.\n        u = cuddE(self.node)\n        return wrap(self.zdd, u)\n\n    @property\n    def high(\n            self\n            ) -> (\n                Function |\n                None):\n        \"\"\"Return \"then\" node.\"\"\"\n        if cuddIsConstant(self.node):\n            return None\n        u: DdRef\n        # refer to the lines:\n        #\n        # } else if (top_var == level) {\n        # res = cuddT(P);\n        #\n        # inside the function `zdd_subset1_aux`,\n        # in file `cudd/cuddZddSetop.c`.\n        u = cuddT(self.node)\n        return wrap(self.zdd, u)\n\n    @property\n    def negated(\n            self\n            ) -> _Yes:\n        raise Exception(\n            'No complemented edges for ZDDs in CUDD, '\n            'only for BDDs.')\n\n    @property\n    def support(\n            self:\n                ZDD\n            ) -> set[_VariableName]:\n        \"\"\"Return `set` of variables in support.\"\"\"\n        return self.zdd.support(self)\n\n    def __dealloc__(\n            self\n            ) -> None:\n        # when changing this method,\n        # update also the function\n        # `_test_call_dealloc` below\n        if self._ref < 0:\n            raise AssertionError(\n                \"The lower bound `_ref` on the node's \"\n                f'reference count has value {self._ref}, '\n                'which is unexpected and should never happen. '\n                'Was the value of `_ref` changed from outside '\n                'this instance?')\n        assert self._ref >= 0, self._ref\n        if self._ref == 0:\n            return\n        if self.node is NULL:\n            raise AssertionError(\n                'The attribute `node` is a `NULL` pointer. '\n                'This is unexpected and should never happen. '\n                'Was the value of `_ref` changed from outside '\n                'this instance?')\n        # anticipate multiple calls to `__dealloc__`\n        self._ref -= 1\n        # deref\n        Cudd_RecursiveDerefZdd(self.manager, self.node)\n        # avoid future access to deallocated memory\n        self.node = NULL\n\n    def __int__(\n            self\n            ) -> int:\n        # inverse is `ZDD._add_int`\n        if sizeof(stdint.uintptr_t) != sizeof(DdRef):\n            raise RuntimeError(\n                'expected equal pointer sizes')\n        i = <stdint.uintptr_t>self.node\n        # 0, 1 are true and false in logic syntax\n        if 0 <= i:\n            i += 2\n        if i in (0, 1):\n            raise AssertionError(i)\n        return i\n\n    def __repr__(\n            self\n            ) -> str:\n        return (\n            f'<dd.cudd_zdd.Function at {hex(id(self))}, '\n            'wrapping a (ZDD) DdNode with '\n            f'var index: {self._index}, '\n            f'ref count: {self.ref}, '\n            f'int repr: {int(self)}>')\n\n    def __str__(\n            self\n            ) -> str:\n        return f'@{int(self)}'\n\n    def __len__(\n            self\n            ) -> _Cardinality:\n        # The function `Cudd_zddDagSize`\n        # is deprecated because it duplicates\n        # the function `Cudd_DagSize`.\n        return Cudd_zddDagSize(self.node)\n\n    @property\n    def dag_size(\n            self\n            ) -> _Cardinality:\n        \"\"\"Return number of ZDD nodes.\n\n        This is the number of ZDD nodes that\n        are reachable from this ZDD reference,\n        i.e., with `self` as root.\n        \"\"\"\n        return len(self)\n\n    def __eq__(\n            self:\n                Function,\n            other:\n                _ty.Optional[Function]\n            ) -> _Yes:\n        if other is None:\n            return False\n        # guard against mixing managers\n        if self.manager != other.manager:\n            raise ValueError(\n                '`self.manager != other.manager`')\n        return self.node == other.node\n\n    def __ne__(\n            self:\n                Function,\n            other:\n                _ty.Optional[Function]\n            ) -> _Yes:\n        if other is None:\n            return True\n        if self.manager != other.manager:\n            raise ValueError(\n                '`self.manager != other.manager`')\n        return self.node != other.node\n\n    def __le__(\n            self:\n                Function,\n            other:\n                Function\n            ) -> _Yes:\n        if self.manager != other.manager:\n            raise ValueError(\n                '`self.manager != other.manager`')\n        return (other | ~ self) == self.zdd.true\n\n    def __lt__(\n            self:\n                Function,\n            other:\n                Function\n            ) -> _Yes:\n        if self.manager != other.manager:\n            raise ValueError(\n                '`self.manager != other.manager`')\n        return (\n            self.node != other.node and\n            (other | ~ self) == self.zdd.true)\n\n    def __ge__(\n            self:\n                Function,\n            other:\n                Function\n            ) -> _Yes:\n        if self.manager != other.manager:\n            raise ValueError(\n                '`self.manager != other.manager`')\n        return (self | ~ other) == self.zdd.true\n\n    def __gt__(\n            self:\n                Function,\n            other:\n                Function\n            ) -> _Yes:\n        if self.manager != other.manager:\n            raise ValueError(\n                '`self.manager != other.manager`')\n        return (\n            self.node != other.node and\n            (self | ~ other) == self.zdd.true)\n\n    def __invert__(\n            self\n            ) -> Function:\n        r: DdRef\n        r = Cudd_zddDiff(\n            self.manager,\n            Cudd_ReadZddOne(self.manager, 0),\n            self.node)\n        return wrap(self.zdd, r)\n\n    def __and__(\n            self:\n                Function,\n            other:\n                Function\n            ) -> Function:\n        if self.manager != other.manager:\n            raise ValueError('`self.manager != other.manager`')\n        r = Cudd_zddIntersect(self.manager, self.node, other.node)\n        return wrap(self.zdd, r)\n\n    def __or__(\n            self:\n                Function,\n            other:\n                Function\n            ) -> Function:\n        if self.manager != other.manager:\n            raise ValueError('`self.manager != other.manager`')\n        r = Cudd_zddUnion(self.manager, self.node, other.node)\n        return wrap(self.zdd, r)\n\n    def implies(\n            self:\n                Function,\n            other:\n                Function\n            ) -> Function:\n        if self.manager != other.manager:\n            raise ValueError('`self.manager != other.manager`')\n        r = Cudd_zddIte(\n            self.manager, self.node,\n            other.node, Cudd_ReadZddOne(self.manager, 0))\n        return wrap(self.zdd, r)\n\n    def equiv(\n            self:\n                Function,\n            other:\n                Function\n            ) -> Function:\n        return self.zdd.apply('<=>', self, other)\n\n    def let(\n            self:\n                Function,\n            **definitions:\n                _VariableName |\n                python_bool |\n                Function\n            ) -> Function:\n        return self.zdd.let(definitions, self)\n\n    def exist(\n            self:\n                Function,\n            *variables:\n                _VariableName\n            ) -> Function:\n        return self.zdd.exist(variables, self)\n\n    def forall(\n            self:\n                Function,\n            *variables:\n                _VariableName\n            ) -> Function:\n        return self.zdd.forall(variables, self)\n\n    def pick(\n            self:\n                Function,\n            care_vars:\n                _abc.Set[\n                    _VariableName] |\n                None=None\n            ) -> (\n                _Assignment |\n                None):\n        return self.zdd.pick(self, care_vars)\n\n    def count(\n            self:\n                Function,\n            nvars:\n                _Cardinality |\n                None=None\n            ) -> _Cardinality:\n        return self.zdd.count(self, nvars)\n\n\n# Similar to the function `dd.cudd._cube_array_to_dict`\ncdef dict _path_array_to_dict(\n        int *x,\n        index_of_var:\n            dict):\n    \"\"\"Return assignment from array of literals `x`.\"\"\"\n    d = dict()\n    for var, j in index_of_var.items():\n        b = x[j]\n        if b == 2:  # absence of ZDD node\n            d[var] = False\n        elif b == 1:  # \"then\" arc\n            d[var] = True\n        elif b == 0:  # \"else\" arc\n            d[var] = False\n        else:\n            raise ValueError(\n                f'unknown polarity: {b}, '\n                f'for variable \"{var}\"')\n    return d\n\n\n# Copy of the function `dd.cudd._cube_array_to_dict`\n# TODO: place in a header file, if used in this module\ncdef dict _cube_array_to_dict(\n        int *x,\n        index_of_var:\n            dict):\n    \"\"\"Return assignment from array of literals `x`.\n\n    @param x:\n        read `dd.cudd._dict_to_cube_array`\n    \"\"\"\n    d = dict()\n    for var, j in index_of_var.items():\n        b = x[j]\n        if b == 2:\n            continue\n        elif b == 1:\n            d[var] = True\n        elif b == 0:\n            d[var] = False\n        else:\n            raise ValueError(\n                f'unknown polarity: {b}, '\n                f'for variable \"{var}\"')\n    return d\n\n\ndef to_nx(\n        u:\n            Function\n        ) -> '_utils.MultiDiGraph':\n    \"\"\"Return graph for the ZDD rooted at `u`.\"\"\"\n    _nx = _utils.import_module('networkx')\n    g = _nx.MultiDiGraph()\n    _to_nx(g, u, umap=dict())\n    return g\n\n\ndef _to_nx(\n        g:\n            '_utils.MultiDiGraph',\n        u:\n            Function,\n        umap:\n            dict\n        ) -> None:\n    \"\"\"Recursively construct a ZDD graph.\"\"\"\n    u_int = int(u)\n    # visited ?\n    if u_int in umap:\n        return\n    u_nd = umap.setdefault(u_int, len(umap))\n    if u.var is None:\n        label = 'FALSE' if u == u.bdd.false else 'TRUE'\n    else:\n        label = u.var\n    g.add_node(u_nd, label=label)\n    if u.var is None:\n        return\n    v, w = u.low, u.high\n    if v is None:\n        raise AssertionError(v)\n    if w is None:\n        raise AssertionError(w)\n    v_int = int(v)\n    w_int = int(w)\n    _to_nx(g, v, umap)\n    _to_nx(g, w, umap)\n    v_nd = umap[v_int]\n    w_nd = umap[w_int]\n    g.add_edge(u_nd, v_nd, taillabel='0', style='dashed')\n    g.add_edge(u_nd, w_nd, taillabel='1', style='solid')\n\n\ndef _to_dot(\n        roots:\n            _abc.Collection[Function]\n        ) -> _utils.DotGraph:\n    \"\"\"Return graph for the ZDD rooted at `u`.\"\"\"\n    if not roots:\n        raise ValueError(\n            f'No `roots` given:  {roots}')\n    assert roots, roots\n    g = _utils.DotGraph(\n        graph_type='digraph')\n    # construct graphs\n    subgraphs = _add_nodes_for_zdd_levels(g, roots)\n    # mapping CUDD ZDD node ID -> node name in DOT graph\n    umap = dict()\n    for u in roots:\n        _to_dot_recurse(\n            g, u, umap, subgraphs)\n    _add_nodes_for_external_references(\n        roots, umap, g, subgraphs[-1])\n    return g\n\n\ndef _add_nodes_for_zdd_levels(\n        g,\n        roots:\n            _abc.Collection[Function]\n        ) -> dict[\n            int,\n            _utils.DotGraph]:\n    \"\"\"Create nodes and subgraphs for ZDD levels.\n\n    For each level of any ZDD node reachable from `roots`,\n    a new node `u_level` and a new subgraph `h_level` are created.\n    The node `u_level` is labeled with the level (as numeral),\n    and added to the subgraph `h_level`.\n\n    For each pair of consecutive levels in\n    `sorted(set of levels of nodes reachable from roots)`,\n    an edge is added to graph `g`, pointing from\n    the node labeled with the smaller level,\n    to the node labeled with the larger level.\n\n    Level `-1` is considered to represent external references\n    to ZDD nodes, i.e., instances of the class `Function`.\n\n    The collection of subgraphs (`h_level` above) is returned.\n\n    @return:\n        mapping from each ZDD level to a subgraph,\n        for the ZDD levels of nodes reachable from `roots`.\n    \"\"\"\n    # mapping level -> var\n    level_to_var = _collect_var_levels(roots)\n    # add layer for external ZDD references\n    level_to_var[-1] = None\n    subgraphs = dict()\n    level_node_names = list()\n    for level in sorted(level_to_var):\n        h = _utils.DotGraph(\n            rank='same')\n        g.subgraphs.append(h)\n        subgraphs[level] = h\n        # add phantom node\n        u = f'\"L{level}\"'\n        level_node_names.append(u)\n        if level == -1:\n            # layer for external ZDD references\n            label = 'ref'\n        else:\n            # ZDD level\n            label = str(level)\n        h.add_node(\n            u,\n            label=label,\n            shape='none')\n    # auxiliary edges for ranking of levels\n    a, a1 = _itr.tee(level_node_names)\n    next(a1, None)\n    for u, v in zip(a, a1):\n        g.add_edge(\n            u, v,\n            style='invis')\n    return subgraphs\n\n\ndef _to_dot_recurse(\n        g:\n            _utils.DotGraph,\n        u:\n            Function,\n        umap:\n            dict[int, int],\n        subgraphs:\n            dict[\n                _Level,\n                _utils.DotGraph]\n        ) -> None:\n    \"\"\"Recursively construct a ZDD graph.\"\"\"\n    u_int = int(u)\n    # visited ?\n    if u_int in umap:\n        return\n    u_nd = umap.setdefault(u_int, len(umap))\n    if u.var is None:\n        label = 'FALSE' if u == u.bdd.false else 'TRUE'\n    else:\n        label = u.var\n    h = subgraphs[u.level]\n    h.add_node(\n        u_nd,\n        label=label)\n    if u.var is None:\n        return\n    v, w = u.low, u.high\n    if v is None:\n        raise AssertionError(v)\n    if w is None:\n        raise AssertionError(w)\n    v_int = int(v)\n    w_int = int(w)\n    _to_dot_recurse(g, v, umap, subgraphs)\n    _to_dot_recurse(g, w, umap, subgraphs)\n    v_nd = umap[v_int]\n    w_nd = umap[w_int]\n    g.add_edge(\n        u_nd, v_nd,\n        taillabel='0',\n        style='dashed')\n    g.add_edge(\n        u_nd, w_nd,\n        taillabel='1',\n        style='solid')\n\n\ndef _add_nodes_for_external_references(\n        roots:\n            list[Function],\n        umap:\n            dict[int, int],\n        g:\n            _utils.DotGraph,\n        h:\n            _utils.DotGraph\n        ) -> None:\n    \"\"\"Add nodes to `g` that represent the references in `roots`.\n\n    @param roots:\n        external references to ZDD nodes\n    @param g:\n        ZDD graph\n    @param h:\n        subgraph of `g`\n    \"\"\"\n    for u in roots:\n        if u is None:\n            raise ValueError(u)\n        u_int = int(u)\n        u_nd = umap[u_int]\n        # add node to subgraph at level -1\n        ref_nd = f'ref{int(u)}'\n        label = f'@{int(u)}'\n        h.add_node(\n            ref_nd,\n            label=label)\n        # add edge from external reference to ZDD node\n        g.add_edge(\n            ref_nd, u_nd,\n            style='dashed')\n\n\ndef _collect_var_levels(\n        roots:\n            list[Function]\n        ) -> dict[\n            _Level,\n            _VariableName]:\n    \"\"\"Add variables and levels reachable from `roots`.\n\n    @param roots:\n        container of ZDD nodes\n    @return:\n        `dict` that maps\n        each level (as `int`) to a variable (as `str`),\n        only for levels of nodes that are\n        reachable from the ZDD node `u`\n    \"\"\"\n    level_to_var = dict()\n    visited = set()\n    for u in roots:\n        _collect_var_levels_recurse(\n            u, level_to_var, visited)\n    return level_to_var\n\n\ndef _collect_var_levels_recurse(\n        u:\n            Function,\n        level_to_var:\n            dict[\n                _Level,\n                _VariableName],\n        visited:\n            set[int]\n        ) -> None:\n    \"\"\"Recursively collect variables and levels.\n\n    @param level_to_var:\n        maps each level to a variable,\n        only for levels of nodes that are\n        reachable from the ZDD node `u`\n    @param visited:\n        already visited ZDD nodes\n    \"\"\"\n    u_int = int(u)\n    if u_int in visited:\n        return\n    visited.add(u_int)\n    level_to_var[u.level] = u.var\n    if u.var is None:\n        return\n    v, w = u.low, u.high\n    if v is None:\n        raise AssertionError(v)\n    if w is None:\n        raise AssertionError(w)\n    _collect_var_levels_recurse(v, level_to_var, visited)\n    _collect_var_levels_recurse(w, level_to_var, visited)\n\n\ncpdef Function _dict_to_zdd(\n        qvars:\n            _abc.Iterable[\n                _VariableName],\n        zdd:\n            ZDD):\n    \"\"\"Return a ZDD that is TRUE over `qvars`.\n\n    This ZDD has nodes at levels of variables in\n    `qvars`. Each such node has same low and high.\n    \"\"\"\n    levels = {zdd.level_of_var(var) for var in qvars}\n    r = zdd.true_node\n    for level in sorted(levels, reverse=True):\n        var = zdd.var_at_level(level)\n        r = zdd.find_or_add(var, r, r)\n    return r\n\n\ncpdef set _cube_to_universe_root(\n        cube:\n            Function,\n        zdd:\n            ZDD):\n    \"\"\"Map the conjunction `cube` to its support.\"\"\"\n    qvars = set()\n    _cube_to_universe(cube, qvars, zdd)\n    qvars_ = zdd.support(cube)\n    if qvars != qvars_:\n        raise AssertionError((qvars, qvars_))\n    return qvars\n\n\ncpdef _cube_to_universe(\n        cube:\n            Function,\n        qvars:\n            set[_VariableName],\n        zdd:\n            ZDD):\n    \"\"\"Recursively map `cube` to its support.\"\"\"\n    if cube == zdd.false:\n        return\n    if cube == zdd.true_node:\n        return\n    if cube.low != cube.high:\n        var = cube.var\n        qvars.add(var)\n        if cube.low != zdd.false:\n            raise ValueError((cube, cube.low, len(cube.low)))\n    _cube_to_universe(cube.high, qvars, zdd)\n\n\ncpdef Function _ith_var(\n        var:\n            _VariableName,\n        zdd:\n            ZDD):\n    \"\"\"Return ZDD of variable `var`.\n\n    This function requires that declared variables\n    have a contiguous range of levels.\n    \"\"\"\n    n_declared_vars = len(zdd.vars)\n    n_cudd_vars = zdd._number_of_cudd_vars()\n    if n_declared_vars > n_cudd_vars:\n        # note that `zdd.var_levels()` cannot\n        # be called here, because not all declared\n        # variables have levels in CUDD, given\n        # that `n_declared_vars > n_cudd_vars`\n        raise AssertionError(_tw.dedent(f'''\n            Found unexpected number of declared variables\n            ({n_declared_vars}),\n            compared to number of CUDD variable indices\n            ({n_cudd_vars}).\n            Expected number of declared variables\n            <= number of CUDD variable indices.\n            The declared variables and their indices are:\n                {zdd._index_of_var}\n            '''))\n    if n_declared_vars != n_cudd_vars:\n        counts = _utils.var_counts(zdd)\n        contiguous = _utils.contiguous_levels(\n            '_ith_var', zdd)\n        raise AssertionError(f'{counts}\\n{contiguous}')\n    level = zdd.level_of_var(var)\n    r = zdd.true_node\n    for j in range(len(zdd.vars) - 1, -1, -1):\n        v = zdd.var_at_level(j)\n        if j == level:\n            r = zdd.find_or_add(v, zdd.false, r)\n        else:\n            r = zdd.find_or_add(v, r, r)\n    return r\n\n\n# changes to the function `_c_exist`\n# are copied here\ncpdef Function _c_forall(\n        variables:\n            _abc.Iterable[\n                _VariableName],\n        u:\n            Function):\n    \"\"\"Universally quantify `variables` in `u`.\"\"\"\n    r: DdRef\n    cube = _dict_to_zdd(variables, u.bdd)\n    r = _forall_root(u.manager, u.node, cube.node)\n    return wrap(u.bdd, r)\n\n\n# changes to the function `_exist_root`\n# are copied here\ncdef DdRef _forall_root(\n        DdManager *mgr,\n        u:\n            DdRef,\n        cube:\n            DdRef\n        ) except NULL:\n    r\"\"\"Root of recursion for \\A.\"\"\"\n    if mgr is NULL:\n        raise AssertionError('`mgr is NULL`')\n    if u is NULL:\n        raise AssertionError('`u is NULL`')\n    if cube is NULL:\n        raise AssertionError('`cube is NULL`')\n    mgr.reordered = 1\n    while mgr.reordered == 1:\n        mgr.reordered = 0\n        r = _forall(mgr, 0, u, cube)\n    if r is NULL:\n        raise AssertionError('`r is NULL`')\n    return r\n\n\ncdef DdRef _forall_cache_id(\n        DdManager *mgr,\n        u:\n            DdRef,\n        cube:\n            DdRef\n        ) noexcept:\n    \"\"\"Used only as cache key.\n\n    Passed inside the function `_forall()`\n    to the C functions:\n\n    - `cuddCacheLookup2Zdd()`\n    - `cuddCacheInsert2()`\n    \"\"\"\n\n\n# changes to the function `_exist`\n# are copied here\ncdef DdRef _forall(\n        DdManager *mgr,\n        level:\n            _c_level,\n        u:\n            DdRef,\n        cube:\n            DdRef\n        ) except? NULL:\n    r\"\"\"Recursive \\A.\n\n    Asserts that:\n    - `level` <= level of `u`\n    - `level` <= level of `cube`\n    \"\"\"\n    if level < 0:\n        raise AssertionError(\n            f'`{level = } < 0`')\n    if u is NULL:\n        raise AssertionError('`u is NULL`')\n    if cube is NULL:\n        raise AssertionError('`cube is NULL`')\n    index = Cudd_ReadInvPermZdd(mgr, level)\n    if (u == DD_ZERO(mgr) or\n            index == -1 or\n            index == CUDD_CONST_INDEX):\n        return u\n    r = cuddCacheLookup2Zdd(\n        mgr, _forall_cache_id, u, cube)\n    if r is not NULL:\n        return r\n    u_index = Cudd_NodeReadIndex(u)\n    u_level = Cudd_ReadPermZdd(mgr, u_index)\n    cube_index = Cudd_NodeReadIndex(cube)\n    cube_level = Cudd_ReadPermZdd(mgr, cube_index)\n    if level > u_level:\n        raise AssertionError((level, u_level))\n    if level > cube_level:\n        raise AssertionError((level, cube_level))\n    # top cofactor\n    if level < u_level:\n        v, w = u, DD_ZERO(mgr)\n    else:\n        v, w = cuddE(u), cuddT(u)\n    if level < cube_level:\n        new_cube = cube\n    else:\n        if cuddE(cube) != cuddT(cube):\n            raise AssertionError(<int>cube)\n        new_cube = cuddE(cube)\n    p = _forall(mgr, level + 1, v, new_cube)\n    if p is NULL:\n        return NULL\n    cuddRef(p)\n    q = _forall(mgr, level + 1, w, new_cube)\n    if q is NULL:\n        Cudd_RecursiveDerefZdd(mgr, p)\n        return NULL\n    cuddRef(q)\n    if level == cube_level:\n        conj = _conjoin(mgr, level + 1, p, q)\n        if conj is NULL:\n            Cudd_RecursiveDerefZdd(mgr, p)\n            Cudd_RecursiveDerefZdd(mgr, q)\n            return NULL\n        cuddRef(conj)\n        r = _find_or_add(mgr, index, conj, conj)\n        Cudd_RecursiveDerefZdd(mgr, conj)\n    else:\n        r = _find_or_add(mgr, index, p, q)\n    if r is NULL:\n        Cudd_RecursiveDerefZdd(mgr, p)\n        Cudd_RecursiveDerefZdd(mgr, q)\n        return NULL\n    cuddRef(r)\n    Cudd_RecursiveDerefZdd(mgr, p)\n    Cudd_RecursiveDerefZdd(mgr, q)\n    cuddCacheInsert2(\n        mgr, _forall_cache_id, u, cube, r)\n    cuddDeref(r)\n    return r\n\n\ncpdef Function _c_exist(\n        variables:\n            _abc.Iterable[\n                _VariableName],\n        u:\n            Function):\n    \"\"\"Existentially quantify `variables` in `u`.\"\"\"\n    r: DdRef\n    cube: Function\n    cube = _dict_to_zdd(variables, u.bdd)\n    r = _exist_root(u.manager, u.node, cube.node)\n    return wrap(u.bdd, r)\n\n\ncdef DdRef _exist_root(\n        DdManager *mgr,\n        u:\n            DdRef,\n        cube:\n            DdRef\n        ) except NULL:\n    r\"\"\"Root of recursion for \\E.\"\"\"\n    if mgr is NULL:\n        raise AssertionError('`mgr is NULL`')\n    if u is NULL:\n        raise AssertionError('`u is NULL`')\n    if cube is NULL:\n        raise AssertionError('`cube is NULL`')\n    mgr.reordered = 1\n    while mgr.reordered == 1:\n        mgr.reordered = 0\n        r = _exist(mgr, 0, u, cube)\n    if r is NULL:\n        raise AssertionError('`r is NULL`')\n    return r\n\n\ncdef DdRef _exist_cache_id(\n        DdManager *mgr,\n        u:\n            DdRef,\n        cube:\n            DdRef\n        ) noexcept:\n    \"\"\"Used only as cache key.\n\n    Passed inside the function `_exist()`\n    to the C functions:\n\n    - `cuddCacheLookup2Zdd()`\n    -  `cuddCacheInsert2()`\n    \"\"\"\n    raise NotImplementedError(\n        'This function is used only '\n        'as cache key.')\n\n\ncdef DdRef _exist(\n        DdManager *mgr,\n        level:\n            _c_level,\n        u:\n            DdRef,\n        cube:\n            DdRef\n        ) except? NULL:\n    r\"\"\"Recursive \\E.\n\n    Asserts that:\n    - `level` <= level of `u`\n    - `level` <= level of `cube`\n    \"\"\"\n    if level < 0:\n        raise AssertionError(\n            f'`{level = } < 0`')\n    if u is NULL:\n        raise AssertionError('`u is NULL`')\n    if cube is NULL:\n        raise AssertionError('`cube is NULL`')\n    index = Cudd_ReadInvPermZdd(mgr, level)\n    if (u == DD_ZERO(mgr) or\n            index == -1 or\n            index == CUDD_CONST_INDEX):\n        return u\n    r = cuddCacheLookup2Zdd(\n        mgr, _exist_cache_id, u, cube)\n    if r is not NULL:\n        return r\n    u_index = Cudd_NodeReadIndex(u)\n    u_level = Cudd_ReadPermZdd(mgr, u_index)\n    cube_index = Cudd_NodeReadIndex(cube)\n    cube_level = Cudd_ReadPermZdd(mgr, cube_index)\n    if level > u_level:\n        raise AssertionError((level, u_level))\n    if level > cube_level:\n        raise AssertionError((level, cube_level))\n    # top cofactor\n    if level < u_level:\n        v, w = u, DD_ZERO(mgr)\n    else:\n        v, w = cuddE(u), cuddT(u)\n    if level < cube_level:\n        new_cube = cube\n    else:\n        if cuddE(cube) != cuddT(cube):\n            raise AssertionError('expected:  E == T')\n        new_cube = cuddE(cube)\n    p = _exist(mgr, level + 1, v, new_cube)\n    if p is NULL:\n        return NULL\n    cuddRef(p)\n    q = _exist(mgr, level + 1, w, new_cube)\n    if q is NULL:\n        Cudd_RecursiveDerefZdd(mgr, p)\n        return NULL\n    cuddRef(q)\n    if level > cube_level:\n        raise AssertionError((level, cube_level))\n    if level == cube_level:\n        disj = _disjoin(mgr, level + 1, p, q)\n        if disj is NULL:\n            Cudd_RecursiveDerefZdd(mgr, p)\n            Cudd_RecursiveDerefZdd(mgr, q)\n            return NULL\n        cuddRef(disj)\n        r = _find_or_add(mgr, index, disj, disj)\n        Cudd_RecursiveDerefZdd(mgr, disj)\n    else:\n        r = _find_or_add(mgr, index, p, q)\n    if r is NULL:\n        Cudd_RecursiveDerefZdd(mgr, p)\n        Cudd_RecursiveDerefZdd(mgr, q)\n        return NULL\n    cuddRef(r)\n    Cudd_RecursiveDerefZdd(mgr, p)\n    Cudd_RecursiveDerefZdd(mgr, q)\n    cuddCacheInsert2(\n        mgr, _exist_cache_id, u, cube, r)\n    cuddDeref(r)\n    return r\n\n\ncdef DdRef _find_or_add(\n        DdManager *mgr,\n        index:\n            _c_int,\n        v:\n            DdRef,\n        w:\n            DdRef\n        ) except? NULL:\n    \"\"\"Find node in table or add new node.\n\n    Calls `cuddUniqueInterZdd` and\n    ensures canonicity of ZDDs.\n\n    Returns `NULL` when:\n    - memory is exhausted\n    - reordering occurred\n    - a termination request was detected\n    - a timeout expired\n\n    These cases are based on the docstring\n    of the CUDD function `cuddUniqueInterZdd()`.\n    \"\"\"\n    if mgr is NULL:\n        raise AssertionError('`mgr is NULL`')\n    if index < 0 or index > CUDD_CONST_INDEX:\n        raise AssertionError(\n            f'`{index = }`')\n    if v is NULL:\n        raise AssertionError('`v is NULL`')\n    if w is NULL:\n        raise AssertionError('`w is NULL`')\n    if w == DD_ZERO(mgr):\n        return v\n    return cuddUniqueInterZdd(mgr, index, w, v)\n\n\ncpdef Function _c_disjoin(\n        u:\n            Function,\n        v:\n            Function):\n    \"\"\"Return the disjunction of `u` and `v`.\n\n    @param u, v:\n        ZDD node\n    @rtype:\n        `Function`\n    \"\"\"\n    r: DdRef\n    cdef DdManager *mgr\n    mgr = u.manager\n    r = _disjoin_root(\n        mgr, u.node, v.node)\n    return wrap(u.bdd, r)\n\n\ncdef DdRef _disjoin_root(\n        DdManager *mgr,\n        u:\n            DdRef,\n        v:\n            DdRef\n        ) except NULL:\n    \"\"\"Return the disjunction of `u` and `v`.\"\"\"\n    if mgr is NULL:\n        raise AssertionError('`mgr is NULL`')\n    if u is NULL:\n        raise AssertionError('`u is NULL`')\n    if v is NULL:\n        raise AssertionError('`v is NULL`')\n    mgr.reordered = 1\n    while mgr.reordered == 1:\n        mgr.reordered = 0\n        r = _disjoin(mgr, 0, u, v)\n    if r is NULL:\n        raise AssertionError('`r is NULL`')\n    return r\n\n\n# This function is used for the hash in cache.\n#\n# This function exists because\n# `except? NULL` cannot be\n# used in the signature of the\n# function `_disjoin_root()`.\n#\n# Doing so changes the signature\n# in a way that passing `_disjoin_root()`\n# to the C function `cuddCacheLookup2Zdd()`\n# raises a Cython compilation error.\ncdef DdRef _disjoin_cache_id(\n        DdManager *mgr,\n        u:\n            DdRef,\n        v:\n            DdRef\n        ) noexcept:\n    \"\"\"Used only as cache key.\n\n    Passed inside the function `_disjoin()`\n    to the C functions:\n\n    - `cuddCacheLookup2Zdd()`\n    - `cuddCacheInsert2()`\n    \"\"\"\n\n\n# The function `_disjoin()` returns `NULL`\n# also in cases that are not due to\n# raising an exception.\n#\n# Those cases are due to reaching the point where\n# the tables need to be resized, and reordering\n# invoked.\ncdef DdRef _disjoin(\n        DdManager *mgr,\n        level:\n            _c_level,\n        u:\n            DdRef,\n        v:\n            DdRef\n        ) except? NULL:\n    \"\"\"Recursively disjoin `u` and `v`.\n\n    Asserts that:\n    - `level` <= level of `u`\n    - `level` <= level of `v`\n    \"\"\"\n    # `mgr is not NULL` BY `_disjoin_root()`.\n    # `mgr` remains unchanged throughout\n    # the recursion, and it is impossible\n    # to call `_disjoin()` from Python,\n    # so `mgr` not checked here, for efficiency.\n    if level < 0:\n        raise AssertionError(\n            f'`{level = } < 0`')\n    if u is NULL:\n        raise AssertionError('`u is NULL`')\n    if v is NULL:\n        raise AssertionError('`v is NULL`')\n    index = Cudd_ReadInvPermZdd(mgr, level)\n    # TODO: review reference counting\n    if u == DD_ZERO(mgr):\n        return v\n    if v == DD_ZERO(mgr):\n        return u\n    if index == -1 or index == CUDD_CONST_INDEX:\n        if u != DD_ONE(mgr):\n            raise AssertionError(\n                'nonconstant node `u` given, '\n                f'when given level:  {level}, '\n                f'with index:  {index}')\n        if v != DD_ONE(mgr):\n            raise AssertionError(\n                'nonconstant node `v` given, '\n                f'when given level:  {level}, '\n                f'with index:  {index}')\n        return u\n    r = cuddCacheLookup2Zdd(\n        mgr, _disjoin_cache_id, u, v)\n    if r is not NULL:\n        return r\n    u_index = Cudd_NodeReadIndex(u)\n    u_level = Cudd_ReadPermZdd(mgr, u_index)\n    v_index = Cudd_NodeReadIndex(v)\n    v_level = Cudd_ReadPermZdd(mgr, v_index)\n    if level > u_level:\n        raise AssertionError((level, u_level))\n    if level > v_level:\n        raise AssertionError((level, v_level))\n    if level < u_level:\n        pu, qu = u, DD_ZERO(mgr)\n    else:\n        pu, qu = cuddE(u), cuddT(u)\n    if level < v_level:\n        pv, qv = v, DD_ZERO(mgr)\n    else:\n        pv, qv = cuddE(v), cuddT(v)\n    p = _disjoin(mgr, level + 1, pu, pv)\n    if p is NULL:\n        return NULL\n    cuddRef(p)\n    q = _disjoin(mgr, level + 1, qu, qv)\n    if q is NULL:\n        Cudd_RecursiveDerefZdd(mgr, p)\n        return NULL\n    cuddRef(q)\n    r = _find_or_add(mgr, index, p, q)\n    if r is NULL:\n        Cudd_RecursiveDerefZdd(mgr, p)\n        Cudd_RecursiveDerefZdd(mgr, q)\n        return NULL\n    cuddRef(r)\n    Cudd_RecursiveDerefZdd(mgr, p)\n    Cudd_RecursiveDerefZdd(mgr, q)\n    cuddCacheInsert2(\n        mgr, _disjoin_cache_id, u, v, r)\n    cuddDeref(r)\n    return r\n\n\ncpdef Function _c_conjoin(\n        u:\n            Function,\n        v:\n            Function):\n    \"\"\"Return the conjunction of `u` and `v`.\"\"\"\n    r: DdRef\n    cdef DdManager *mgr\n    mgr = u.manager\n    r = _conjoin_root(\n        mgr, u.node, v.node)\n    return wrap(u.bdd, r)\n\n\ncdef DdRef _conjoin_root(\n        DdManager *mgr,\n        u:\n            DdRef,\n        v:\n            DdRef\n        ) except NULL:\n    \"\"\"Return the conjunction of `u` and `v`.\"\"\"\n    if mgr is NULL:\n        raise AssertionError('`mgr is NULL`')\n    if u is NULL:\n        raise AssertionError('`u is NULL`')\n    if v is NULL:\n        raise AssertionError('`v is NULL`')\n    mgr.reordered = 1\n    while mgr.reordered == 1:\n        mgr.reordered = 0\n        r = _conjoin(mgr, 0, u, v)\n    if r is NULL:\n        raise AssertionError('`r is NULL`')\n    return r\n\n\n# Similar to function `_disjoin_cache_id()`.\n# This function is used for the hash in cache.\ncdef DdRef _conjoin_cache_id(\n        DdManager *mgr,\n        u:\n            DdRef,\n        v:\n            DdRef\n        ) noexcept:\n    \"\"\"Used only as cache key.\n\n    Passed inside the function `_conjoin()`\n    to the C functions:\n\n    - `cuddCacheLookup2Zdd()`\n    - `cuddCacheInsert2()`\n    \"\"\"\n\n\ncdef DdRef _conjoin(\n        DdManager *mgr,\n        level:\n            _c_level,\n        u:\n            DdRef,\n        v:\n            DdRef\n        ) except? NULL:\n    \"\"\"Recursively conjoin `u` and `v`.\n\n    Asserts that:\n    - `level` <= level of `u`\n    - `level` <= level of `v`\n    \"\"\"\n    if level < 0:\n        raise AssertionError(\n            f'`{level = } < 0`')\n    if u is NULL:\n        raise AssertionError('`u is NULL`')\n    if v is NULL:\n        raise AssertionError('`v is NULL`')\n    index = Cudd_ReadInvPermZdd(mgr, level)\n    if u == DD_ZERO(mgr):\n        return u\n    if v == DD_ZERO(mgr):\n        return v\n    if index == -1 or index == CUDD_CONST_INDEX:\n        if u != DD_ONE(mgr):\n            raise AssertionError(\n                'nonconstant node `u` given, '\n                f'when given level:  {level}, '\n                f'with index:  {index}')\n        if v != DD_ONE(mgr):\n            raise AssertionError(\n                'nonconstant node `v` given, '\n                f'when given level:  {level}, '\n                f'with index:  {index}')\n        return u\n    r = cuddCacheLookup2Zdd(\n        mgr, _conjoin_cache_id, u, v)\n    if r is not NULL:\n        return r\n    u_index = Cudd_NodeReadIndex(u)\n    u_level = Cudd_ReadPermZdd(mgr, u_index)\n    v_index = Cudd_NodeReadIndex(v)\n    v_level = Cudd_ReadPermZdd(mgr, v_index)\n    if level > u_level:\n        raise AssertionError((level, u_level))\n    if level > v_level:\n        raise AssertionError((level, v_level))\n    if level < u_level:\n        pu, qu = u, DD_ZERO(mgr)\n    else:\n        pu, qu = cuddE(u), cuddT(u)\n    if level < v_level:\n        pv, qv = v, DD_ZERO(mgr)\n    else:\n        pv, qv = cuddE(v), cuddT(v)\n    p = _conjoin(mgr, level + 1, pu, pv)\n    if p is NULL:\n        return NULL\n    cuddRef(p)\n    q = _conjoin(mgr, level + 1, qu, qv)\n    if q is NULL:\n        Cudd_RecursiveDerefZdd(mgr, p)\n        return NULL\n    cuddRef(q)\n    r = _find_or_add(mgr, index, p, q)\n    if r is NULL:\n        Cudd_RecursiveDerefZdd(mgr, p)\n        Cudd_RecursiveDerefZdd(mgr, q)\n        return NULL\n    cuddRef(r)\n    Cudd_RecursiveDerefZdd(mgr, p)\n    Cudd_RecursiveDerefZdd(mgr, q)\n    cuddCacheInsert2(\n        mgr, _conjoin_cache_id, u, v, r)\n    cuddDeref(r)\n    return r\n\n\ncpdef Function _c_compose(\n        u:\n            Function,\n        dvars:\n            dict[\n                _VariableName,\n                Function]):\n    \"\"\"Compute composition of `u` with `dvars`.\"\"\"\n    r: DdRef\n    cdef DdManager *mgr\n    g: Function\n    mgr = u.manager\n    zdd = u.bdd\n    n_declared_vars = len(zdd.vars)\n    n_cudd_vars = zdd._number_of_cudd_vars()\n    if n_declared_vars != n_cudd_vars:\n        counts = _utils.var_counts(zdd)\n        contiguous = _utils.contiguous_levels(\n            '_c_compose', zdd)\n        raise AssertionError(f'{counts}\\n{contiguous}')\n    # convert `dvars` to `DdRef *`\n    cdef DdRef *vector\n    vector = <DdRef *> PyMem_Malloc(\n        n_cudd_vars * sizeof(DdRef))\n    for var in zdd.vars:\n        i = zdd._index_of_var[var]\n        if var in dvars:\n            g = dvars[var]\n        else:\n            g = zdd.var(var)\n        cuddRef(g.node)\n        if g.ref <= 0:\n            raise AssertionError((var, g.ref))\n        vector[i] = g.node\n    # compose\n    r = NULL\n    try:\n        r = _compose_root(mgr, u.node, vector)\n    finally:\n        if r is not NULL:\n            cuddRef(r)\n            if r.ref <= 0:\n                raise AssertionError(r.ref)\n        for i in range(n_cudd_vars):\n            Cudd_RecursiveDerefZdd(mgr, vector[i])\n        if r is not NULL:\n            cuddDeref(r)\n        PyMem_Free(vector)\n    if r is NULL:\n        raise AssertionError('r is NULL')\n    return wrap(u.bdd, r)\n\n\ncdef DdRef _compose_root(\n        DdManager *mgr,\n        u:\n            DdRef,\n        DdRef *vector\n        ) except NULL:\n    \"\"\"Root of recursive composition.\"\"\"\n    if mgr is NULL:\n        raise AssertionError('`mgr is NULL`')\n    if u is NULL:\n        raise AssertionError('`u is NULL`')\n    if vector is NULL:\n        raise AssertionError('`vector is NULL`')\n    r: DdRef\n    level = 0\n    mgr.reordered = 1\n    while mgr.reordered == 1:\n        mgr.reordered = 0\n        table = dict()\n        # table = cuddHashTableInit(mgr, 1, 2)\n        # if table is NULL:\n        #     return NULL\n        r = _compose(mgr, level, table, u, vector)\n        # if mgr.reordered == 1:\n        #     if r is not NULL:\n        #         raise AssertionError(r)\n        if r is not NULL:\n            cuddRef(r)\n            if r.ref <= 0:\n                raise AssertionError(r.ref)\n        # cuddHashTableQuitZdd(table)\n        for nd in table.values():\n            Cudd_RecursiveDerefZdd(mgr,\n                <DdRef><stdint.uintptr_t>nd)\n        if r is not NULL:\n            cuddDeref(r)\n    if r is NULL:\n        raise AssertionError('`r is NULL`')\n    return r\n\n\ncdef DdRef _compose(\n        DdManager *mgr,\n        level:\n            _c_level,\n        # DdHashTable *table,\n        table:\n            dict,\n        u:\n            DdRef,\n        DdRef *vector\n        ) except? NULL:\n    \"\"\"Recursively compute composition.\n\n    The composition is defined in the\n    array `vector`.\n\n    Asserts that:\n    - `level` <= level of `u`\n    \"\"\"\n    if level < 0:\n        raise AssertionError(\n            f'`{level = } < 0`')\n    if u is NULL:\n        raise AssertionError('`u is NULL`')\n    if vector is NULL:\n        raise AssertionError('`vector is NULL`')\n    if u == DD_ZERO(mgr):\n        return u\n    if u == DD_ONE(mgr):\n        r = Cudd_ReadZddOne(mgr, 0)\n        if r is NULL:\n            raise AssertionError(\n                '`Cudd_ReadZddOne()` returned `NULL`.')\n        return r\n    t = (<stdint.uintptr_t>u, level)\n    if t in table:\n        return <DdRef><stdint.uintptr_t>table[t]\n    # r = cuddHashTableLookup1(table, u)\n    # if r is not NULL:\n    #     return r\n    u_index = Cudd_NodeReadIndex(u)\n    u_level = Cudd_ReadPermZdd(mgr, u_index)\n    if level > u_level:\n        raise AssertionError((level, u_level))\n    index = Cudd_ReadInvPermZdd(mgr, level)\n    g = vector[index]\n    if g is NULL:\n        raise AssertionError('`g is NULL`')\n    if g.ref <= 0:\n        raise AssertionError((index, g.ref))\n    if level < u_level:\n        if level + 1 > u_level:\n            raise AssertionError((level, u_level))\n        c = _compose(\n            mgr, level + 1, table, u, vector)\n        if c is NULL:\n            return NULL\n        cuddRef(c)\n        if c.ref <= 0:\n            raise AssertionError(c.ref)\n        r = cuddZddIte(mgr, g, DD_ZERO(mgr), c)\n        if r is NULL:\n            Cudd_RecursiveDerefZdd(mgr, c)\n            return NULL\n        cuddRef(r)\n        if r.ref <= 0:\n            raise AssertionError(r.ref)\n        Cudd_RecursiveDerefZdd(mgr, c)\n    else:\n        if level != u_level:\n            raise AssertionError((level, u_level))\n        v, w = cuddE(u), cuddT(u)\n        if v.ref <= 0:\n            raise AssertionError(v.ref)\n        if w.ref <= 0:\n            raise AssertionError(w.ref)\n        p = _compose(\n            mgr, level + 1, table, v, vector)\n        if p is NULL:\n            return NULL\n        cuddRef(p)\n        if p.ref <= 0:\n            raise AssertionError(p.ref)\n        q = _compose(\n            mgr, level + 1, table, w, vector)\n        if q is NULL:\n            Cudd_RecursiveDerefZdd(mgr, p)\n            return NULL\n        cuddRef(q)\n        if q.ref <= 0:\n            raise AssertionError(q.ref)\n        r = cuddZddIte(mgr, g, q, p)\n        if r is NULL:\n            Cudd_RecursiveDerefZdd(mgr, q)\n            Cudd_RecursiveDerefZdd(mgr, p)\n            return NULL\n        cuddRef(r)\n        if r.ref <= 0:\n            raise AssertionError(r.ref)\n        Cudd_RecursiveDerefZdd(mgr, p)\n        Cudd_RecursiveDerefZdd(mgr, q)\n    # insert in the hash table\n    cuddRef(r)\n    table[t] = <stdint.uintptr_t>r\n    # fanout = <ptrint> u.ref\n    # tr = cuddHashTableInsert1(table, u, r, fanout)\n    # if tr == 0:\n    #     Cudd_RecursiveDerefZdd(mgr, r)\n    #     return NULL\n    cuddDeref(r)\n    return r\n\n\n# This function is similar to `cuddHashTableQuit`.\ncdef bint cuddHashTableQuitZdd(\n        DdHashTable * hash\n        ) except False:\n    \"\"\"Shutdown a hash table.\n\n    This function calls `Cudd_RecursiveDerefZdd()`.\n    The function `cuddHashTableQuit()` calls\n    `Cudd_RecursiveDeref()`.\n    \"\"\"\n    if hash is NULL:\n        raise AssertionError('`hash is NULL`')\n    cdef unsigned int i;\n    cdef DdManager *dd = hash.manager;\n    cdef DdHashItem *bucket;\n    cdef DdHashItem **memlist\n    cdef DdHashItem **nextmem;\n    cdef unsigned int numBuckets = hash.numBuckets;\n    for i in range(numBuckets):\n        bucket = hash.bucket[i]\n        while bucket is not NULL:\n            Cudd_RecursiveDerefZdd(dd, bucket.value)\n            bucket = bucket.next\n    memlist = hash.memoryList\n    while (memlist is not NULL):\n        nextmem = <DdHashItem **> memlist[0]\n        FREE(memlist)\n        memlist = nextmem\n    FREE(hash.bucket)\n    FREE(hash)\n    return True\n\n\ncpdef set _c_support(\n        u:\n            Function):\n    \"\"\"Compute support of `u`.\"\"\"\n    zdd = u.bdd\n    n = max(zdd._var_with_index) + 1\n    cdef int *support\n    support = <int *> PyMem_Malloc(n * sizeof(int))\n    for i in range(n):\n        support[i] = 0\n    try:\n        level = 0\n        if u.manager is NULL:\n            raise AssertionError('`u.manager is NULL`')\n        _support(u.manager, level, Cudd_Regular(u.node), support)\n        _clear_markers(Cudd_Regular(u.node))\n        support_vars = set()\n        for i in range(n):\n            if support[i] == 1:\n                var = zdd._var_with_index[i]\n                support_vars.add(var)\n    finally:\n        PyMem_Free(support)\n    return support_vars\n\n\ncdef bint _support(\n        DdManager *mgr,\n        level:\n            _c_level,\n        u:\n            DdRef,\n        int *support\n        ) except False:\n    \"\"\"Recursively compute the support.\"\"\"\n    if level < 0:\n        raise AssertionError(\n            f'`{level = } < 0`')\n    if u is NULL:\n        raise AssertionError('`u is NULL`')\n    if support is NULL:\n        raise AssertionError('`support is NULL`')\n    index = Cudd_ReadInvPermZdd(mgr, level)\n    # terminal ?\n    if (u == DD_ZERO(mgr) or\n            index == -1 or\n            index == CUDD_CONST_INDEX):\n        return True\n    # visited ?\n    if Cudd_IsComplement(u.next):\n        return True\n    u_index = Cudd_NodeReadIndex(u)\n    u_level = Cudd_ReadPermZdd(mgr, u_index)\n    if level > u_level:\n        raise AssertionError((level, u_level))\n    v, w = Cudd_Regular(cuddE(u)), cuddT(u)\n    if level < u_level:\n        support[index] = 1\n        _support(mgr, level + 1, u, support)\n    elif v == w:\n        _support(mgr, level + 1, v, support)\n    else:\n        support[index] = 1\n        _support(mgr, level + 1, v, support)\n        _support(mgr, level + 1, w, support)\n    u.next = Cudd_Not(u.next)\n    return True\n\n\ncdef bint _clear_markers(\n        u:\n            DdRef\n        ) except False:\n    \"\"\"Recursively clear complementation bits.\"\"\"\n    if u is NULL:\n        raise AssertionError('`u is NULL`')\n    if not Cudd_IsComplement(u.next):\n        return True\n    u.next = Cudd_Regular(u.next)\n    if cuddIsConstant(u):\n        return True\n    v, w = Cudd_Regular(cuddE(u)), cuddT(u)\n    _clear_markers(v)\n    _clear_markers(w)\n    return True\n\n\ncpdef _test_call_dealloc(\n        u:\n            Function):\n    \"\"\"Duplicates the code of `Function.__dealloc__`.\n\n    For details read the docstring of the function\n    `dd.cudd._test_call_dealloc`.\n    \"\"\"\n    self = u\n    # the code of `Function.__dealloc__` follows:\n    if self._ref < 0:\n        raise AssertionError(\n            \"The lower bound `_ref` on the node's \"\n            f'reference count has value {self._ref}, '\n            'which is unexpected and should never happen. '\n            'Was the value of `_ref` changed from outside '\n            'this instance?')\n    assert self._ref >= 0, self._ref\n    if self._ref == 0:\n        return\n    if self.node is NULL:\n        raise AssertionError(\n            'The attribute `node` is a `NULL` pointer. '\n            'This is unexpected and should never happen. '\n            'Was the value of `_ref` changed from outside '\n            'this instance?')\n    # anticipate multiple calls to `__dealloc__`\n    self._ref -= 1\n    # deref\n    Cudd_RecursiveDerefZdd(self.manager, self.node)\n    # avoid future access to deallocated memory\n    self.node = NULL\n\n\ncpdef Function _call_method_disjoin(\n        zdd:\n            ZDD,\n        level:\n            _Level,\n        u:\n            Function,\n        v:\n            Function,\n        cache:\n            dict):\n    \"\"\"Wrapper of method `ZDD._disjoin()`.\n\n    To enable testing the `cdef` method from Python.\n    \"\"\"\n    return zdd._disjoin(level, u, v, cache)\n\n\ncpdef Function _call_method_conjoin(\n        zdd:\n            ZDD,\n        level:\n            _Level,\n        u:\n            Function,\n        v:\n            Function,\n        cache:\n            dict):\n    \"\"\"Wrapper of method `ZDD._conjoin()`.\n\n    Similar to `_call_method_disjoin()`.\n    \"\"\"\n    return zdd._conjoin(level, u, v, cache)\n\n\ncpdef Function _call_disjoin(\n        level:\n            _Level,\n        u:\n            Function,\n        v:\n            Function):\n    \"\"\"Wrapper of function `_disjoin()`.\n\n    To enable testing the `cdef` function from Python.\n    \"\"\"\n    r = _disjoin(\n        u.bdd.manager,\n        level,\n        u.node,\n        v.node)\n    return wrap(u.bdd, r)\n\n\ncpdef Function _call_conjoin(\n        level:\n            _Level,\n        u:\n            Function,\n        v:\n            Function):\n    \"\"\"Wrapper of function `_conjoin()`.\n\n    Similar to the function `_call_disjoin()`.\n    \"\"\"\n    r = _conjoin(\n        u.bdd.manager,\n        level,\n        u.node,\n        v.node)\n    return wrap(u.bdd, r)\n"
  },
  {
    "path": "dd/dddmp.py",
    "content": "\"\"\"Parser for DDDMP file format.\n\nCUDD exports Binary Decision Diagrams (BDD) in DDDMP.\nFor more details on the Decision Diagram DuMP (DDDMP) package,\nread the file [1] included in the CUDD distribution [2].\nThe text file format details can be found\nby reading the source code [3].\n\n\nReferences\n==========\n\n[1] Gianpiero Cabodi and Stefano Quer\n    \"DDDMP: Decision Diagram DuMP package\"\n    `cudd-X.Y.Z/dddmp/doc/dddmp-2.0-Letter.ps`, 2004\n\n[2] <http://vlsi.colorado.edu/~fabio/CUDD/>\n\n[3] `cudd-X.Y.Z/dddmp/dddmpStoreBdd.c`, lines: 329--331, 345, 954\n\"\"\"\n# Copyright 2014 by California Institute of Technology\n# All rights reserved. Licensed under BSD-3.\n#\nimport logging\nimport typing as _ty\n\nimport astutils\nimport ply.lex\nimport ply.yacc\n\nimport dd.bdd as _bdd\n\n\nlogger = logging.getLogger(__name__)\nTABMODULE: _ty.Final = 'dd._dddmp_parser_state_machine'\nLEX_LOG: _ty.Final = 'dd.dddmp.lex_logger'\nYACC_LOG: _ty.Final = 'dd.dddmp.yacc_logger'\nPARSER_LOG: _ty.Final = 'dd.dddmp.parser_logger'\n\n\nclass Lexer:\n    \"\"\"Token rules to build LTL lexer.\"\"\"\n\n    def __init__(\n            self,\n            debug=False):\n        reserved = {\n            'ver':\n                'VERSION',\n            'add':\n                'ADD',\n            'mode':\n                'FILEMODE',\n            'varinfo':\n                'VARINFO',\n            'dd':\n                'DD',\n            'nnodes':\n                'NNODES',\n            'nvars':\n                'NVARS',\n            'orderedvarnames':\n                'ORDEREDVARNAMES',\n            'nsuppvars':\n                'NSUPPVARS',\n            'suppvarnames':\n                'SUPPVARNAMES',\n            'ids':\n                'IDS',\n            'permids':\n                'PERMIDS',\n            'auxids':\n                'AUXIDS',\n            'nroots':\n                'NROOTS',\n            'rootids':\n                'ROOTIDS',\n            'rootnames':\n                'ROOTNAMES',\n            # 'nodes':\n            #     'NODES',\n            # 'end':\n            #     'END'\n            }\n        self.reserved = {\n            f'.{k}': v\n            for k, v in reserved.items()}\n        self.misc = [\n            'MINUS',\n            'DOT',\n            'NAME',\n            'NUMBER']\n        self.tokens = self.misc + list(\n            sorted(self.reserved.values()))\n        self.build(debug=debug)\n\n    # token rules\n    t_MINUS = r' \\- '\n    t_DOT = r' \\. '\n    t_NUMBER = r' \\d+ '\n    t_ignore = ''.join(['\\x20', '\\t'])\n\n    def t_KEYWORD(\n            self,\n            token):\n        r\"\"\"\n        \\.\n        [a-zA-Z]\n        [a-zA-Z]*\n        \"\"\"\n        token.type = self.reserved.get(\n            token.value, 'NAME')\n        return token\n\n    def t_NAME(\n            self,\n            token):\n        r\"\"\"\n        [a-zA-Z_]\n        [a-zA-Z_@0-9'\\.]*\n        \"\"\"\n        token.type = self.reserved.get(\n            token.value, 'NAME')\n        return token\n\n    def t_comment(\n            self,\n            token):\n        r' \\# .* '\n        return\n\n    def t_newline(\n            self,\n            token):\n        r' \\n+ '\n\n    def t_error(\n            self,\n            token):\n        raise ValueError(\n            f'Unexpected character \"{token.value[0]}\"')\n\n    def build(\n            self,\n            debug=False,\n            debuglog=None,\n            **kwargs):\n        \"\"\"Create a lexer.\n\n        @param kwargs:\n            Same arguments as `ply.lex.lex`:\n\n            - except for `module` (fixed to `self`)\n            - `debuglog` defaults to `logger`.\n        \"\"\"\n        if debug and debuglog is None:\n            debuglog = logging.getLogger(LEX_LOG)\n        self.lexer = ply.lex.lex(\n            module=self,\n            debug=debug,\n            debuglog=debuglog,\n            **kwargs)\n\n\nclass Parser:\n    \"\"\"Production rules to build LTL parser.\"\"\"\n\n    def __init__(\n            self):\n        self.tabmodule = TABMODULE\n        self._lexer = Lexer()\n        self.tokens = self._lexer.tokens\n        self.reset()\n        self.parser = None\n\n    def build(\n            self,\n            tabmodule=None,\n            outputdir=None,\n            write_tables=False,\n            debug=False,\n            debuglog=None):\n        if tabmodule is None:\n            tabmodule = self.tabmodule\n        if debug and debuglog is None:\n            debuglog = logger\n        self._lexer.build(debug=debug)\n        self.parser = ply.yacc.yacc(\n            module=self,\n            start='file',\n            tabmodule=tabmodule,\n            outputdir=outputdir,\n            write_tables=write_tables,\n            debug=debug,\n            debuglog=debuglog)\n\n    def parse(\n            self,\n            filename,\n            debuglog=None):\n        \"\"\"Parse DDDMP file containing BDD.\"\"\"\n        if self.parser is None:\n            self.build()\n        levels, roots = self._parse_header(filename, debuglog)\n        self._parse_body(filename)\n        return self.bdd, self.n_vars, levels, roots\n\n    def _parse_header(\n            self,\n            filename,\n            debuglog):\n        self.reset()\n        if debuglog is None:\n            debuglog = logging.getLogger(PARSER_LOG)\n        # parse header (small but inhomogeneous)\n        with open(filename, 'r') as f:\n            a = list()\n            for line in f:\n                if '.nodes' in line:\n                    break\n                a.append(line)\n            s = '\\n'.join(a)\n            lexer = self._lexer.lexer\n            lexer.input(s)\n            r = self.parser.parse(lexer=lexer, debug=debuglog)\n        if r is None:\n            raise Exception('failed to parse')\n        self._assert_consistent()\n        # prepare mapping from fixed var index to level among all vars\n        # id2name = {\n        #     i: var\n        #     for i, var in zip(self.var_ids, self.support_vars)}\n        c = self.var_extra_info\n        if c == 0:\n            logger.info('var IDs')\n            id2permid = {\n                i: k\n                for i, k in zip(self.var_ids, self.permuted_var_ids)}\n            self.info2permid = id2permid\n        elif c == 1:\n            logger.info('perm IDs')\n            self.info2permid = {k: k for k in self.permuted_var_ids}\n        elif c == 2:\n            logger.info('aux IDs')\n            raise NotImplementedError\n        elif c == 3:\n            logger.info('var names')\n            self.info2permid = {\n                var: k for k, var in enumerate(self.ordered_vars)}\n        elif c == 4:\n            logger.info('none')\n            raise NotImplementedError\n        else:\n            raise Exception('unknown `varinfo` case')\n        self.info2permid['T'] = self.n_vars + 1\n        # support_var_ord_ids = {\n        #     d['var_index'] for u, d in g.nodes(data=True)}\n        # if len(support_var_ord_ids) != self.n_support_vars:\n        #     raise AssertionError((\n        #         support_var_ord_ids, self.n_support_vars))\n        # prepare levels\n        if self.ordered_vars is not None:\n            levels = {var: k for k, var in enumerate(self.ordered_vars)}\n        elif self.support_vars is not None:\n            permid2var = {\n                k: var for k, var in zip(self.permuted_var_ids,\n                                         self.support_vars)}\n            levels = {\n                permid2var[k]: k for k in sorted(self.permuted_var_ids)}\n        else:\n            levels = {\n                idx: level for level, idx in\n                enumerate(self.permuted_var_ids)}\n        roots = set(self.rootids)\n        return levels, roots\n\n    def _parse_body(\n            self,\n            filename):\n        # parse nodes (large but very uniform)\n        with open(filename, 'r') as f:\n            for line in f:\n                if '.nodes' in line:\n                    break\n            for line in f:\n                if '.end' in line:\n                    break\n                u, info, index, v, w = line.split(' ')\n                u, index, v, w = map(int, (u, index, v, w))\n                try:\n                    info = int(info)\n                except ValueError:\n                    pass  # info == 'T' or `str` var name\n                if info not in self.info2permid:\n                    raise AssertionError(\n                        (info, self.info2permid))\n                self._add_node(u, info, index, v, w)\n        if len(self.bdd) != self.n_nodes:\n            raise AssertionError((len(self.bdd), self.n_nodes))\n\n    def _add_node(\n            self,\n            u,\n            info,\n            index,\n            v,\n            w):\n        \"\"\"Add new node to BDD.\n\n        @type u, index, v, w:\n            `int`\n        @type info:\n            `int` or `\"T\"`\n        \"\"\"\n        if v == 0:\n            v = None\n        elif v < 0:\n            raise ValueError(\n                'only \"else\" edges '\n                f'can be complemented ({v = })')\n        if w == 0:\n            w = None\n        # map fixed var index to level among all vars\n        level = self.info2permid[info]\n        # dddmp stores (high, low)\n        # swap to (low, high), as used in `dd.bdd`\n        self.bdd[u] = (level, w, v)\n\n    def reset(\n            self):\n        self.bdd = dict()\n        self.algebraic_dd = None\n        self.var_extra_info = None\n        self.n_nodes = None\n        self.rootids = None\n        self.n_roots = None\n        # vars\n        self.n_vars = None\n        self.ordered_vars = None\n        # support vars\n        self.n_support_vars = None\n        self.support_vars = None\n        # permuted and aux vars\n        self.var_ids = None\n        self.permuted_var_ids = None\n        self.aux_var_ids = None\n        self.info2permid = None\n\n    def _assert_consistent(\n            self):\n        \"\"\"Check that the loaded attributes are reasonable.\"\"\"\n        if self.support_vars is not None:\n            if len(self.support_vars) != self.n_support_vars:\n                raise AssertionError((\n                    len(self.support_vars),\n                    self.n_support_vars,\n                    self.support_vars))\n        if self.ordered_vars is not None:\n            if len(self.ordered_vars) != self.n_vars:\n                raise AssertionError((\n                    len(self.ordered_vars),\n                    self.n_vars,\n                    self.ordered_vars))\n        if len(self.var_ids) != self.n_support_vars:\n            raise AssertionError((\n                len(self.var_ids),\n                self.n_support_vars,\n                self.var_ids))\n        if len(self.permuted_var_ids) != self.n_support_vars:\n            raise AssertionError((\n                len(self.permuted_var_ids),\n                self.n_support_vars,\n                self.permuted_var_ids))\n        if self.aux_var_ids is not None:\n            if len(self.aux_var_ids) != self.n_support_vars:\n                raise AssertionError((\n                    len(self.aux_var_ids),\n                    self.n_support_vars,\n                    self.aux_var_ids))\n        if len(self.rootids) != self.n_roots:\n            raise AssertionError((\n                len(self.rootids),\n                self.n_roots,\n                self.rootids))\n\n    def p_file(\n            self, p):\n        \"\"\"file : lines\"\"\"\n        p[0] = True\n\n    def p_lines_iter(\n            self, p):\n        \"\"\"lines : lines line\"\"\"\n\n    def p_lines_end(\n            self, p):\n        \"\"\"lines : line\"\"\"\n\n    def p_line(\n            self, p):\n        \"\"\"line : version\n                | mode\n                | varinfo\n                | diagram_name\n                | nnodes\n                | nvars\n                | nsupportvars\n                | supportvars\n                | orderedvars\n                | varids\n                | permids\n                | auxids\n                | nroots\n                | rootids\n                | algdd\n                | rootnames\n        \"\"\"\n\n    def p_version(\n            self, p):\n        \"\"\"version : VERSION name MINUS number DOT number\"\"\"\n\n    def p_text_mode(\n            self, p):\n        \"\"\"mode : FILEMODE NAME\"\"\"\n        f = p[2]\n        if f == 'A':\n            logger.debug('text mode')\n        elif f == 'B':\n            logger.debug('binary mode')\n            raise Exception('This parser supports only text DDDMP format.')\n        else:\n            raise Exception(f'unknown DDDMP format: {f}')\n\n    def p_varinfo(\n            self, p):\n        \"\"\"varinfo : VARINFO number\"\"\"\n        self.var_extra_info = p[2]\n\n    def p_dd_name(\n            self, p):\n        \"\"\"diagram_name : DD name\"\"\"\n        self.bdd_name = p[2]\n\n    def p_num_nodes(\n            self, p):\n        \"\"\"nnodes : NNODES number\"\"\"\n        self.n_nodes = p[2]\n\n    def p_num_vars(\n            self, p):\n        \"\"\"nvars : NVARS number\"\"\"\n        self.n_vars = p[2]\n\n    def p_nsupport_vars(\n            self, p):\n        \"\"\"nsupportvars : NSUPPVARS number\"\"\"\n        self.n_support_vars = p[2]\n\n    def p_support_varnames(\n            self, p):\n        \"\"\"supportvars : SUPPVARNAMES varnames\"\"\"\n        self.support_vars = p[2]\n\n    def p_ordered_varnames(\n            self, p):\n        \"\"\"orderedvars : ORDEREDVARNAMES varnames\"\"\"\n        self.ordered_vars = p[2]\n\n    def p_varnames_iter(\n            self, p):\n        \"\"\"varnames : varnames varname\"\"\"\n        p[1].append(p[2])\n        p[0] = p[1]\n\n    def p_varnames_end(\n            self, p):\n        \"\"\"varnames : varname\"\"\"\n        p[0] = [p[1]]\n\n    def p_varname(\n            self, p):\n        \"\"\"varname : name\n                   | number\n        \"\"\"\n        p[0] = p[1]\n\n    def p_var_ids(\n            self, p):\n        \"\"\"varids : IDS integers\"\"\"\n        self.var_ids = p[2]\n\n    def p_permuted_ids(\n            self, p):\n        \"\"\"permids : PERMIDS integers\"\"\"\n        self.permuted_var_ids = p[2]\n\n    def p_aux_ids(\n            self, p):\n        \"\"\"auxids : AUXIDS integers\"\"\"\n        self.aux_var_ids = p[2]\n\n    def p_integers_iter(\n            self, p):\n        \"\"\"integers : integers number\"\"\"\n        p[1].append(p[2])\n        p[0] = p[1]\n\n    def p_integers_end(\n            self, p):\n        \"\"\"integers : number\"\"\"\n        p[0] = [p[1]]\n\n    def p_num_roots(\n            self, p):\n        \"\"\"nroots : NROOTS number\"\"\"\n        self.n_roots = p[2]\n\n    def p_root_ids(\n            self, p):\n        \"\"\"rootids : ROOTIDS integers\"\"\"\n        self.rootids = p[2]\n\n    def p_root_names(\n            self, p):\n        \"\"\"rootnames : ROOTNAMES varnames\"\"\"\n        raise NotImplementedError\n\n    def p_algebraic_dd(\n            self, p):\n        \"\"\"algdd : ADD\"\"\"\n        self.algebraic_dd = True\n\n    def p_number(\n            self, p):\n        \"\"\"number : NUMBER\"\"\"\n        p[0] = int(p[1])\n\n    def p_neg_number(\n            self, p):\n        \"\"\"number : MINUS NUMBER\"\"\"\n        p[0] = -int(p[2])\n\n    def p_expression_name(\n            self, p):\n        \"\"\"name : NAME\"\"\"\n        p[0] = p[1]\n\n    def p_error(\n            self, p):\n        raise Exception(f'Syntax error at \"{p}\"')\n\n\ndef load(\n        fname:\n            str\n        ) -> _bdd.BDD:\n    \"\"\"Return a `BDD` loaded from DDDMP file `fname`.\n\n    If no `.orderedvarnames` appear in the file,\n    then `.suppvarnames` and `.permids` are used instead.\n    In the second case, the variable levels contains blanks.\n    To avoid blanks, the levels are re-indexed here.\n    This has no effect if `.orderedvarnames` appears in the file.\n\n    DDDMP files are dumped by [CUDD](\n        http://vlsi.colorado.edu/~fabio/CUDD/).\n    \"\"\"\n    parser = Parser()\n    bdd_succ, n_vars, levels, roots = parser.parse(fname)\n    # reindex to ensure no blanks\n    perm = {k: var for var, k in levels.items()}\n    perm = {i: perm[k] for i, k in enumerate(sorted(perm))}\n    new_levels = {var: k for k, var in perm.items()}\n    old2new = {levels[var]: new_levels[var] for var in levels}\n    # convert\n    bdd = _bdd.BDD(new_levels)\n    umap = {-1: -1, 1: 1}\n    for j in range(len(new_levels) - 1, -1, -1):\n        for u, (k, v, w) in bdd_succ.items():\n            # terminal ?\n            if v is None:\n                if w is not None:\n                    raise AssertionError(w)\n                continue\n            # non-terminal\n            i = old2new[k]\n            if i != j:\n                continue\n            p, q = umap[abs(v)], umap[w]\n            if v < 0:\n                p = -p\n            r = bdd.find_or_add(i, p, q)\n            umap[abs(u)] = r\n    bdd.roots.update(roots)\n    return bdd\n\n\ndef _rewrite_tables(\n        outputdir:\n            str='./'\n        ) -> None:\n    \"\"\"Write the parser table file, even if it exists.\"\"\"\n    astutils.rewrite_tables(Parser, TABMODULE, outputdir)\n\n\nif __name__ == '__main__':\n    _rewrite_tables()\n"
  },
  {
    "path": "dd/mdd.py",
    "content": "\"\"\"Ordered multi-valued decision diagrams.\n\n\nReferences\n==========\n\nArvind Srinivasan, Timothy Kam, Sharad Malik, Robert K. Brayton\n    \"Algorithms for discrete function manipulation\"\n    IEEE International Conference on\n    Computer-Aided Design (ICCAD), 1990\n    pages 92--95\n\nMichael Miller and Rolf Drechsler\n    \"Implementing a multiple-valued decision diagram package\"\n    28th International Symposium on\n    Multiple-Valued Logic (ISMVL), 1998\n    pages 52--57\n\"\"\"\n# Copyright 2015 by California Institute of Technology\n# All rights reserved. Licensed under BSD-3.\n#\nimport collections.abc as _abc\nimport itertools as _itr\nimport logging\nimport sys\nimport typing as _ty\n\nimport dd._abc\nimport dd.bdd as _bdd\nimport dd._utils as _utils\n\n\nlogger = logging.getLogger(__name__)\nTABMODULE: _ty.Final = (\n    'dd.mdd_parser_state_machine')\nPLY_LOG: _ty.Final = 'dd.mdd.ply'\n\n\n_Ref: _ty.TypeAlias = int\n_Level: _ty.TypeAlias = int\n_Successors: _ty.TypeAlias = tuple[\n    int |\n    None, ...]\n\n\nclass MDD:\n    \"\"\"Shared ordered multi-valued decision diagram.\n\n    Represents a Boolean function of integer variables.\n    Nodes are integers.\n    The terminal node is 1.\n    Complemented edges are represented as negated nodes.\n    Values returned by methods are edges, possibly complemented.\n    Edge 0 is never complemented.\n\n    Attributes:\n      - `vars`\n      - `max_nodes`\n\n    If you want a node to survive garbage collection,\n    increment its reference counter:\n\n    `mdd.incref(edge)`\n    \"\"\"\n\n    # dvars:\n    # `dict` that maps each variable to a `dict`\n    # that defines its:\n    #  - len\n    #  - level\n\n    def __init__(\n            self,\n            dvars:\n                dict[str, dict] |\n                None=None):\n        self._pred: dict[\n            tuple[int, ...],\n            int]= dict()\n        self._succ: dict[\n            int,\n            _Successors\n            ] = dict()\n        self._ref: dict[\n            int, int\n            ] = dict()\n        self._max: int = 1\n        self._ite_table: dict = dict()\n        if dvars is None:\n            dvars = dict()\n        else:\n            i = len(dvars)\n            self._succ[1] = (i, None)\n            self._ref[1] = 0\n        self.vars: dict[str, dict] = dict(dvars)\n        self._level_to_var: dict[\n            _Level, str\n            ] | None = None\n        self._parser = None\n        self._free: set[int] = set()\n        self.max_nodes: int = sys.maxsize\n\n    def __len__(\n            self):\n        \"\"\"Return number of BDD nodes.\"\"\"\n        return len(self._succ)\n\n    def __contains__(\n            self,\n            u:\n                _Ref\n            ) -> bool:\n        \"\"\"Return `True` if `u` is a BDD node.\"\"\"\n        return abs(u) in self._succ\n\n    def __iter__(\n            self\n            ) -> _abc.Iterable[int]:\n        return iter(self._succ)\n\n    def _allocate(\n            self\n            ) -> int:\n        \"\"\"Return free integer, mark it as occupied.\"\"\"\n        if self._free:\n            return self._free.pop()\n        else:\n            self._max += 1\n            return self._max\n\n    def _release(\n            self,\n            u:\n                int\n            ) -> None:\n        \"\"\"Unmark integer from used ones.\"\"\"\n        if u > self._max:\n            raise AssertionError(u)\n        if u in self._free:\n            raise AssertionError(u)\n        if u in self._succ:\n            raise AssertionError(u)\n        if u in self._pred:\n            raise AssertionError(u)\n        if u in self._ref:\n            raise AssertionError(u)\n        self._free.add(u)\n\n    def incref(\n            self,\n            u:\n                _Ref\n            ) -> None:\n        \"\"\"Increment reference count of node `abs(u)`.\"\"\"\n        self._ref[abs(u)] += 1\n\n    def decref(\n            self,\n            u:\n                _Ref\n            ) -> None:\n        \"\"\"Decrement reference count of node `abs(u)`, with 0 as min.\"\"\"\n        if self._ref[abs(u)] > 0:\n            self._ref[abs(u)] -= 1\n\n    def ref(\n            self,\n            u:\n                _Ref\n            ) -> int:\n        \"\"\"Return reference count for node `abs(u)`.\"\"\"\n        return self._ref[abs(u)]\n\n    def var_at_level(\n            self,\n            i:\n                _Level\n            ) -> str:\n        \"\"\"Return variable with level `i`.\"\"\"\n        if self._level_to_var is None:\n            self._level_to_var = {\n                d['level']: var\n                for var, d in self.vars.items()}\n        return self._level_to_var[i]\n\n    def level_of_var(\n            self,\n            var:\n                str\n            ) -> _Level:\n        \"\"\"Return level of variable `var`.\"\"\"\n        return self.vars[var]['level']\n\n    def ite(\n            self,\n            g:\n                _Ref,\n            u:\n                _Ref,\n            v:\n                _Ref\n            ) -> _Ref:\n        \"\"\"Return node for `if g then u, else v`.\"\"\"\n        # is g terminal ?\n        if g == 1:\n            return u\n        elif g == -1:\n            return v\n        # g is non-terminal\n        # already computed ?\n        t = (g, u, v)\n        w = self._ite_table.get(t)\n        if w is not None:\n            return w\n        z = min(self._succ[abs(g)][0],\n                self._succ[abs(u)][0],\n                self._succ[abs(v)][0])\n        gc = self._top_cofactor(g, z)\n        uc = self._top_cofactor(u, z)\n        vc = self._top_cofactor(v, z)\n        nodes = tuple(_itr.starmap(\n            self.ite,\n            zip(gc, uc, vc)))\n        w = self.find_or_add(z, *nodes)\n        # cache\n        self._ite_table[t] = w\n        return w\n\n    def _top_cofactor(\n            self,\n            u:\n                _Ref,\n            level:\n                _Level\n            ) -> tuple[\n                _Ref,\n                ...]:\n        \"\"\"Return topmost cofactors.\n\n        If `level` is the topmost level of `u`,\n        then return successors. Else return\n        copies of `u`.\n        \"\"\"\n        varname = self.var_at_level(level)\n        n = self.vars[varname]['len']\n        # leaf ?\n        if abs(u) == 1:\n            return (u,) * n\n        u_level, *nodes = self._succ[abs(u)]\n        if level < u_level:\n            return (u,) * n\n        def check(\n                node\n                ) -> int:\n            if node:\n                return node\n            raise AssertionError(node)\n        nodes = map(check, nodes)\n        if level == u_level:\n            if u > 0:\n                return tuple(nodes)\n            if u < 0:\n                return tuple(-v for v in nodes)\n            raise AssertionError(\n                'Expected `u != 0`, '\n                f'but: {u = }')\n        raise AssertionError(\n            'for `u_level < level`, '\n            'call instead the method '\n            '`cofactor()`. (here: '\n            f'{level = } and {u_level = }')\n\n    def find_or_add(\n            self,\n            i:\n                _Level,\n            *nodes:\n                _Ref\n            ) -> _Ref:\n        \"\"\"Return node at level `i` with successors in `nodes`.\n\n        @param i:\n            level in `range(len(vars))`\n        \"\"\"\n        if not (0 <= i < len(self.vars)):\n            raise ValueError(i)\n        var = self.var_at_level(i)\n        if len(nodes) != self.vars[var]['len']:\n            raise ValueError(\n                (var, len(nodes), self.vars[var]['len']))\n        if not nodes:  # in case max == 0\n            raise ValueError(nodes)\n        for u in nodes:\n            if abs(u) not in self:\n                raise ValueError(u)\n        # canonicity of complemented edges\n        if nodes[0] < 0:\n            nodes = tuple(-u for u in nodes)\n            r = -1\n        else:\n            r = 1\n        # eliminate\n        if len(set(nodes)) == 1:\n            return r * nodes[0]\n        # already exists ?\n        t = (i, *nodes)\n        u = self._pred.get(t)\n        if u is not None:\n            return r * u\n        u = self._allocate()\n        if u in self:\n            raise AssertionError((self._succ, u, t))\n        # add node\n        self._pred[t] = u\n        self._succ[u] = t\n        self._ref[u] = 0\n        # reference counting\n        for v in nodes:\n            self.incref(v)\n        return r * u\n\n    def collect_garbage(\n            self,\n            roots:\n                _abc.Iterable[_Ref] |\n                None=None\n            ) -> None:\n        \"\"\"Recursively remove nodes with zero reference count.\"\"\"\n        if roots is None:\n            roots = self._ref\n        unused = {u for u in roots if not self.ref(u)}\n        # keep terminal\n        if 1 in unused:\n            unused.remove(1)\n        while unused:\n            u = unused.pop()\n            if u == 1:\n                raise AssertionError(u)\n            # remove\n            t = self._succ.pop(u)\n            u_ = self._pred.pop(t)\n            uref = self._ref.pop(u)\n            self._release(u)\n            if u != u_:\n                raise AssertionError((u, u_))\n            if uref != 0:\n                raise AssertionError(uref)\n            if u not in self._free:\n                raise AssertionError((u, self._free))\n            # recurse\n            # decrement reference counters\n            nodes = t[1:]\n            for v in nodes:\n                self.decref(v)\n                # unused ?\n                if not self._ref[abs(v)] and abs(v) != 1:\n                    unused.add(abs(v))\n        self._ite_table = dict()\n\n    def to_expr(\n            self,\n            u:\n                _Ref\n            ) -> str:\n        if u == 1:\n            return str(u)\n        elif u == -1:\n            return '0'\n        t = self._succ[abs(u)]\n        i = t[0]\n        nodes = t[1:]\n        var = self.var_at_level(i)\n        # group per target node\n        c = tuple(set(nodes))\n        e = {x: self.to_expr(x) for x in c}\n        cond = {v: set() for v in c}\n        for j, x in enumerate(nodes):\n            cond[x].add(j)\n        # format\n        cond_str = dict()\n        for k, v in cond.items():\n            if len(v) == 1:\n                (j,) = v\n                cond_str[k] = f'= {j}'\n            else:\n                cond_str[k] = f'in {v}'\n        x = c[0]\n        s = 'if ({var} {j}): {p}, '.format(\n            var=var, j=cond_str[x], p=e[x])\n        s += ', '.join(\n            '\\nelif ({var} {j}): {p}'.format(\n                var=var, j=cond_str[x], p=e[x])\n            for x in c[1:])\n        if u < 0:\n            s = f'! {s}'\n        s = f'({s})'\n        return s\n\n    def apply(\n            self,\n            op:\n                dd._abc.OperatorSymbol,\n            u:\n                _Ref,\n            v:\n                _Ref |\n                None=None,\n            w:\n                _Ref |\n                None=None\n            ) -> _Ref:\n        _utils.assert_operator_arity(op, v, w, 'bdd')\n        if u not in self:\n            raise ValueError(u)\n        if v is not None and v not in self:\n            raise ValueError(v)\n        if w is not None and w not in self:\n            raise ValueError(w)\n        # unary\n        if op in ('~', 'not', '!'):\n            return -u\n        # Implied by `assert_operator_arity()` above,\n        # present here for type-checking.\n        elif v is None:\n            raise ValueError(\n                '`v is None`')\n        # binary\n        elif op in ('or', r'\\/', '|', '||'):\n            return self.ite(u, 1, v)\n        elif op in ('and', '/\\\\', '&', '&&'):\n            return self.ite(u, v, -1)\n        elif op in ('#', 'xor', '^'):\n            return self.ite(u, -v, v)\n        elif op in ('=>', '->', 'implies'):\n            return self.ite(u, v, 1)\n        elif op in ('<=>', '<->', 'equiv'):\n            return self.ite(u, v, -v)\n        elif op in ('diff', '-'):\n            return self.ite(u, -v, -1)\n        elif op in (r'\\A', 'forall'):\n            raise NotImplementedError(\n                'quantification is not implemented for MDDs.')\n        elif op in (r'\\E', 'exists'):\n            raise NotImplementedError(\n                'quantification is not implemented for MDDs.')\n        # Implied by `assert_operator_arity()` above,\n        # present here for type-checking.\n        elif w is None:\n            raise ValueError(\n                '`w is None`')\n        # ternary\n        elif op == 'ite':\n            return self.ite(u, v, w)\n        raise ValueError(\n            f'unknown operator \"{op}\"')\n\n    def dump(\n            self,\n            fname:\n                str\n            ) -> None:\n        \"\"\"Write MDD as a diagram to PDF file `fname`.\n\n        @param fname:\n            file name, ending with the substring `.pdf`.\n\n        The diagram includes all nodes in the MDD.\n        The diagram is created using GraphViz.\n        \"\"\"\n        g = _to_dot(self)\n        if fname.endswith('.pdf'):\n            filetype = 'pdf'\n        elif fname.endswith('.dot'):\n            filetype = 'dot'\n        else:\n            raise ValueError(\n                f'unknown file extension: {fname}')\n        g.dump(\n            fname,\n            filetype=filetype)\n\n\ndef bdd_to_mdd(\n        bdd,\n        dvars:\n            dict[str, dict]\n        ) -> tuple[\n            MDD,\n            dict]:\n    \"\"\"Return MDD for given BDD.\n\n    Caution: collects garbage.\n\n    `dvars` must:\n    - map each MDD variable to\n      the corresponding bits in `bdd`\n    - give the order as \"level\" keys.\n    \"\"\"\n    # i = level in BDD\n    # j = level in MDD\n    # bit = BDD variable\n    # var = MDD variable\n    #\n    # map from bits to integers\n    bit_to_var = dict()\n    for var, d in dvars.items():\n        bits = d['bitnames']\n        b = {bit: var for bit in bits}\n        bit_to_var.update(b)\n    # find target bit order\n    order = list()  # target\n    levels = {d['level']: var for var, d in dvars.items()}\n    m = len(levels)\n    for j in range(m):\n        var = levels[j]\n        bits = dvars[var]['bitnames']\n        order.extend(bits)\n    bit_to_sort = {bit: k for k, bit in enumerate(order)}\n    # reorder\n    bdd.collect_garbage()\n    _bdd.reorder(bdd, order=bit_to_sort)\n    # BDD -> MDD\n    mdd = MDD(dvars)\n    # zones of bits per integer var\n    zones = dict()\n    for var, d in dvars.items():\n        bits = d['bitnames']\n        lsb = bits[0]\n        msb = bits[-1]\n        min_level = bit_to_sort[lsb]\n        max_level = bit_to_sort[msb]\n        zones[var] = (min_level, max_level)\n    # reverse edges\n    pred = {u: set() for u in bdd}\n    for u, (_, v, w) in bdd._succ.items():\n        if u <= 0:\n            raise AssertionError(u)\n        # terminal ?\n        if u == 1:\n            continue\n        # non-terminal\n        pred[abs(v)].add(u)\n        pred[abs(w)].add(u)\n    # find BDD nodes mentioned from above\n    rm = set()\n    for u, p in pred.items():\n        rc = bdd.ref(u)\n        k = len(p)  # number of predecessors\n        # has external refs ?\n        if rc > k:\n            continue\n        # has refs from outside zone ?\n        i, _, _ = bdd._succ[u]\n        bit = bdd.var_at_level(i)\n        var = bit_to_var[bit]\n        min_level, _ = zones[var]\n        pred_levels = {bdd._succ[v][0] for v in p}\n        min_pred_level = min(pred_levels)\n        if min_pred_level < min_level:\n            continue\n        # referenced only from inside zone\n        rm.add(u)\n    pred = {u: p for u, p in pred.items() if u not in rm}\n    # build layer by layer\n    # TODO: use bins, instad of iterating through all nodes\n    bdd.assert_consistent()\n    # _debug_dump(pred, bdd)\n    umap = dict()\n    umap[1] = 1\n    for u, i, v, w in bdd.levels(skip_terminals=True):\n        # ignore function ?\n        if u not in pred:\n            continue\n        # keep `u`\n        bit = bdd.var_at_level(i)\n        var = bit_to_var[bit]\n        bits = dvars[var]['bitnames']\n        bit_succ = list()\n        for d in _enumerate_integer(bits):\n            x = bdd.cofactor(u, d)\n            bit_succ.append(x)\n        # map edges\n        int_succ = [umap[abs(z)] if z > 0 else -umap[abs(z)]\n                    for z in bit_succ]\n        # add new MDD node at level j\n        j = dvars[var]['level']\n        r = mdd.find_or_add(j, *int_succ)\n        # cache\n        # signed r, because low never inverted,\n        # opposite to canonicity chosen for BDDs\n        umap[u] = r\n    return mdd, umap\n\n\ndef _enumerate_integer(\n        bits:\n            list[str]\n        ) -> _abc.Iterator[\n            dict[str, int]]:\n    n = len(bits)\n    for i in range(int(2**n)):\n        values = list(reversed(bin(i).lstrip('-0b').zfill(n)))\n        d = {bit: int(v) for bit, v in zip(bits, values)}\n        for bit in bits[len(values):]:\n            d[bit] = 0\n        yield d\n\n\ndef _debug_dump(\n        pred:\n            _abc.Iterable,\n        bdd\n        ) -> None:\n    \"\"\"Dump nodes of `bdd`, coloring nodes in `pred`.\"\"\"\n    g = _bdd._to_dot(bdd._succ, bdd)\n    color = 'red'\n    for u in pred:\n        if u < 1:\n            raise ValueError(u)\n        g.add_node(u, color=color)\n    for u in g.nodes:\n        if u < 1:\n            raise AssertionError(u)\n        if u == 1:\n            continue\n        level, _, _ = bdd._succ[u]\n        var = bdd.var_at_level(level)\n        label = f'{var}-{u}'\n        g.add_node(u, label=label)\n    g.dump(\n        'bdd_colored.pdf',\n        filetype='pdf')\n    bdd.dump('bdd.pdf')\n\n\ndef _to_dot(\n        mdd:\n            MDD\n        ) -> _utils.DotGraph:\n    g = _utils.DotGraph(\n        graph_type='digraph')\n    skeleton = list()\n    subgraphs = dict()\n    n = len(mdd.vars) + 1\n    for i in range(n):\n        h = _utils.DotGraph(\n            rank='same')\n        g.subgraphs.append(h)\n        subgraphs[i] = h\n        # add phantom node\n        u = f'\"-{i}\"'\n        skeleton.append(u)\n        h.add_node(\n            u,\n            label=str(i),\n            shape='none')\n    # auxiliary edges for ranking\n    for i, u in enumerate(skeleton[:-1]):\n        v = skeleton[i + 1]\n        g.add_edge(\n            u, v,\n            style='invis')\n    # add nodes\n    for u, t in mdd._succ.items():\n        if u <= 0:\n            raise AssertionError(u)\n        i = t[0]\n        nodes = t[1:]\n        # terminal ?\n        if nodes[0] is None:\n            var = '1'\n        else:\n            var = mdd.var_at_level(i)\n        # add node\n        label = f'{var}-{u}'\n        h = subgraphs[i]  # level i\n        h.add_node(\n            u,\n            label=label)\n        # add edges\n        if nodes[0] is None:\n            continue\n        # has successors\n        for j, v in enumerate(nodes):\n            label = str(j)\n            # tail_label = '-1' if v < 0 else ' '\n            if v < 0:\n                style = 'dashed'\n            else:\n                style = 'solid'\n            g.add_edge(\n                u, abs(v),\n                label=label,\n                style=style)\n    return g\n"
  },
  {
    "path": "dd/py.typed",
    "content": ""
  },
  {
    "path": "dd/sylvan.pyx",
    "content": "\"\"\"Cython interface to Sylvan.\n\n\nReference\n=========\n    Tom van Dijk, Alfons Laarman, Jaco van de Pol\n    \"Multi-Core BDD Operations for\n     Symbolic Reachability\"\n    PDMC 2012\n    <https://doi.org/10.1016/j.entcs.2013.07.009>\n    <https://github.com/utwente-fmt/sylvan>\n\"\"\"\n# Copyright 2016 by California Institute of Technology\n# All rights reserved. Licensed under BSD-3.\n#\nimport collections.abc as _abc\nimport logging\nimport pickle\nimport signal\nimport time\nimport typing as _ty\n\nfrom cpython cimport bool as _py_bool\nfrom libcpp cimport bool\n\nimport dd._abc as _dd_abc\nfrom dd import _parser\nfrom dd import bdd as _bdd\nfrom dd cimport c_sylvan as sy\nimport dd._utils as _utils\n\n\nlogger = logging.getLogger(__name__)\n\n\n_Yes: _ty.TypeAlias = _py_bool\n_Nat: _ty.TypeAlias = _dd_abc.Nat\n_Cardinality: _ty.TypeAlias = _dd_abc.Cardinality\n_VariableName: _ty.TypeAlias = _dd_abc.VariableName\n_Level: _ty.TypeAlias = _dd_abc.Level\n_VariableLevels: _ty.TypeAlias = _dd_abc.VariableLevels\n_Assignment: _ty.TypeAlias = _dd_abc.Assignment\n_Renaming: _ty.TypeAlias = _dd_abc.Renaming\n_Formula: _ty.TypeAlias = _dd_abc.Formula\n\n\n# TODO: check for invalid nodes returned by sylvan calls\n\n\ncdef class BDD:\n    \"\"\"Wrapper of Sylvan manager.\n\n    Interface similar to `dd.bdd.BDD`.\n    Variable names are strings.\n    Attributes:\n\n      - `vars`: `set` of bit names as strings\n    \"\"\"\n\n    cdef public object vars\n    cdef public object _index_of_var\n    cdef public object _var_with_index\n\n    def __cinit__(\n            self\n            ) -> None:\n        \"\"\"Initialize BDD manager.\n\n        Due to the architecture of `sylvan`,\n        there is a single unique table,\n        so you can create only one `BDD` instance at\n        any given time.\n\n        If the current `BDD` instance is `del`eted,\n        then a new `BDD` instance can be created.\n        But two `BDD` instances cannot coexist in\n        the same process.\n        \"\"\"\n        sy.lace_init(0, 10**6)\n        sy.lace_startup(0, NULL, NULL)\n        sy.LACE_ME_WRAP\n        sy.sylvan_init_package(1LL<<25, 1LL<<26, 1LL<<24, 1LL<<25)\n        sy.sylvan_init_bdd(1)\n\n    def __init__(\n            self,\n            memory_estimate=None,\n            initial_cache_size=None\n            ) -> None:\n        # self.configure(reordering=True, max_cache_hard=MAX_CACHE)\n        self.vars = set()\n        self._index_of_var = dict()  # map: str -> unique fixed int\n        self._var_with_index = dict()\n\n    def __dealloc__(\n            self\n            ) -> None:\n        # n = len(self)\n        # if n != 0:\n        #     raise AssertionError(\n        #         f'Still {n} nodes '\n        #         'referenced upon shutdown.')\n        sy.LACE_ME_WRAP\n        sy.sylvan_quit()\n        sy.lace_exit()\n\n    def __eq__(\n            self:\n                BDD,\n            other:\n                BDD |\n                None\n            ) -> _Yes:\n        \"\"\"Return `True` if `other` has same manager.\n\n        If `other is None`, then return `False`.\n        \"\"\"\n        if other is None:\n            return False\n        # `sylvan` supports one manager only\n        return True\n\n    def __ne__(\n            self:\n                BDD,\n            other:\n                BDD |\n                None\n            ) -> _Yes:\n        \"\"\"Return `True` if `other` has different manager.\n\n        If `other is None`, then return `True`.\n        \"\"\"\n        if other is None:\n            return True\n        # `sylvan` supports one manager only\n        return False\n\n    def __len__(\n            self\n            ) -> _Cardinality:\n        \"\"\"Return number of nodes with non-zero references.\"\"\"\n        sy.LACE_ME_WRAP\n        return sy.sylvan_count_refs()\n\n    def __contains__(\n            self,\n            u:\n                Function\n            ) -> _Yes:\n        if self is not u.bdd:\n            raise ValueError(u)\n        try:\n            self.apply('not', u)\n            return True\n        except:\n            return False\n\n    def __str__(\n            self\n            ) -> str:\n        return 'Binary decision diagram (Sylvan wrapper)'\n\n    def configure(\n            self,\n            **kw\n            ) -> dict[\n                str,\n                _ty.Any]:\n        \"\"\"Has no effect, present for compatibility only.\n\n        Compatibility here refers to `BDD` classes in\n        other modules of the package `dd`.\n        \"\"\"\n        # TODO: use `sy.gc_enabled == 1` when not `static`\n        garbage_collection = None\n        d = dict(\n            reordering=False,\n            garbage_collection=garbage_collection,\n            max_memory=None,\n            loose_up_to=None,\n            max_cache_soft=None,\n            max_cache_hard=None,\n            min_hit=None,\n            max_growth=None,\n            max_swaps=None,\n            max_vars=None)\n        return d\n\n    cpdef tuple succ(\n            self,\n            u:\n                Function):\n        \"\"\"Return `(level, low, high)` for `u`.\"\"\"\n        i = u._index  # level, assuming\n            # static variable order\n        v = u.low\n        w = u.high\n        # account for complement bit propagation\n        if u.negated:\n            v, w = ~ v, ~ w\n        return i, v, w\n\n    cdef incref(\n            self,\n            u:\n                sy.BDD):\n        sy.sylvan_ref(u)\n\n    cdef decref(\n            self,\n            u:\n                sy.BDD):\n        sy.sylvan_deref(u)\n\n    def declare(\n            self,\n            *variables:\n                _VariableName\n            ) -> None:\n        \"\"\"Add names in `variables` to `self.vars`.\"\"\"\n        for var in variables:\n            self.add_var(var)\n\n    cpdef int add_var(\n            self,\n            var:\n                _VariableName,\n            index:\n                int |\n                None=None):\n        \"\"\"Return index of variable named `var`.\n\n        If a variable named `var` exists,\n        the assert that it has `index`.\n        Otherwise, create a variable named `var`\n        with `index` (if given).\n\n        If no reordering has yet occurred,\n        then the returned index equals the level,\n        provided `add_var` has been used so far.\n        \"\"\"\n        sy.LACE_ME_WRAP\n        # var already exists ?\n        j = self._index_of_var.get(var)\n        if j is not None:\n            if not (j == index or index is None):\n                raise AssertionError(j, index)\n            return j\n        # new var\n        if index is None:\n            index = len(self._index_of_var)\n        j = index\n        u = sy.sylvan_ithvar(j)\n        if u == sy.sylvan_invalid:\n            raise RuntimeError(\n                f'failed to add var \"{var}\"')\n        self._add_var(var, j)\n        return j\n\n    cpdef insert_var(\n            self,\n            var:\n                _VariableName,\n            level:\n                _Level):\n        \"\"\"Create a new variable named `var`, at `level`.\"\"\"\n        raise Exception(\n            'in `sylvan`, variable indices equal levels.\\n'\n            'Call method `BDD.add_var` instead.')\n\n    cdef _add_var(\n            self,\n            var:\n                _VariableName,\n            index:\n                int):\n        \"\"\"Add to `self` a *new* variable named `var`.\"\"\"\n        if var in self.vars:\n            raise ValueError((var, self.vars))\n        if var in self._index_of_var:\n            raise ValueError((var, self._index_of_var))\n        if index in self._var_with_index:\n            raise ValueError((index, self._var_with_index))\n        self.vars.add(var)\n        self._index_of_var[var] = index\n        self._var_with_index[index] = var\n        if (len(self._index_of_var) !=\n                len(self._var_with_index)):\n            raise AssertionError((\n                len(self._index_of_var),\n                len(self._var_with_index),\n                self._index_of_var,\n                self._var_with_index))\n\n    cpdef Function var(\n            self,\n            var:\n                _VariableName):\n        \"\"\"Return node for variable named `var`.\"\"\"\n        if var not in self._index_of_var:\n            raise ValueError(\n                f'undeclared variable \"{var}\", '\n                'the declared variables are:\\n'\n                f'{self._index_of_var}')\n        sy.LACE_ME_WRAP\n        j = self._index_of_var[var]\n        r = sy.sylvan_ithvar(j)\n        return wrap(self, r)\n\n    def var_at_level(\n            self,\n            level:\n                _Level\n            ) -> _VariableName:\n        \"\"\"Return name of variable at `level`.\"\"\"\n        j = level  # indices equal levels in `sylvan`\n        if j not in self._var_with_index:\n            levels = {\n                var: self.level_of_var(var)\n                for var in self._index_of_var}\n            raise ValueError(\n                f'no variable has level:  {level}, '\n                'the current levels of all variables '\n                f'are:  {levels}')\n        var = self._var_with_index[j]\n        return var\n\n    def level_of_var(\n            self,\n            var:\n                _VariableName\n            ) -> _Level:\n        \"\"\"Return level of variable named `var`.\"\"\"\n        if var not in self._index_of_var:\n            raise ValueError(\n                f'undeclared variable \"{var}\", '\n                'the declared variables are:'\n                f'\\n{self._index_of_var}')\n        j = self._index_of_var[var]\n        level = j\n        return level\n\n    cpdef set support(\n            self,\n            f:\n                Function):\n        \"\"\"Return the variables that node `f` depends on.\"\"\"\n        if self is not f.bdd:\n            raise ValueError(f)\n        sy.LACE_ME_WRAP\n        cube: sy.BDD\n        cube = sy.sylvan_support(f.node)\n        ids = set()\n        while cube != sy.sylvan_true:\n            # get var\n            j = sy.sylvan_var(cube)\n            ids.add(j)\n            # descend\n            u = sy.sylvan_low(cube)\n            v = sy.sylvan_high(cube)\n            if u != sy.sylvan_false:\n                raise AssertionError(u)\n            cube = v\n        support = {self._var_with_index[j] for j in ids}\n        return support\n\n    cpdef Function let(\n            self,\n            definitions:\n                _Renaming |\n                _Assignment |\n                dict[_VariableName, Function],\n            u:\n                Function):\n        \"\"\"Replace variables with `definitions` in `u`.\"\"\"\n        d = definitions\n        if not d:\n            logger.warning(\n                'Call to `BDD.let` with no effect: '\n                '`defs` is empty.')\n            return u\n        var = next(iter(d))\n        value = d[var]\n        if isinstance(value, _py_bool):\n            return self._cofactor(u, d)\n        elif isinstance(value, Function):\n            return self._compose(u, d)\n        try:\n            value + 's'\n        except TypeError:\n            raise ValueError(\n                'Key must be variable name as `str`, '\n                'or Boolean value as `bool`, '\n                f'or BDD node as `int`. Got: {value}')\n        return self._rename(u, d)\n\n    cpdef Function _compose(\n            self,\n            u:\n                Function,\n            var_sub:\n                dict[_VariableName, Function]):\n        if self is not u.bdd:\n            raise ValueError(u)\n        sy.LACE_ME_WRAP\n        map: sy.BDDMAP\n        j: sy.BDDVAR\n        r: sy.BDD\n        g: Function\n        map = sy.sylvan_map_empty()\n        for var, g in var_sub.items():\n            j = self._index_of_var[var]\n            map = sy.sylvan_map_add(map, j, g.node)\n        r = sy.sylvan_compose(u.node, map)\n        return wrap(self, r)\n\n    cpdef Function _cofactor(\n            self,\n            u:\n                Function,\n            values:\n                _Assignment):\n        \"\"\"Return the cofactor f|_g.\"\"\"\n        var_sub = {\n            var: self.true if value else self.false\n            for var, value in values.items()}\n        return self._compose(u, var_sub)\n\n    cpdef Function _rename(\n            self,\n            u:\n                Function,\n            dvars:\n                _Renaming):\n        \"\"\"Return node `u` after renaming variables in `dvars`.\"\"\"\n        if self is not u.bdd:\n            raise ValueError(u)\n        var_sub = {\n            var: self.var(sub) for var, sub in dvars.items()}\n        r = self._compose(u, var_sub)\n        return r\n\n    def pick(\n            self,\n            u:\n                Function,\n            care_vars:\n                _abc.Collection[\n                    _VariableName] |\n                None=None\n            ) -> _Assignment:\n        \"\"\"Return a single assignment as `dict`.\"\"\"\n        return next(self.pick_iter(u, care_vars), None)\n\n    def pick_iter(\n            self,\n            u:\n                Function,\n            care_vars:\n                _abc.Collection[\n                    _VariableName] |\n                None=None\n            ) -> _abc.Iterable[\n                _Assignment]:\n        \"\"\"Return generator over assignments.\"\"\"\n        support = self.support(u)\n        if care_vars is None:\n            care_vars = support\n        missing = {v for v in support if v not in care_vars}\n        if missing:\n            logger.warning(\n                'Missing bits:  '\n                f'support - care_vars = {missing}')\n        cube = dict()\n        value = True\n        config = self.configure(reordering=False)\n        for cube in self._sat_iter(u, cube, value, support):\n            for m in _bdd._enumerate_minterms(cube, care_vars):\n                yield m\n        self.configure(reordering=config['reordering'])\n\n    def _sat_iter(\n            self,\n            u:\n                Function,\n            cube:\n                _Assignment,\n            value:\n                _py_bool,\n            support:\n                set[_VariableName]\n            ) -> _Assignment:\n        \"\"\"Recurse to enumerate models.\"\"\"\n        if u.negated:\n            value = not value\n        # terminal ?\n        if u.var is None:\n            # high nodes are negated\n            # the constant node is 0\n            if not value:\n                if not set(cube).issubset(support):\n                    raise AssertionError(\n                        set(cube).difference(support))\n                yield cube\n            return\n        # non-terminal\n        _, v, w = self.succ(u)\n        var = u.var\n        d0 = dict(cube)\n        d0[var] = False\n        d1 = dict(cube)\n        d1[var] = True\n        for x in self._sat_iter(v, d0, value, support):\n            yield x\n        for x in self._sat_iter(w, d1, value, support):\n            yield x\n\n    cpdef int count(\n            self,\n            u:\n                Function):\n        \"\"\"Return number of models of node `u`.\"\"\"\n        sy.LACE_ME_WRAP\n        support = self.support(u)\n        cube = self.cube(support)\n        n_models = sy.sylvan_satcount(\n            u.node, cube.node)\n        if n_models < 0:\n            raise AssertionError(n_models)\n        return int(n_models)\n\n    cpdef Function ite(\n            self,\n            g:\n                Function,\n            u:\n                Function,\n            v:\n                Function):\n        if self is not g.bdd:\n            raise ValueError(g)\n        if self is not u.bdd:\n            raise ValueError(u)\n        if self is not v.bdd:\n            raise ValueError(v)\n        sy.LACE_ME_WRAP\n        r: sy.BDD\n        r = sy.sylvan_ite(g.node, u.node, v.node)\n        return wrap(self, r)\n\n    cpdef Function apply(\n            self,\n            op:\n                _dd_abc.OperatorSymbol,\n            u:\n                Function,\n            v:\n                _ty.Optional[Function]\n                =None,\n            w:\n                _ty.Optional[Function]\n                =None):\n        \"\"\"Return as `Function` the result of applying `op`.\"\"\"\n        _utils.assert_operator_arity(op, v, w, 'bdd')\n        if self is not u.bdd:\n            raise ValueError(u)\n        if v is not None and v.bdd is not self:\n            raise ValueError(v)\n        if w is not None and w.bdd is not self:\n            raise ValueError(w)\n        r: sy.BDD\n        sy.LACE_ME_WRAP\n        # unary\n        if op in ('~', 'not', '!'):\n            r = sy.sylvan_not(u.node)\n        # binary\n        elif op in ('and', '/\\\\', '&', '&&'):\n            r = sy.sylvan_and(u.node, v.node)\n        elif op in ('or', r'\\/', '|', '||'):\n            r = sy.sylvan_or(u.node, v.node)\n        elif op in ('#', 'xor', '^'):\n            r = sy.sylvan_xor(u.node, v.node)\n        elif op in ('=>', '->', 'implies'):\n            r = sy.sylvan_imp(u.node, v.node)\n        elif op in ('<=>', '<->', 'equiv'):\n            r = sy.sylvan_biimp(u.node, v.node)\n        elif op in ('diff', '-'):\n            r = sy.sylvan_diff(u.node, v.node)\n        elif op in (r'\\A', 'forall'):\n            r = sy.sylvan_forall(u.node, v.node)\n        elif op in (r'\\E', 'exists'):\n            r = sy.sylvan_exists(u.node, v.node)\n        elif op == 'ite':\n            r = sy.sylvan_ite(u.node, v.node, w.node)\n        else:\n            raise AssertionError(op)\n        if r == sy.sylvan_invalid:\n            raise ValueError(\n                f'unknown operator: \"{op}\"')\n        return wrap(self, r)\n\n    cpdef Function cube(\n            self,\n            dvars:\n                _Assignment |\n                set[_VariableName]):\n        \"\"\"Return node for cube over `dvars`.\n\n        @param dvars:\n            maps each variable to a `bool`.\n            If `set` given, then all values assumed `True`.\n        \"\"\"\n        # TODO: call sylvan cube function\n        u = self.true\n        if isinstance(dvars, set):\n            for var in dvars:\n                u &= self.var(var)\n            return u\n        for var, sign in dvars.items():\n            v = self.var(var)\n            if sign is False:\n                v = ~v\n            u &= v\n        return u\n\n    cpdef Function quantify(\n            self,\n            u:\n                Function,\n            qvars:\n                _abc.Iterable[\n                    _VariableName],\n            forall:\n                _Yes=False):\n        \"\"\"Abstract variables `qvars` from node `u`.\"\"\"\n        if self is not u.bdd:\n            raise ValueError(u)\n        sy.LACE_ME_WRAP\n        c = set(qvars)\n        cube = self.cube(c)\n        # quantify\n        if forall:\n            r = sy.sylvan_forall(u.node, cube.node)\n        else:\n            r = sy.sylvan_exists(u.node, cube.node)\n        return wrap(self, r)\n\n    cpdef Function forall(\n            self,\n            qvars:\n                _abc.Iterable[\n                    _VariableName],\n            u:\n                Function):\n        \"\"\"Quantify `qvars` in `u` universally.\n\n        Wraps method `quantify` to be more readable.\n        \"\"\"\n        return self.quantify(u, qvars, forall=True)\n\n    cpdef Function exist(\n            self,\n            qvars:\n                _abc.Iterable[\n                    _VariableName],\n            u:\n                Function):\n        \"\"\"Quantify `qvars` in `u` existentially.\n\n        Wraps method `quantify` to be more readable.\n        \"\"\"\n        return self.quantify(u, qvars, forall=False)\n\n    cpdef assert_consistent(\n            self):\n        \"\"\"Raise `AssertionError` if not consistent.\"\"\"\n        # c = Cudd_DebugCheck(self.manager)\n        # if c != 0:\n        #     raise AssertionError(c)\n        n = len(self.vars)\n        m = len(self._var_with_index)\n        k = len(self._index_of_var)\n        if n != m:\n            raise AssertionError((n, m))\n        if m != k:\n            raise AssertionError((m, k))\n\n    def add_expr(\n            self,\n            expr:\n                _Formula\n            ) -> Function:\n        \"\"\"Return node for `str` expression `e`.\"\"\"\n        return _parser.add_expr(expr, self)\n\n    def to_expr(\n            self,\n            u:\n                Function\n            ) -> _Formula:\n        if self is not u.bdd:\n            raise ValueError(u)\n        raise NotImplementedError()\n\n    cpdef dump(\n            self,\n            u:\n                Function,\n            fname:\n                str):\n        \"\"\"Dump BDD as DDDMP file `fname`.\"\"\"\n        if self is not u.bdd:\n            raise ValueError(u)\n        raise NotImplementedError()\n\n    cpdef load(\n            self,\n            fname:\n                str):\n        \"\"\"Return `Function` loaded from file `fname`.\"\"\"\n        raise NotImplementedError()\n\n    @property\n    def false(\n            self\n            ) -> Function:\n        \"\"\"`Function` for Boolean value false.\"\"\"\n        return self._bool(False)\n\n    @property\n    def true(\n            self\n            ) -> Function:\n        \"\"\"`Function` for Boolean value true.\"\"\"\n        return self._bool(True)\n\n    cdef Function _bool(\n            self,\n            v:\n                _py_bool):\n        \"\"\"Return terminal node for Boolean `v`.\"\"\"\n        r: sy.BDD\n        if v:\n            r = sy.sylvan_true\n        else:\n            r = sy.sylvan_false\n        return wrap(self, r)\n\n\ncpdef Function restrict(\n        u:\n            Function,\n        care_set:\n            Function):\n    if u.bdd is not care_set.bdd:\n        raise ValueError((u, care_set))\n    sy.LACE_ME_WRAP\n    r: sy.BDD\n    r = sy.sylvan_restrict(u.node, care_set.node)\n    return wrap(u.bdd, r)\n\n\ncpdef Function and_exists(\n        u:\n            Function,\n        v:\n            Function,\n        qvars:\n            set[_VariableName]):\n    r\"\"\"Return `\\E qvars:  u /\\ v`.\"\"\"\n    if u.bdd is not v.bdd:\n        raise ValueError((u, v))\n    bdd = u.bdd\n    sy.LACE_ME_WRAP\n    cube = bdd.cube(qvars)\n    r = sy.sylvan_and_exists(u.node, v.node, cube.node)\n    return wrap(u.bdd, r)\n\n\ncpdef Function or_forall(\n        u:\n            Function,\n        v:\n            Function,\n        qvars:\n            set[_VariableName]):\n    r\"\"\"Return `\\A qvars:  u \\/ v`.\"\"\"\n    if u.bdd is not v.bdd:\n        raise ValueError((u, v))\n    bdd = u.bdd\n    sy.LACE_ME_WRAP\n    cube = bdd.cube(qvars)\n    r = sy.sylvan_and_exists(\n        sy.sylvan_not(u.node),\n        sy.sylvan_not(v.node),\n        cube.node)\n    r = sy.sylvan_not(r)\n    return wrap(u.bdd, r)\n\n\ncpdef reorder(\n        bdd:\n            BDD,\n        dvars:\n            _VariableLevels |\n            None=None):\n    \"\"\"Reorder `bdd` to order in `dvars`.\n\n    If `dvars` is `None`, then invoke group sifting.\n    \"\"\"\n    raise NotImplementedError\n\n\ndef copy_vars(\n        source:\n            BDD,\n        target:\n            BDD\n        ) -> None:\n    \"\"\"Copy variables, preserving Sylvan indices.\"\"\"\n    for var, index in source._index_of_var.items():\n        target.add_var(var, index=index)\n\n\ncdef Function wrap(\n        bdd:\n            BDD,\n        node:\n            sy.BDD):\n    \"\"\"Return a `Function` that wraps `node`.\"\"\"\n    f = Function()\n    f.init(node, bdd)\n    return f\n\n\ncdef class Function:\n    \"\"\"Wrapper of `BDD` from Sylvan.\n\n    Attributes (those that are properties are\n    described in their docstrings):\n\n    - `_index`\n    - `var`\n    - `low`\n    - `high`\n    - `negated`\n    - `support`\n    - `dag_size`\n\n    In Python, use as:\n\n    ```python\n    from dd import sylvan\n\n    bdd = sylvan.BDD()\n    u = bdd.true\n    v = bdd.false\n    w = u | ~ v\n    ```\n\n    In Cython, use as:\n\n    ```cython\n    bdd = BDD()\n    u: sy.BDD\n    u = sylvan_true\n    f = Function()\n    f.init(u)\n    ```\n    \"\"\"\n\n    __weakref__: object\n    cdef public BDD bdd\n    node: sy.BDD\n\n    cdef init(\n            self,\n            u:\n                sy.BDD,\n            bdd:\n                BDD):\n        if u == sy.sylvan_invalid:\n            raise ValueError(\n                '`sy.BDD u` is `NULL` pointer.')\n        self.bdd = bdd\n        self.node = u\n        sy.sylvan_ref(u)\n\n    def __hash__(\n            self\n            ) -> int:\n        return int(self)\n\n    @property\n    def _index(\n            self\n            ) -> _Nat:\n        \"\"\"Index of `self.node`.\"\"\"\n        return sy.sylvan_var(self.node)\n\n    @property\n    def var(\n            self\n            ) -> _VariableName:\n        \"\"\"Variable at level where this node is.\"\"\"\n        if sy.sylvan_isconst(self.node):\n            return None\n        return self.bdd._var_with_index[self._index]\n\n    @property\n    def level(\n            self\n            ) -> _Level:\n        \"\"\"Level where this node is.\"\"\"\n        raise NotImplementedError\n\n    @property\n    def ref(\n            self\n            ) -> _Cardinality:\n        \"\"\"Sum of reference counts of node and its negation.\"\"\"\n        # u = Cudd_Regular(self.node)\n        # return u.ref\n        raise NotImplementedError\n\n    @property\n    def low(\n            self\n            ) -> Function:\n        \"\"\"Return \"else\" node as `Function`.\"\"\"\n        # propagates complement bit\n        u = sy.sylvan_low(self.node)\n        return wrap(self.bdd, u)\n\n    @property\n    def high(\n            self\n            ) -> Function:\n        \"\"\"Return \"then\" node as `Function`.\"\"\"\n        u = sy.sylvan_high(self.node)\n        return wrap(self.bdd, u)\n\n    @property\n    def negated(\n            self\n            ) -> _Yes:\n        \"\"\"Return `True` if `self` is a complemented edge.\"\"\"\n        # read the definition of `BDD_HASMARK`\n        # in `sylvan_bdd`\n        if self.node & sy.sylvan_complement:\n            return True\n        else:\n            return False\n\n    @property\n    def support(\n            self\n            ) -> set[_VariableName]:\n        \"\"\"Return `set` of variables in support.\"\"\"\n        return self.bdd.support(self)\n\n    def __dealloc__(\n            self\n            ) -> None:\n        sy.sylvan_deref(self.node)\n        self.node = 0\n\n    def __str__(\n            self\n            ) -> str:\n        return (\n            'Function(DdNode with: '\n            f'node={self.node}, '\n            f'var_index={self._index}, '\n            f'ref_count={None})')\n\n    def __len__(\n            self\n            ) -> _Cardinality:\n        return sy.sylvan_nodecount(self.node)\n\n    @property\n    def dag_size(\n            self\n            ) -> _Cardinality:\n        \"\"\"Return number of BDD nodes.\n\n        This is the number of BDD nodes that\n        are reachable from this BDD reference,\n        i.e., with `self` as root.\n        \"\"\"\n        return len(self)\n\n    def __eq__(\n            self:\n                Function,\n            other:\n                Function |\n                None\n            ) -> _Yes:\n        if other is None:\n            return False\n        other_: Function = other\n        if self.bdd is not other_.bdd:\n            raise ValueError((self, other_))\n        return self.node == other_.node\n\n    def __ne__(\n            self:\n                Function,\n            other:\n                Function |\n                None\n            ) -> _Yes:\n        if other is None:\n            return True\n        other_: Function = other\n        if self.bdd is not other_.bdd:\n            raise ValueError((self, other_))\n        return self.node != other_.node\n\n    def __invert__(\n            self:\n                Function\n            ) -> Function:\n        r = sy.sylvan_not(self.node)\n        return wrap(self.bdd, r)\n\n    def __and__(\n            self:\n                Function,\n            other:\n                Function\n            ) -> Function:\n        if self.bdd is not other.bdd:\n            raise ValueError((self, other))\n        sy.LACE_ME_WRAP\n        r = sy.sylvan_and(self.node, other.node)\n        return wrap(self.bdd, r)\n\n    def __or__(\n            self:\n                Function,\n            other:\n                Function\n            ) -> Function:\n        if self.bdd is not other.bdd:\n            raise ValueError((self, other))\n        sy.LACE_ME_WRAP\n        r = sy.sylvan_or(self.node, other.node)\n        return wrap(self.bdd, r)\n\n    def __xor__(\n            self:\n                Function,\n            other:\n                Function\n            ) -> Function:\n        if self.bdd is not other.bdd:\n            raise ValueError((self, other))\n        sy.LACE_ME_WRAP\n        r = sy.sylvan_xor(self.node, other.node)\n        return wrap(self.bdd, r)\n\n    def count(\n            self:\n                Function\n            ) -> _Cardinality:\n        return self.bdd.count(self)\n"
  },
  {
    "path": "doc.md",
    "content": "# `dd` documentation\n\n\n# Table of Contents\n\n- [Design principles](#design-principles)\n- [Create and plot a binary decision diagram](#create-and-plot-a-binary-decision-diagram)\n    - [Using a `BDD` manager](#using-a-bdd-manager)\n    - [Plotting](#plotting)\n    - [Alternatives](#alternatives)\n    - [Reminders about the implementation beneath](#reminders-about-the-implementation-beneath)\n    - [Pickle](#pickle)\n    - [Nodes as `Function` objects](#nodes-as-function-objects)\n        - [BDD equality](#bdd-equality)\n        - [Other methods](#other-methods)\n- [CUDD interface: `dd.cudd`](#cudd-interface-ddcudd)\n    - [Functions](#functions)\n- [Lower level: `dd.bdd`](#lower-level-ddbdd)\n    - [Reference counting](#reference-counting)\n    - [Reordering](#reordering)\n    - [Other methods](#other-methods)\n- [Example: Reachability analysis](#example-reachability-analysis)\n- [Syntax for quantified Boolean formulas](#syntax-for-quantified-boolean-formulas)\n- [Multi-valued decision diagrams (MDD)](#multi-valued-decision-diagrams-mdd)\n- [Installation of C extension modules](#installation-of-c-extension-modules)\n    - [Environment variables that activate C extensions](#environment-variables-that-activate-c-extensions)\n    - [Alternative: directly running `setup.py`](#alternative-directly-running-setuppy)\n    - [Using the package `build`](#using-the-package-build)\n    - [Customizing the C compilation](#customizing-the-c-compilation)\n- [Installing the development version](#installing-the-development-version)\n- [Footnotes](#footnotes)\n- [Copying](#copying)\n\n\n## Design principles\n\nThe interface is in Python. The representation depends on what you want and\nhave installed. For solving small to medium size problems, say for teaching,\nor prototyping new algorithms, pure Python can be more convenient.\nTo work with larger problems, it works better if you install the C library\n[CUDD](https://web.archive.org/web/http://vlsi.colorado.edu/~fabio/CUDD/html/index.html).\nLet's call these “backends”.\n\nThe same user code can run with both the Python and C backends.\nYou only need to modify an `import dd.autoref as _bdd` to\n`import dd.cudd as _bdd`, or import the best available interface:\n\n```python\ntry:\n    import dd.cudd as _bdd\nexcept ImportError:\n    import dd.autoref as _bdd\n```\n\nThe following sections describe how to use the high level interface\n(almost identical in `autoref` and `cudd`). The lower level interface to\nthe pure-Python implementation in `dd.bdd` is also described,\nfor those interested in more details.\n\n\n## Create and plot a binary decision diagram\n\nThe starting point for using a BDD library is a *shared* reduced ordered\nbinary decision diagram. Implementations call this *manager*.\nThe adjectives mean:\n\n- binary: each node represents a propositional formula\n- ordered: variables have a fixed order that changes in a controlled way\n- reduced: given variable order, equivalent propositional formulas are\n  represented by a unique diagram\n- shared: common subformulas are represented using common elements in memory.\n\nThe manager is a directed graph, with each node representing a formula.\nEach formula can be understood as a collection of assignments to variables.\nAs mentioned above, the variables are ordered.\nThe *level* of a variable is its index in this order, starting from 0.\nThe *terminal* nodes correspond to `TRUE` and `FALSE`, with maximal index.\n\nEach manager is a `BDD` class, residing in a different module:\n\n- `dd.autoref.BDD`: high-level interface to pure Python implementation\n- `dd.cudd.BDD`: same high-level interface to a C implementation\n- `dd.sylvan.BDD`: interface to another C implementation (multi-core)\n- `dd.bdd.BDD`: low-level interface to pure Python implementation\n  (wrapped by `dd.autoref.BDD`).\n\nThe main difference between these modules is how a BDD node is represented:\n\n- `autoref`: `autoref.Function`\n- `cudd`: `cudd.Function`\n- `sylvan`: `sylvan.Function`\n- `bdd`: `int`\n\nIn `autoref` and `cudd`, a `Function` class represents a node.\nIn `bdd`, a signed integer represents a node.\n\nAll implementations use negated edges,\nso logical negation takes *constant* time.\n\n\n### Using a `BDD` manager\n\n\nRoughly four kinds of operations suffice to perform most tasks:\n\n- creating BDDs from formulas\n- quantification (`forall`, `exist`)\n- substitution (`let`)\n- enumerating models that satisfy a BDD\n\nFirst, instantiate a manager and declare variables\n\n```python\nimport dd.autoref as _bdd\n\n\nbdd = _bdd.BDD()\nbdd.declare('x', 'y', 'z')\n```\n\nTo create a BDD node for a propositional formula, call the parser\n\n```python\nu = bdd.add_expr(r'x /\\ y')  # conjunction\nv = bdd.add_expr(r'z \\/ ~ y')  # disjunction and negation\nw = u & ~ v\n```\n\nThe formulas above are in [TLA+](https://en.wikipedia.org/wiki/TLA%2B) syntax.\nIf you prefer the syntax `&, |, !`, the parser recognizes those operators too.\nThe inverse of `BDD.add_expr` is `BDD.to_expr`:\n\n```python\ns = bdd.to_expr(u)\nprint(s)\n    # 'ite(x, y, False)'\n```\n\nLets create the BDD of a more colorful Boolean formula\n\n```python\ns = r'(x /\\ y) <=> (~ z \\/ ~ (y <=> x))'\nv = bdd.add_expr(s)\n```\n\nIn natural language, the expression `s` reads:\n“(x and y) if and only if ( (not z) or (y xor x) )”.\nThe Boolean constants are `bdd.false` and `bdd.true`, and in syntax\n`FALSE` and `TRUE`. The available operators are listed in the\n[Syntax for quantified Boolean formulas](#syntax-for-quantified-boolean-formulas)\nsection.\n\nVariables can be quantified by calling the methods `exist` and `forall`. They\nsymbolically consider all values of the variables mentioned in the\nquantification, and these variables are therefore not present in the result.\n\n```python\nu = bdd.add_expr(r'x /\\ y')\nv = bdd.exist(['x'], u)\nprint(v.to_expr())\n    # 'y'\n```\n\nExistential quantification can also be done by writing quantified formulas\n\n```python\n# there exists a value of x, such that (x and y)\nu = bdd.add_expr(r'\\E x:  x /\\ y')\ny = bdd.add_expr('y')\nassert u == y, (u, y)\n\n# forall x, there exists y, such that (y or x)\nu = bdd.add_expr(r'\\A x:  \\E y:  y \\/ z')\nassert u == bdd.true, u\n```\n\n`dd` supports \"inline BDD references\" via the `@` operator. Each BDD node `u`\nhas an integer representation `int(u)` and a string representation `str(u)`.\nFor example, if the integer representation is `5`, then the string\nrepresentation is `@5`. These enable you to mention existing BDD nodes\nin formulas, without the need to expand them as formulas. For example:\n\n```python\nu = bdd.add_expr(r'y \\/ z')\ns = rf'x /\\ {u}'\nv = bdd.add_expr(s)\nv_ = bdd.add_expr(r'x /\\ (y \\/ z)')\nassert v == v_\n```\n\nSubstitution comes in several forms:\n\n- replace some variable names by other variable names\n- replace some variable names by Boolean constants\n- replace some variable names by BDD nodes, so by arbitrary formulas\n\nAll these kinds of substitution are performed via the method `let`,\nwhich takes a `dict` that maps the variable names to replacements.\nTo substitute some variables for some other variables\n\n```python\nbdd.declare('x', 'p', 'y', 'q', 'z')\nu = bdd.add_expr(r'x  \\/  (y /\\ z)')\n# substitute variables for variables (rename)\nd = dict(x='p', y='q')\nv = bdd.let(d, u)\nprint(f'support = {v.support}')\n    # support = {'p', 'q', 'z'}\n```\n\nThe other forms of substitution are similar\n\n```python\n# substitute constants for variables (cofactor)\nvalues = dict(x=True, y=False)\nv = bdd.let(values, u)\n# substitute BDDs for variables (compose)\nd = dict(x=bdd.add_expr(r'z \\/ w'))\nv = bdd.let(d, u)\n```\n\nReplacement variables are (conceptually) inserted in-place in the expression.\nIf the expression also contains other occurrences of the replacements (like\nvariable `p` in the example below), the result may not be what is expected:\n\n```python\nbdd.declare('x', 'p')\nu = bdd.add_expr(r'x /\\ ~ p')\nd = dict(x='p')  # rename `x` to `p`\nv = bdd.let(d, u)  # equivalent to `p /\\ ~ p`, which is `FALSE`\nassert v == bdd.false\n```\n\nTo rename variables without side-effects like above, it is useful to first\nremove all occurrences of the replacement variables in the expression.\nDepending on the algorithm being implemented, it might be appropriate\nto first rename variable `p` above before the substitution, or\nto quantify it.\n\nA BDD represents a formula, a syntactic object. Semantics is about how\nsyntax is used to describe the world. We could interpret the same formula\nusing different semantics and reach different conclusions.\n\nA formula is usually understood as describing some assignments of values to\nvariables. Such an assignment is also called a *model*.\nA model is represented as a `dict` that maps variable names to values.\n\n```python\nu = bdd.add_expr(r'x \\/ y')\nassignment = bdd.pick(u)  # choose an assignment, `u.pick()` works too\nprint(f'{assignment = }')\n    # assignment = {'x': False, 'y': True}\n```\n\nWhen working with BDDs, two issues arise:\n\n- which variable names are present in an assignment?\n- what values do the variables take?\n\nThe values are Boolean, because BDD machinery is designed to reason for\nthat case only, so `1 /\\ 5` is a formula outside the realm of BDD reasoning.\nThe choice of variable names is a matter we discuss below.\n\nConsider the example\n\n```python\nbdd.declare('x', 'y', 'z')\nu = bdd.add_expr(r'x \\/ y')\nsupport = u.support\nprint(f'{support = }')\n    # support = {'x', 'y'}\n```\n\nThis tells us that the variables `x` and `y` occur in the formula that the\nBDD node `u` represents. Knowing what (Boolean) values a model assigns to\nthe variables `x` and `y` suffices to decide whether the model satisfies `u`.\nIn other words, the values of other variables, like `z`, are irrelevant to\nevaluating the expression `x \\/ y`.\n\nThe choice of semantics is yours. Which variables you want an assignment to\nmention depends on what you are doing with the assignment in your algorithm.\n\n```python\nu = bdd.add_expr('x')\n# default: variables in support(u)\nmodels = list(bdd.pick_iter(u))\nprint(models)\n    # [{'x': True}]\n# variables in `care_vars`\nmodels = list(bdd.pick_iter(u, care_vars=['x', 'y']))\nprint(models)\n    # [{'x': True, 'y': False}, {'x': True, 'y': True}]\n```\n\nBy default, `pick_iter` returns assignments to all variables in the support\nof the BDD node `u` given as input.\nIn this example, the support of `u` contains one variable: `x`\n(because the value of the expression `'x'` is independent of variable `y`).\n\nWe can use the argument `care_vars` to specify the variables that we want\nthe assignment to include. The assignments returned will include all variables\nin `care_vars`, plus the variables that appear along each path traversed in\nthe BDD. Variables in `care_vars` that are unassigned along each path will\nbe exhaustively enumerated (i.e., all combinations of `True` and `False`).\n\nFor example, if `care_vars == []`, then the assignments will contain only\nthose variables that appear along the recursive traversal of the BDD.\nIf `care_vars == support(u)`, then the result equals the default result.\nFor `care_vars > support(u)` we will observe more variables in each assignment\nthan the variables in the support.\n\n\nWe can also count how many assignments satisfy a BDD. The number depends on\nhow many variables occur in an assignment. The default number is as many\nvariables are contained in the support of that node. You can pass a larger\nnumber\n\n```python\nbdd.declare('x', 'y')\nu = bdd.add_expr('x')\ncount = u.count()\nprint(f'{count = }')\n    # count = 1\nmodels = list(bdd.pick_iter(u))\nprint(models)\n    # [{'x': True}]\n# pass a larger number of variables\ncount = u.count(nvars=3)\nprint(f'{count = }')\n    # count = 4\nmodels = list(bdd.pick_iter(u, ['x', 'y', 'z']))\nprint(models)\n    # [{'x': True, 'y': False, 'z': False},\n    #  {'x': True, 'y': True, 'z': False},\n    #  {'x': True, 'y': False, 'z': True},\n    #  {'x': True, 'y': True, 'z': True}]\n```\n\nA convenience method for creating a BDD from an assignment `dict` is\n\n```python\nd = dict(x=True, y=False, z=True)\nu = bdd.cube(d)\nv = bdd.add_expr(r'x /\\ ~ y /\\ z')\nassert u == v, (u, v)\n```\n\nThe interface is specified in the module `dd._abc`. Although [internal](\n    https://www.python.org/dev/peps/pep-0008/#descriptive-naming-styles),\nyou may want to take a look at the `_abc` module.\n\nAbove we discussed semantics from a [proof-theoretic viewpoint](\n    https://en.wikipedia.org/wiki/Metamathematics).\nThe same discussion can be rephrased in terms of function domains containing\nassignments to variables, so a [model-theoretic viewpoint](\n    https://en.wikipedia.org/wiki/Model_theory).\n\n\n### Plotting\n\n\nYou can dump a PDF of all nodes in the manager as follows\n\n```python\nimport dd.autoref as _bdd\n\n\nbdd = _bdd.BDD()\nbdd.declare('x', 'y', 'z')\nu = bdd.add_expr(r'(x /\\ y) \\/ ~ z')\nbdd.collect_garbage()  # optional\nbdd.dump('awesome.pdf')\n```\n\nThe result is shown below, with the meaning:\n\n- integers on the left signify levels (thus variables)\n- each node is annotated with a variable name (like `x`) dash the\n  node index in `dd.bdd.BDD._succ` (mainly for debugging purposes)\n- solid arcs represent the “if” branches\n- dashed arcs the “else” branches\n- only an “else” branch can be negated, signified by a `-1` annotation\n\nNegated edges mean that logical negation, i.e., `~`, is applied to the node\nthat is pointed to.\nNegated edges and BDD theory won't be discussed here, please refer to\na reference from those listed in the docstring of the module `dd.bdd`.\nFor example, [this document](\n    http://www.ecs.umass.edu/ece/labs/vlsicad/ece667/reading/somenzi99bdd.pdf)\nby Fabio Somenzi (CUDD's author).\n\nIn the following diagram, the BDD rooted at node `x-7` represents the\nBoolean function `(x /\\ y) \\/ ~ z`. For example, for the assignment\n`dict(x=False)`, the dashed arc from node `x-7` leads to the negation\n(due to the negated edge, signified by a `-1`) of the node `z-5`.\nThe BDD rooted at `z-5` represents the Boolean function `z`,\nso its negation is `~ z`.\n\nThe nodes `x-2`, `x-4`, `y-3` are intermediate results that result while\nconstructing the BDD for the Boolean function `(x /\\ y) \\/ ~ z`.\nThe BDD rooted at node `x-2` represents the Boolean function `x`, and the\nBDD rooted at node `x-4` represents the Boolean function `x /\\ y`.\n\n![example_bdd](https://rawgithub.com/johnyf/binaries/main/dd/awesome.png)\n\nAn external reference to a BDD is an arc that points to a node.\nFor example, `u` above is an external reference. An external reference can be\na complemented arc. External references can be included in a BDD diagram by\nusing the argument `roots` of the method `BDD.dump`. For example\n\n```python\nimport dd.autoref as _bdd\n\n\nbdd = _bdd.BDD()\nbdd.declare('x', 'y', 'z')\nu = bdd.add_expr(r'(x /\\ y) \\/ ~ z')\nprint(u.negated)\nv = ~ u\nprint(v.negated)\nbdd.collect_garbage()\nbdd.dump('rooted.pdf', roots=[v])\n```\n\nThe result is the following diagram, where the node `@-7` is the external\nreference `v`, which is a complemented arc.\n\n![example_bdd](https://rawgithub.com/johnyf/binaries/main/dd/rooted.png)\n\n\nIt is instructive to dump the `bdd` with and without collecting garbage.\n\n\n### Alternatives\n\n\nAs mentioned above, there are various ways to apply propositional operators\n\n```python\nx = bdd.var('x')\ny = bdd.var('y')\nu = x & y\nu = bdd.apply('and', x, y)\nu = bdd.apply('/\\\\', x, y)  # TLA+ syntax\nu = bdd.apply('&', x, y)  # Promela syntax\n```\n\nInfix Python operators work for BDD nodes in `dd.autoref` and `dd.cudd`,\nnot in `dd.bdd`, because nodes there are plain integers (`int`).\nBesides the method `apply`, there is also the ternary conditional method `ite`,\nbut that is more commonly used internally.\n\nFor single variables, the following are equivalent\n\n```python\nu = bdd.add_expr('x')\nu = bdd.var('x')  # faster\n```\n\nIn `autoref`, a few functions (not methods) are available for efficient\noperations that arise naturally in fixpoint algorithms when transition\nrelations are involved:\n\n- `image`, `preimage`: rename some variables, conjoin, existentially quantify,\n  and rename some variables, all at once\n- `copy_vars`: copy the variables of one BDD manager to another manager\n\n\n### Reminders about the implementation beneath\n\n\nThe Python syntax for Boolean operations (`u & v`) and the method `apply`\nare faster than the method `add_expr`, because the latter invokes the\nparser (generated using [`ply.yacc`](https://github.com/dabeaz/ply)).\nUsing `add_expr` is generally quicker and more readable.\nIn practice, prototype new algorithms using `add_expr`,\nthen profile, and if it matters convert the code to use `~`, `&`, `|`,\nand to call directly `apply`, `exist`, `let`, and other methods.\n\nIn the future, a compiler may be added, to compile expressions into\nfunctions that can be called multiple times, without invoking again the parser.\n\n\nThe number of nodes in the manager `bdd` is `len(bdd)`.\nAs noted earlier, each variable corresponds to a level, which is an index in\nthe variable order. This mapping can be obtained with\n\n```python\nlevel = bdd.level_of_var('x')\nvar = bdd.var_at_level(level)\nassert var == 'x', var\n```\n\nIn `autoref.BDD`, the `dict` that maps each defined variable to its\ncorresponding level can be obtained also from the attribute `BDD.vars`\n\n```python\nprint(bdd.vars)\n    # {'x': 0, 'y': 1, 'z': 2}\n```\n\nTo copy a node from one BDD manager to another manager\n\n```python\na = _bdd.BDD()\na.declare('x', 'y', 'z')\nu = a.add_expr(r'(x /\\ y) \\/ z')\n# copy to another BDD manager\nb = _bdd.BDD()\nb.declare(*a.vars)\nv = a.copy(u, b)\n```\n\nIn each of the modules `dd.autoref` and `dd.cudd`, references to BDD nodes\nare represented with a class named `Function`. When `Function` objects are\nnot referenced by any Python variable, CPython deallocates them, thus in\nthe next BDD garbage collection, the relevant BDD nodes can be deallocated.\n\nFor this reason, it is useful to avoid unnecessary references to nodes.\nThis includes the [underscore variable `_`](\n    https://docs.python.org/3/reference/lexical_analysis.html#reserved-classes-of-identifiers),\nfor example:\n\n```python\nimport dd.autoref\n\n\nbdd = dd.autoref.BDD()\nbdd.declare('x', 'y')\nc = [bdd.add_expr(r'x /\\ y'), bdd.add_expr(r'x \\/ ~ y')]\nu, _ = c  # `_` is assigned the `Function` that references\n    # the root of the BDD that represents x \\/ ~ y\nc = list()  # Python deallocates the `list` object created above\n# so `u` refers to the root of the BDD that represents x /\\ y,\n# as expected,\n# but `_` still refers to the BDD that represents x \\/ ~ y\nprint(bdd.to_expr(_))\n```\n\nThe Python reference by `_` in the previous example can be avoided\nby indexing, i.e., `u = c[0]`.\n\n\n### Pickle\n\nThe `dd.autoref` and `dd.bdd` modules can dump the manager to a\n[pickle file](https://en.wikipedia.org/wiki/Pickle_%28Python%29) and\nload it back\n\n```python\nbdd.dump('manager.p')\n```\n\nand later, or in another run:\n\n```python\nbdd = BDD.load('manager.p')\n```\n\n\n### Nodes as `Function` objects\n\nAs mentioned earlier, the main difference between the main `dd` modules is\nwhat type of object appears at the user interface as a “node”:\n\n- `dd.bdd` gives to the user signed integers as nodes\n- `dd.autoref` and `dd.cudd` give her `Function` objects as nodes.\n\nSeldom should this make a difference to the user.\nHowever, for integers, the meaning of the Python operators `~`, `&`, `|`, `^`\nis *unrelated* to the BDD manager.\nSo, if `u = -3` and `v = 25` are nodes in the `dd.bdd.BDD` manager `bdd`,\nyou cannot write `w = u & v` to get the correct result.\nYou have to use either:\n\n- `BDD.apply('and', u, v)` or\n- `BDD.add_expr(rf'{u} /\\ {v}')`.\n\nUnlike `dd.bdd`, the nodes in `autoref` and `cudd` are of class `Function`.\nThis abstracts away the underlying node representation, so that you can run\nthe same code in both pure Python (with `dd.bdd.BDD` underneath as manager),\nas well as C (with the `struct` named [`DdManager`](\n    https://github.com/johnyf/cudd/blob/80c9396b7efcb24c33868aeffb89a557af0dc356/cudd/cudd/cudd.h#L281)\nin `cudd.h` underneath as manager).\n\nThe [magic methods](https://github.com/RafeKettler/magicmethods) for\n`~`, `&`, `|`, `^` implemented by `Function` are its most frequently used aspect.\nTwo methods called `implies` and `equiv` are available.\nBut it is more readable to use `bdd.apply` or `bdd.add_expr`,\nor just `v | ~ u` for `u.implies(v)` and `~ (u ^ v)` for `u.equiv(v)`.\n\n\n#### BDD equality\n\nIf `u` and `v` are instances of `Function`, and `u == v`, then `u` represents\nthe same BDD as `v`.\n\nThis is **NOT** true for nodes in `dd.bdd`, because `u` and `v` may be nodes\nfrom *different* manager instances.\nSo, that `u == -3 == v` does not suffice to deduce that `u` and `v`\nrepresent the same BDD. In this case, we have to ensure that `u` and `v`\noriginate from the same manager. Thus, using `dd.bdd` offers less protection\nagainst subtle errors that will go undetected.\n\n\n#### Other methods\n\nInformation about a node `u` can be read with a few attributes and magic:\n\n- `len(u)` is the number of nodes in the graph that represents `u` in memory\n- `u.var` is the name (`str`) of the variable in `BDD.support(u)` with\n  minimal level (recall that variables are ordered)\n- `u.level` is the level of `u.var`\n- `u.ref` is the reference count of `u`, meaning the number of other nodes\n  `v` with an edge `(v, u)`, plus external references to `u` by the user.\n\nWe say that the node `u` is “labeled” with variable `u.var`.\nAt this point, recall that these diagrams track *decisions*.\nAt each node, we decide where to go next, depending on the value of `u.var`.\nIf `u.var` is true, then we go to node `u.high`, else to node `u.low`.\nFor this reason, `u.high` is also called the “then” child of `u`,\nand `u.low` the “else” child.\n\nThis object-oriented appearance is only an external interface for the user's\nconvenience. Typically, the bulk of the nodes aren't referenced externally.\nInternally, the nodes are managed efficiently en masse, no `Function`s there.\nA `Function.to_expr` method is present, but using `BDD.to_expr` looks tidier.\n\n\n## CUDD interface: `dd.cudd`\n\nWe said earlier that you can develop with `autoref`, deferring usage of\n`CUDD` for when really needed. This raises two questions:\n\n1. Why not use `cudd` from the start ?\n2. When should you switch from `autoref` to `cudd` ?\n\nThe answer to the second question is simple: when your problem takes more\ntime and memory than available.\nFor light to moderate use, `cudd` probably won't be needed.\n\nRegarding the first question, `dd.cudd` requires to:\n\n- *compile* CUDD, and\n- *cythonize, compile, and link* `dd/cudd.pyx`.\n\nThe `setup.py` of `dd` can do these for you, as described in the file\n[`README.md`](\n    https://github.com/tulip-control/dd/blob/main/README.md#cython-bindings).\nHowever, this may require more attention than appropriate for the occassion.\nAn example is teaching BDDs in a class on data structures, with the objective\nfor students to play with BDDs, not with [`gcc`](\n    https://en.wikipedia.org/wiki/GNU_Compiler_Collection)\nand [linking](\n    https://en.wikipedia.org/wiki/Linker_%28computing%29)\nerrors (that's enlightening too, but in the realm of a slightly different class).\n\n\nIf you are interested in tuning CUDD to get most out of it (or because some\nproblem demands it due to its size), then use:\n\n- `BDD.statistics` to obtain the information described in\n  [CUDD Programmer's manual / Gathering and interpreting statistics](\n    https://www.cs.rice.edu/~lm30/RSynth/CUDD/cudd/doc/node4.html#SECTION00048000000000000000).\n- `BDD.configure` to read and set the parameters “max memory”, “loose up to”,\n  “max cache hard”, “min hit”, and “max growth”.\n\nDue to how CUDD manages variables, the method `add_var` takes as keyword\nargument the variable index, *not* the level (which `autoref` does).\nThe level can still be set with the method `insert_var`.\n\nThe methods `dump` and `load` store the BDD of a selected node in a DDDMP file.\nPickling and PDF plotting are not available yet in `dd.cudd`.\n\nAn interface to the BuDDy C libary also exists, as `dd.buddy`.\nHowever, experimentation suggests that BuDDy does not contain as successful\nheuristics for deciding *when* to invoke reordering.\n\nCUDD is initialized with a `memory_estimate` of 1 GiB (1 [gibibyte](\n    https://en.wikipedia.org/wiki/Gibibyte)).\nIf the machine has less RAM, then `cudd.BDD` will raise an error. In this case,\npass a smaller initial memory estimate, for example\n\n```python\ncudd.BDD(memory_estimate=0.5 * 2**30)\n```\n\nNote that `2**30` bits is 1 gi*bi*byte (GiB), not 1 gi*ga*byte (GB).\nRelevant reading about [gigabyte](https://en.wikipedia.org/wiki/Gigabyte),\n[IEC prefixes for binary multiples](\n    https://en.wikipedia.org/wiki/Binary_prefix#IEC_prefixes), and\nthe [ISO/IEC 80000 standard](\n    https://en.wikipedia.org/wiki/ISO/IEC_80000#Information_science_and_technology).\n\n\n### Functions\n\nThe functions `and_exists`, `or_forall` in `dd.cudd` offer the functionality of\nrelational products (meaning neighboring variable substitution, conjunction,\nand quantification, all at one pass over BDDs).\nThis functionality is implemented with `image`, `preimage` in `dd.autoref`.\nNote that (pre)image contains substitution, unlike `and_exists`.\n\nThe function `cudd.reorder` is similar to `autoref.reorder`,\nbut does not default to invoking automated reordering.\nTypical use of CUDD enables dynamic reordering.\n\n\n### Checking for reference counting errors\n\nWhen a BDD manager `dd.cudd.BDD` is deallocated, it asserts that no BDD nodes\nhave nonzero reference count in CUDD. By default, this assertion should never\nfail, because automated reference counting makes it impossible.\n\nIf the assertion fails, then the exception is ignored and a message is printed\ninstead, and Python continues execution (read also the Cython documentation of\n[`__dealloc__`](\n    https://cython.readthedocs.io/en/latest/src/userguide/special_methods.html#finalization-method-dealloc),\nthe Python documentation of\n[\"Finalization and De-allocation\"](\n    https://docs.python.org/3/extending/newtypes.html#finalization-and-de-allocation),\nand of [`tp_dealloc`](\n    https://docs.python.org/3/c-api/typeobj.html#c.PyTypeObject.tp_dealloc)).\n\nIn case the user decides to explicitly modify the reference counts,\nignoring exceptions can make it easier for reference counting errors\nto go unnoticed. To make Python exit when reference counting errors exist\nbefore a BDD manager is deallocated, use:\n\n```python\nimport dd.cudd as cudd\n\n\nbdd = cudd.BDD()\n# ... statements ...\n# raise `AssertionError` if any nodes have nonzero reference count\n# just before deallocating the BDD manager\nassert len(bdd) == 0, len(bdd)\n```\n\nNote that the meaning of `len` for the class `dd.autoref.BDD` is slightly\ndifferent. As a result, the code for checking that no BDD nodes have nonzero\nreference count in `dd.autoref` is:\n\n```python\nimport dd.autoref as _bdd\n\n\nbdd = _bdd.BDD()\n# ... statements ...\n# raise `AssertionError` if any nodes have nonzero reference count\n# just before deallocating the BDD manager\nbdd._bdd.__del__()  # directly calling `__del__` does raise\n    # any exception raised inside `__del__`\n```\n\nNote that if an assertion fails inside `__del__`, then\n[the exception is ignored and a message is printed to `sys.stderr` instead](\n    https://docs.python.org/3/reference/datamodel.html#object.__del__),\nand Python continues execution. This is similar to what happens with\nexceptions raised inside `__dealloc__` of extension types in Cython.\nWhen `__del__` is called directly, exceptions raised inside it are not ignored.\n\n\n## Lower level: `dd.bdd`\n\nWe discuss now some more details about the pure Python implementation\nin `dd.bdd`. Two interfaces are available:\n\n- convenience: the module\n  [`dd.autoref`](\n    https://github.com/tulip-control/dd/blob/main/dd/autoref.py) wraps\n  `dd.bdd` and takes care of reference counting\n  using [`__del__`](\n    https://docs.python.org/3/reference/datamodel.html#object.__del__).\n\n- \"low level\": the module\n  [`dd.bdd`](\n    https://github.com/tulip-control/dd/blob/main/dd/bdd.py) requires that\n  the user in/decrement the reference counters associated with nodes that\n  are used outside of a `BDD`.\n\nThe pure-Python module `dd.bdd` can be used directly,\nwhich allows access more extensive than `dd.autoref`.\nThe `n` variables in a `dd.bdd.BDD` are ordered\nfrom `0` (top level) to `n - 1` (bottom level).\nThe terminal node `1` is at level `n`.\nThe constant `TRUE` is represented by `+1`, and `FALSE` by `-1`.\n\nTo avoid running out of memory, a BDD manager deletes nodes when they are not\nused anymore. This is called [garbage collection](\n    https://en.wikipedia.org/wiki/Garbage_collection_%28computer_science%29).\nSo, two things need to happen:\n\n1. keep track of node “usage”\n2. invoke garbage collection\n\nGarbage collection is triggered either explicitly by the user, or\nwhen invoking the reordering algorithm.\nTo prevent nodes from being garbage collected, their reference counts should\nbe incremented, which is discussed in the next section.\n\nNode usage is tracked with reference counting, for each node.\nIn `autoref`, the reference counts are maintained by the constructor and\ndestructor methods of `Function` (hence the “auto”).\nThese methods are invoked when the `Function` object is not referenced any\nmore by variables, so [Python decides](\n    https://docs.python.org/3/glossary.html#term-garbage-collection)\nto delete it.\n\nIn `dd.bdd`, you have to perform the reference counting by suitably adding to\nand subtracting from the counter associated to the node you reference.\nAlso, garbage collection is invoked either explicitly or by reordering\n(explicit or dynamic). So if you don't need to collect garbage, then you\ncan skip the reference counting (not recommended).\n\n\n### Reference counting\n\nThe reference counters live inside [`dd.bdd.BDD._ref`](\n    https://github.com/tulip-control/dd/blob/cbbc96f93da68d3d10f161ef27ccc5e3756c5ae2/dd/bdd.py#L81).\nTo guard against corner cases, like attempting to decrement a zero counter, use\n\n- [`BDD.incref(u)`](\n    https://github.com/tulip-control/dd/blob/cbbc96f93da68d3d10f161ef27ccc5e3756c5ae2/dd/bdd.py#L126):\n  +1 to the counter of node `u`\n- [`BDD.decref(u)`](\n    https://github.com/tulip-control/dd/blob/cbbc96f93da68d3d10f161ef27ccc5e3756c5ae2/dd/bdd.py#L130):\n  -1 to the same counter.\n\nThe method names `incref` and `decref` originate from the\n[Python reference counting](https://docs.python.org/3/c-api/refcounting.html)\nimplementation. If we want node `u` to persist after garbage collection,\nthen it needs to be actively referenced at least once\n\n```python\nu = bdd.add_expr(s)\nbdd.incref(u)\n```\n\nRevisiting an earlier example, manual reference counting looks like:\n\n```python\nimport dd.bdd as _bdd\n\n\nbdd = _bdd.BDD()\nbdd.declare('x', 'y', 'z')\ns = r'(x /\\ y) \\/ ~ z'  # TLA+ syntax\ns = '(x & y) | ! z'  # Promela syntax\nu = bdd.add_expr(s)\nbdd.incref(u)\nbdd.dump('before_collections.pdf')\nbdd.collect_garbage()\nbdd.dump('middle.pdf')\nbdd.decref(u)\nbdd.collect_garbage()\nbdd.dump('after_collections.pdf')\n```\n\nA formula may depend on a variable, or not.\nThere are two ways to find out\n\n```python\nu = bdd.add_expr(r'x /\\ y')  # TLA+ syntax\nu = bdd.add_expr('x & y')  # Promela syntax\n\nc = 'x' in bdd.support(u)  # more readable\nc_ = bdd.is_essential(u, 'x')  # slightly more efficient\nassert c == True, c\nassert c == c_, (c, c_)\n\nc = 'z' in bdd.support(u)\nassert c == False, c\n```\n\n\n### Reordering\n\nGiven a BDD, the size of its graph representation depends on the variable order.\nReordering changes the variable order. Reordering *optimization* searches for\na variable order better than the current one.\n*Dynamic* reordering is the automated invocation of reordering optimization.\nBDD managers use heuristics to decide when to invoke reordering,\nbecause it is [NP-hard](https://en.wikipedia.org/wiki/NP-hardness) to\nfind a variable order that minimizes a given BDD.\n\nThe function `dd.bdd.reorder` implements [Rudell's sifting algorithm](\n    http://www.eecg.toronto.edu/~ece1767/project/rud.pdf).\nThis reordering heuristic is the most commonly used, also in CUDD.\nDynamic variable reordering can be enabled by calling:\n\n```python\nimport dd.bdd as _bdd\n\n\nbdd = _bdd.BDD()\nbdd.configure(reordering=True)\n```\n\nBy default, dynamic reordering in `dd.bdd.BDD` is disabled.\nThis default is unlike `dd.cudd` and will change in the future to enabled.\n\nYou can also invoke reordering explicitly when desired, besides dynamic\ninvocation. For example:\n\n```python\nimport dd.bdd as _bdd\n\n\nbdd = _bdd.BDD()\nvrs = [f'x{i}' for i in range(3)]\nvrs.extend(f'y{i}' for i in range(3))\nbdd.declare(*vrs)\nprint(bdd.vars)\n    # {'x0': 0, 'x1': 1, 'x2': 2, 'y0': 3, 'y1': 4, 'y2': 5}\n\ns = r'(x0 /\\ y0) \\/ (x1 /\\ y1) \\/ (x2 /\\ y2)'  # TLA+ syntax\ns = '(x0 & y0) | (x1 & y1) | (x2 & y2)'  # Promela syntax\nu = bdd.add_expr(s)\nbdd.incref(u)\nnumber_of_nodes = len(bdd)\nprint(f'{number_of_nodes = }')\n    # number_of_nodes = 22\n\n# collect intermediate results produced while making u\nbdd.collect_garbage()\nnumber_of_nodes = len(bdd)\nprint(f'{number_of_nodes = }')\n    # number_of_nodes = 15\nbdd.dump('before_reordering.pdf')\n\n# invoke variable order optimization by sifting\n_bdd.reorder(bdd)\nnumber_of_nodes = len(bdd)\nprint(f'{number_of_nodes = }')\n    # number_of_nodes = 7\nprint(bdd.vars)\n    # {'x0': 0, 'x1': 3, 'x2': 5, 'y0': 1, 'y1': 2, 'y2': 4}\nbdd.dump('after_reordering.pdf')\n```\n\nIf you want to obtain a particular variable order, then give the\ndesired variable order as a `dict` to the function `reorder`.\n\n```python\nmy_favorite_order = dict(\n    x0=0, x1=1, x2=2,\n    y0=3, y1=4, y2=5)\nnumber_of_nodes = len(bdd)\nprint(f'{number_of_nodes = }')\n    # number_of_nodes = 7\n_bdd.reorder(bdd, my_favorite_order)\nnumber_of_nodes = len(bdd)\nprint(f'{number_of_nodes = }')\n    # number_of_nodes = 15\n```\n\n(Request such inefficient reordering only if you have some special purpose.)\nYou can turn [`logging`](https://docs.python.org/3/library/logging.html) to\n`DEBUG`, if you want to watch reordering in action.\n\nIn some cases you might want to make some pairs of variables adjacent to\neach other, but don't care about the location of each pair in the\nvariable order (e.g., this enables efficient variable renaming).\nUse `reorder_to_pairs`.\n\nAll reordering algorithms rely on the elementary operation of swapping two\nadjacent levels in the manager. You can do this by calling `BDD.swap`,\nso you can implement some reordering optimization algorithm different\nthan Rudell's.\nThe difficult part to implement is `swap`, not the optimization heuristic.\n\nThe garbage collector in\n[`dd.bdd.BDD.collect_garbage`](\n    https://github.com/tulip-control/dd/blob/cbbc96f93da68d3d10f161ef27ccc5e3756c5ae2/dd/bdd.py#L614)\nworks by scanning all nodes, marking the unreferenced ones,\nthen collecting those (mark-and-sweep).\n\nThe function `dd.bdd.to_nx(bdd, roots)` converts the subgraph of `bdd` rooted\nat `roots` to a [`networkx.MultiDiGraph`](\n    https://networkx.org/documentation/stable/tutorial.html#multigraphs).\n\n\n### Other methods\n\nThe remaining methods of `dd.bdd.BDD` will be of interest more to developers\nof algorithms that manipulate or read the graph of BDD nodes itself.\nFor example, say you wanted to write a little function that explores the\nBDD graph rooted at node `u`.\n\n```python\ndef print_descendants_forgetful(bdd, u):\n    i, v, w = bdd._succ[abs(u)]\n    print(u)\n    # u is terminal ?\n    if v is None:\n        return\n    print_descendants_forgetful(bdd, v)\n    print_descendants_forgetful(bdd, w)\n```\n\nIn the worst case, this can take time exponential in the nodes of `bdd`.\nTo make sure that it takes linear time, we have to remember visited nodes\n\n```python\ndef print_descendants(bdd, u, visited):\n    p = abs(u)\n    i, v, w = bdd._succ[p]\n    # visited ?\n    if p in visited:\n        return\n    # remember\n    visited.add(p)\n    print(u)\n    # u is terminal ?\n    if v is None:\n        return\n    print_descendants(bdd, v, visited)\n    print_descendants(bdd, w, visited)\n```\n\nRun it with `visited = set()`.\n\nNew nodes are created with `BDD.find_or_add(level, low, high)`.\nAlways use this method to make a new node, because it first checks in\nthe *unique table* `BDD._pred` whether the node at `level` with successors\n`(low, high)` already exists. This uniqueness is at the heart of reduced\nordered BDDs, the reason of their efficiency.\n\nThroughout `dd.bdd`, nodes are frequently referred to as *edges*.\nThe reason is that all nodes stored are positive integers.\nNegative integers signify negation, and appear only as either edges to\nsuccessors (negated edges), or references given to the user (because\na negation is popped to above the root node, for reasons of\n[representation uniqueness](https://dx.doi.org/10.1145/123186.123222)).\n\nThe method `BDD.levels` returns a generator of tuples `(u, i, v, w)`,\nover all nodes `u` in `BDD._succ`, where:\n\n- `u` is a node in the iteration\n- `i` is the level that `u` lives at\n- `v` is the low (“else”) successor\n- `w` is the high (“then”) successor\n\nThe iteration starts from the bottom (largest level), just above the terminal\nnode for “true” and “false” (which is `-1` in `dd.bdd`).\nIt scans each level, moving upwards, until it reaches the top level\n(indexed with 0).\n\nThe nodes contained in the graph rooted at node `u` are `bdd.descendants([u])`.\nAn efficient implementation of `let` that works only for variables with\nlevel <= the level of any variable in the support of node `u` is\n`_top_cofactor(u, level)`.\n\nFinally, `BDD.reduction` is of only academic interest.\nIt takes a binary decision diagram that contains redundancy in its graph\nrepresentation, and *reduces* it to the non-redundant, canonical form that\ncorresponds to the chosen variable order.\nThis is the function described originally [by Bryant](\n    https://dx.doi.org/10.1109/TC.1986.1676819).\n\nIt is never used, because all BDD graphs are *constructed* bottom-up\nto be reduced. To observe `reduction` in action, you have to manually\ncreate a BDD graph that is not reduced.\n\n\n## Example: Reachability analysis\n\nWe have been talking about BDDs, but you're probably here because you\nwant to *use* them. A common application is manipulation of Boolean functions\nin the context of relations that represent dynamics, sometimes called\ntransition relations.\n\nSuppose that we have an elevator that moves between three floors.\nWe are interested in the elevator's location, which can be at one of\nthe three floors. So, we can pretend that 0, 1, 2 are the three floors.\n\nUsing bits, we need at least two bits to represent the triple `{0, 1, 2}`.\nTwo bits can take a few too many values, so we should tell the computer that\n3 is not possible in our model of the three floors.\n\nSuppose that now the elevator is at floor `x0`, `x1`, and next at floor\n`x0'`, `x1'` (read “x prime”).\nThe identifiers `[\"x0\", \"x1\", \"x0'\", \"x1'\"]` are just four bits.\nThe elevator can move as follows\n\n```python\nimport dd.autoref as _bdd\n\n\nbdd = _bdd.BDD()\nbdd.declare(\"x0\", \"x0'\", \"x1\", \"x1'\")\n# TLA+ syntax\ns = (\n    r\"((~ x0 /\\ ~ x1) => ( (~ x0' /\\ ~ x1') \\/ (x0' /\\ ~ x1') )) /\\ \"\n    r\"((x0 /\\ ~ x1) => ~ (x0' /\\ x1')) /\\ \"\n    r\"((~ x0 /\\ x1) => ( (~ x0' /\\ x1') \\/ (x0' /\\ ~ x1') )) /\\ \"\n    r\" ~ (x0 /\\ x1)\")\ntransitions = bdd.add_expr(s)\n```\n\nWe can now find from which floors the elevator can reach floor 2.\nTo compute this, we find the floors that either:\n\n- are already inside the set `{2}`, or\n- can reach `q` after one transition.\n\nThis looks for existence of floors, hence the existential quantification.\nWe enlarge `q` by the floors we found, and repeat.\nWe continue this backward iteration, until reaching a [least fixpoint](\n    https://en.wikipedia.org/wiki/Knaster%E2%80%93Tarski_theorem)\n(meaning that two successive iterates are equal).\n\n```python\n# target is the set {2}\ntarget = bdd.add_expr(r'~ x0 /\\ x1')\n# start from empty set\nq = bdd.false\nqold = None\nprime = {\"x0\": \"x0'\", \"x1\": \"x1'\"}\nqvars = {\"x0'\", \"x1'\"}\n# fixpoint reached ?\nwhile q != qold:\n    qold = q\n    next_q = bdd.let(prime, q)\n    u = transitions & next_q\n    # existential quantification over x0', x1'\n    pred = bdd.quantify(u, qvars, forall=False)\n    q = q | pred | target\n```\n\nAt the end, we obtain\n\n```python\nexpr = q.to_expr()\nprint(expr)\n    # '(! ite(x0, x1, False))'\n```\n\nwhich is the set `{0, 1, 2}` (it does not contain 3, because that would\nevaluate to `! ite(True, True, False)` which equals `! True`, so `False`).\n\nMore about building symbolic algorithms, together with infrastructure for\narithmetic and automata, and examples, can be found in the package [`omega`](\n    https://github.com/tulip-control/omega/blob/main/doc/doc.md).\n\n\n## Syntax for quantified Boolean formulas\n\nThe method `BDD.add_expr` parses the following grammar.\nThe TLA+ module `dd_expression_grammar` extends\nthe TLA+ module `BNFGrammars`, which is defined on page 184 of\nthe book [\"Specifying Systems\"](\n    https://lamport.azurewebsites.net/tla/book.html).\n\n```tla\n------- MODULE dd_expression_grammar -------\n(* Grammar of expressions parsed by the\nfunction `dd._parser.Parser.parse`.\n*)\nEXTENDS\n    BNFGrammars\n\n\nCOMMA == tok(\",\")\nmaybe(x) ==\n    | Nil\n    | x\ncomma1(x) ==\n    x & (COMMA & x)^*\n\n\nNUMERAL == OneOf(\"0123456789\")\n\n\nis_dd_lexer_grammar(L) ==\n    /\\ L.A = tok(\"\\\\A\")\n    /\\ L.E = tok(\"\\\\E\")\n    /\\ L.S = tok(\"\\\\S\")\n    /\\ L.COLON = tok(\":\")\n    /\\ L.COMMA = COMMA\n    /\\ L.NOT = tok(\"~\")\n    /\\ L.AND = tok(\"/\\\\\")\n    /\\ L.OR = tok(\"\\\\/\")\n    /\\ L.IMPLIES = tok(\"=>\")\n    /\\ L.IFF = tok(\"<=>\")\n    /\\ L.EQ = tok(\"=\")\n    /\\ L.NEQ = tok(\"#\")\n    /\\ L.EXCLAMATION = tok(\"!\")\n    /\\ L.ET = tok(\"&\")\n    /\\ L.PIPE = tok(\"|\")\n    /\\ L.RARROW = tok(\"->\")\n    /\\ L.LR_ARROW = tok(\"<->\")\n    /\\ L.CIRCUMFLEX = tok(\"^\")\n    /\\ L.SLASH = tok(\"/\")\n    /\\ L.AT = tok(\"@\")\n    /\\ L.LPAREN = tok(\"(\")\n    /\\ L.RPAREN = tok(\")\")\n    /\\ L.FALSE = Tok({\n        \"FALSE\",\n        \"false\"\n        })\n    /\\ L.TRUE = Tok({\n        \"TRUE\",\n        \"true\"\n        })\n    /\\ L.ITE = tok(\"ite\")\n    /\\ L.NAME =\n        LET\n            LETTER ==\n                | OneOf(\"abcdefghijklmnopqrstuvwxyz\")\n                | OneOf(\"ABCDEFGHIJKLMNOPQRSTUVWXYZ\")\n            UNDERSCORE == tok(\"_\")\n            start ==\n                | LETTER\n                | UNDERSCORE\n            DOT == tok(\".\")\n            PRIME == tok(\"'\")\n            symbol ==\n                | start\n                | NUMERAL\n                | DOT\n                | PRIME\n            tail == symbol^*\n        IN\n            start & tail\n    /\\ L.INTEGER =\n        LET\n            DASH == tok(\"-\")\n            _dash == maybe(DASH)\n            numerals == NUMERAL^+\n        IN\n            _dash & numerals\n\n\nis_dd_parser_grammar(L, G) ==\n    /\\ \\A symbol \\in DOMAIN L:\n        G[symbol] = L[symbol]\n    /\\ G.expr =\n        (* predicate logic *)\n        | L.A & G.names & L.COLON & G.expr\n            (* universal quantification (\"forall\") *)\n        | L.E & G.names & L.COLON & G.expr\n            (* existential quantification (\"exists\") *)\n        | L.S & G.pairs & G.COLON & G.expr\n            (* renaming of variables (\"substitution\") *)\n        (* propositional *)\n        (* TLA+ syntax *)\n        | G.NOT & G.expr\n            (* negation (\"not\") *)\n        | G.expr & G.AND & G.expr\n            (* conjunction (\"and\") *)\n        | G.expr & G.OR & G.expr\n            (* disjunction (\"or\") *)\n        | G.expr & G.IMPLIES & G.expr\n            (* implication (\"implies\") *)\n        | G.expr & G.IFF & G.expr\n            (* equivalence (\"if and only if\") *)\n        | G.expr & G.NEQ & G.expr\n            (* difference (negation of `<=>`) *)\n        (* Promela syntax *)\n        | L.EXCLAMATION & G.expr\n            (* negation *)\n        | G.expr & L.ET & G.expr\n            (* conjunction *)\n        | G.expr & L.PIPE & G.expr\n            (* disjunction *)\n        | G.expr & L.RARROW & G.expr\n            (* implication *)\n        | G.expr & L.LR_ARROW & G.expr\n            (* equivalence *)\n        (* other *)\n        | G.expr & L.CIRCUMFLEX & G.expr\n            (* xor (exclusive disjunction) *)\n        | L.ITE & L.LPAREN &\n                    G.expr & L.COMMA &\n                    G.expr & L.COMMA &\n                    G.expr &\n                L.RPAREN\n            (* ternary conditional\n            (if-then-else) *)\n        | G.expr & L.EQ & G.expr\n        | L.LPAREN & G.expr & L.RPAREN\n            (* parentheses *)\n        | L.NAME\n            (* identifier (bit variable) *)\n        | L.AT & L.INTEGER\n            (* BDD node reference *)\n        | L.FALSE\n        | L.TRUE\n            (* Boolean constants *)\n    /\\ G.names =\n        comma1(L.NAME)\n    /\\ G.pairs =\n        comma1(G.pair)\n    /\\ G.pair =\n        L.NAME & L.SLASH & L.NAME\n\n\ndd_grammar ==\n    LET\n        L == LeastGrammar(is_dd_lexer_grammar)\n        is_parser_grammar(G) ==\n            is_dd_parser_grammar(L, G)\n    IN\n        LeastGrammar(is_parser_grammar)\n\n============================================\n```\n\nComments are written using TLA+ syntax:\n- `(* this is a doubly-delimited comment *)`\n- `\\* this is a trailing comment`\n\nDoubly-delimited comments can span multiple lines.\n\nThe token precedence (lowest to highest) and associativity is:\n\n- `:` (left)\n- `<=>, <->` (left)\n- `=>, ->` (left)\n- `-` (left)\n- `#`, `^` (left)\n- `\\/, |` (left)\n- `/\\, &` (left)\n- `=` (left)\n- `~, !`\n- `-` unary minus, as in `-5`\n\nThe meaning of a number of operators,\nassuming `a` and `b` take Boolean values:\n- `a => b` means `b \\/ ~ a`\n- `a <=> b` means `(a /\\ b) \\/ (~ a /\\ ~ b)`\n- `a # b` means `(a /\\ ~ b) \\/ (b /\\ ~ a)`\n- `a - b` means `a /\\ ~ b` (for BDDs only)\n- `ite(a, b, c)` means `(a /\\ b) \\/ (~ a /\\ c)`\n\nBoth and `setup.py`, and a developer may want to force a rebuild of the\nparser table. For this purpose, each module that contains a parser,\nalso has a function `_rewrite_tables` that deletes and rewrites the tables.\nIf the module is run as a script, then the `__main__` stanza calls this\nfunction to delete and the write the parser tables to the current directory.\nThe parsers use [`astutils`](https://pypi.org/project/astutils/).\n\n\n## Multi-valued decision diagrams (MDD)\n\nA representation for functions from integer variables to Boolean values.\nThe primary motivation for implementing MDDs was to produce more readable\nstring and graphical representations of BDDs.\nMDDs are implemented in pure Python.\nThe interface is “low”, similar to `dd.bdd`, with reference counting\nmanaged by the user.\nA core of necessary methods have been implemented, named as the `BDD` methods\nwith the same functionality in other `dd` modules.\nComplemented edges are used here too.\n\nBDDs are predicates over binary variables (two-valued).\nMDDs are predicates over integer variables (multi-valued).\nAs expected, the data structure and algorithms for representing an MDD are\na slight generalization of those for BDDs.\nFor example, compare the body of `dd.mdd.MDD.find_or_add` with\n`dd.bdd.BDD.find_or_add`.\n\nThe variables are defined by a `dict` argument to the constructor\n(in the future, dynamic variable addition may be implemented,\nby adding a method `MDD.add_var`)\n\n```python\nimport dd.mdd as _mdd\n\n\ndvars = dict(\n    x=dict(level=0, len=4),\n    y=dict(level=1, len=2))\nmdd = _mdd.MDD(dvars)\n```\n\nSo, variable `x` is an integer that can take values in `range(4)`.\n\nCurrently, the main use of an MDD is for more comprehensive representation of\na predicate stored in a BDD. This is achieved with the function\n`dd.mdd.bdd_to_mdd` that takes a `dd.bdd.BDD` and a mapping from\nMDD integers to BDD bits. Referencing of BDD nodes is necessary,\nbecause `bdd_to_mdd` invokes garbage collection on the BDD.\n\n```python\nimport dd.bdd as _bdd\nimport dd.mdd as _mdd\n\n\nbits = dict(x=0, y0=1, y1=2)\nbdd = _bdd.BDD(bits)\nu = bdd.add_expr(r'x \\/ (~ y0 /\\ y1)')\nbdd.incref(u)\n\n# convert BDD to MDD\nints = dict(\n    x=dict(level=1, len=2, bitnames=['x']),\n    y=dict(level=0, len=4, bitnames=['y0', 'y1']))\nmdd, umap = _mdd.bdd_to_mdd(bdd, ints)\n\n# map node `u` from BDD to MDD\nv = umap[abs(u)]\n# complemented ?\nif u < 0:\n    v = - v\nprint(v)\n    # -3\nexpr = mdd.to_expr(v)\nprint(expr)\n    # (! if (y in set([0, 1, 3])): (if (x = 0): 1,\n    # elif (x = 1): 0),\n    # elif (y = 2): 0)\n\n# plot MDD with graphviz\nmdd.dump('mdd.pdf')\n```\n\nNote that the `MDD` node `v` is complemented (-3 < 0), so the predicate\nin the negated value computed for node `y-3` in the next image.\n\n![example_bdd](https://rawgithub.com/johnyf/binaries/main/dd/mdd.png)\n\n\n## Installation of C extension modules\n\n\n### Environment variables that activate C extensions\n\nBy default, the package `dd` installs only its Python modules.\nYou can select to install Cython extensions using\nenvironment variables:\n\n- `DD_FETCH=1`: download CUDD v3.0.0 sources from the internet,\n  check the tarball's hash, unpack the tarball, and `make` CUDD.\n- `DD_CUDD=1`: build module `dd.cudd`, for CUDD BDDs\n- `DD_CUDD_ZDD=1`: build module `dd.cudd_zdd`, for CUDD ZDDs\n- `DD_SYLVAN=1`: build module `dd.sylvan`, for Sylvan BDDs\n- `DD_BUDDY=1`: build module `dd.buddy`, for BuDDy BDDs\n\nExample scripts are available that fetch and install\nthe Cython bindings:\n- [`examples/install_dd_cudd.sh`](\n    https://github.com/tulip-control/dd/blob/main/examples/install_dd_cudd.sh)\n- [`examples/install_dd_sylvan.sh`](\n    https://github.com/tulip-control/dd/blob/main/examples/install_dd_sylvan.sh)\n- [`examples/install_dd_buddy.sh`](\n    https://github.com/tulip-control/dd/blob/main/examples/install_dd_buddy.sh)\n\n\n### Alternative: Directly running `setup.py`\n\nActivating the Cython build by directly running\n`python setup.py` is an alternative to\nusing environment variables (e.g., `export DD_CUDD=1` etc).\nThe relevant command-line options of `setup.py` are:\n\n- `--fetch`: same effect as `DD_FETCH=1`\n- `--cudd`: same effect as `DD_CUDD=1`\n- `--cudd_zdd`: same effect as `DD_CUDD_ZDD=1`\n- `--sylvan`: same effect as `DD_SYLVAN=1`\n- `--buddy`: same effect as `DD_BUDDY=1`\n\nThese options work for `python setup.py sdist` and\n`python setup.py install`, but directly running\n`python setup.py` is deprecated by `setuptools >= 58.3.0`.\n\nExample:\n\n```shell\npip download dd --no-deps\ntar xzf dd-*.tar.gz\npushd dd-*/\n    # `pushd` means `cd`\npython setup.py install --fetch --cudd --cudd_zdd\npopd\n```\n\n[`pushd directory`](\n    https://en.wikipedia.org/wiki/Pushd_and_popd)\nis akin to `stack.append(directory)` in\nPython, and `popd` to `stack.pop()`.\n\nThe path to an existing CUDD build directory\ncan be passed as an argument, for example:\n\n```shell\npython setup.py install \\\n    --fetch \\\n    --cudd=\"/home/user/cudd\"\n```\n\n\n### Using the package `build`\n\nThe following also works for building source tarballs and wheels:\n\n```sh\npip install cython\nexport DD_FETCH=1 DD_CUDD=1\npython -m build --no-isolation\n```\n\nTo build a source tarball:\n\n```sh\nDD_CUDD=1 python -m build --sdist --no-isolation\n```\n\n\n### Customizing the C compilation\n\nIf you build and install CUDD, Sylvan, or BuDDy yourself, then ensure that:\n\n- the header files and libraries are present, and\n- the compiler is configured appropriately (include,\n  linking, and library configuration),\n\neither by setting [environment variables](\n    https://en.wikipedia.org/wiki/Environment_variable)\nprior to calling `pip`, or by editing the file [`download.py`](\n    https://github.com/tulip-control/dd/blob/main/download.py).\n\nCurrently, `download.py` expects to find Sylvan under `dd/sylvan` and\nbuilt with [Autotools](\n    https://en.wikipedia.org/wiki/GNU_Build_System)\n(for an example, read `.github/workflows/setup_build_env.sh`).\nIf the path differs in your environment, remember to update it.\n\nIf you prefer defining installation directories, then follow\n[Cython's instructions](\n    https://cython.readthedocs.io/en/latest/src/tutorial/clibraries.html#compiling-and-linking)\nto define `CFLAGS` and `LDFLAGS` before installing.\nYou need to have copied `CuddInt.h` to the installation's include location\n(CUDD omits it).\n\nFor example, to use CUDD as installed by MacPorts\n(port [`libcudd`](\n    https://ports.macports.org/port/libcudd/)), use\n\n```sh\nCFLAGS=\"-I/opt/local/include\" LDFLAGS=\"-L/opt/local/lib\"\n```\n\n\n## Installing the development version\n\nFor installing the development version of `dd` from the `git` repository,\nan alternative to cloning the repository and installing from the cloned\nrepository is to [use `pip` for doing so](\n    https://pip.pypa.io/en/stable/cli/pip_install/#argument-handling):\n\n```shell\npip install https://github.com/tulip-control/dd/archive/main.tar.gz\n```\n\nor with [`pip` using `git`](\n    https://pip.pypa.io/en/stable/topics/vcs-support/#git)\n(this alternative requires that `git` be installed):\n\n```shell\npip install git+https://github.com/tulip-control/dd\n```\n\nA `git` URL can be passed also to [`pip download`](\n    https://pip.pypa.io/en/stable/cli/pip_download/#overview),\nfor example:\n\n```shell\npip download --no-deps https://github.com/tulip-control/dd/archive/main.tar.gz\n```\n\nThe extension `.zip` too can be used for the name of the [archive file](\n    https://en.wikipedia.org/wiki/Archive_file)\nin the URL. Analogously, with `pip` using `git`:\n\n```shell\npip download --no-deps git+https://github.com/tulip-control/dd\n```\n\nNote that the naming of paths *within* the archive file downloaded from\nGitHub in this way will differ, depending on whether `https://` or\n`git+https://` is used.\n\n\n## Footnotes\n\n- The `Makefile` contains the rules `sdist` and `wheel` that\n  create distributions for uploading to PyPI with `twine`.\n\n- Press `Ctrl + \\` on Linux and Darwin to quit the Python process when\n  CUDD computations take a long time. Read `stty -a` for your settings.\n\n- If you are interested in exploring other decision diagram packages,\n  you can find [a list at `github.com/johnyf/tool_lists/`](\n    https://github.com/johnyf/tool_lists/blob/main/bdd.md).\n\n\n## Copying\n\nThis document is copyright 2015-2022 by California Institute of Technology.\nAll rights reserved. Licensed under 3-clause BSD.\n"
  },
  {
    "path": "download.py",
    "content": "\"\"\"Retrieve and build dependencies of C extensions.\"\"\"\nimport argparse as _arg\nimport collections.abc as _abc\nimport ctypes\nimport functools as _ft\nimport hashlib\nimport os\nimport platform\nimport shutil\nimport subprocess\nimport sys\nimport tarfile\nimport textwrap as _tw\nimport typing as _ty\nimport urllib.error\nimport urllib.request\n\n\ntry:\n    import Cython.Build as _build\n    pyx = '.pyx'\nexcept ImportError:\n    print('`import cython` failed')\n    pyx = '.c'\nimport setuptools.extension as _extension\n\n\nEXTENSIONS: _ty.Final = [\n    'cudd', 'cudd_zdd', 'buddy', 'sylvan']\n# CUDD\nCUDD_VERSION: _ty.Final = '3.0.0'\nCUDD_TARBALL: _ty.Final = f'cudd-{CUDD_VERSION}.tar.gz'\nCUDD_URL: _ty.Final = (\n    'https://sourceforge.net/projects/cudd-mirror/files/'\n    f'cudd-{CUDD_VERSION}.tar.gz/download')\nCUDD_SHA256: _ty.Final = (\n    'b8e966b4562c96a03e7fbea239729587'\n    'd7b395d53cadcc39a7203b49cf7eeb69')\nCC = 'gcc'\nFILE_PATH = os.path.dirname(os.path.realpath(__file__))\nCUDD_PATH = os.path.join(\n    FILE_PATH,\n    f'cudd-{CUDD_VERSION}')\nCUDD_DIRS: _ty.Final = [\n    'cudd', 'dddmp', 'epd', 'mtr', 'st', 'util']\nCUDD_INCLUDE = ['.', *CUDD_DIRS]\nCUDD_LINK: _ty.Final = ['cudd/.libs', 'dddmp/.libs']\nCUDD_LIB: _ty.Final = ['cudd', 'dddmp']\nCUDD_CFLAGS = [\n    # '-arch x86_64',\n    '-fPIC',\n    '-std=c99',\n    '-DBSD',\n    '-DHAVE_IEEE_754',\n    '-pthread', '-fwrapv',\n    '-fno-strict-aliasing',\n    '-Wall', '-W', '-O3']\nsizeof_long = ctypes.sizeof(ctypes.c_long)\nsizeof_void_p = ctypes.sizeof(ctypes.c_void_p)\nCUDD_CFLAGS.extend([\n    f'-DSIZEOF_LONG={sizeof_long}',\n    f'-DSIZEOF_VOID_P={sizeof_void_p}'])\n# add -fPIC\nXCFLAGS = (\n    'XCFLAGS=-fPIC -mtune=native -DHAVE_IEEE_754 -DBSD '\n    f'-DSIZEOF_VOID_P={sizeof_void_p} '\n    f'-DSIZEOF_LONG={sizeof_long}')\n# Sylvan\nSYLVAN_PATH = os.path.join(\n    FILE_PATH,\n    'sylvan')\nSYLVAN_INCLUDE = [\n    [SYLVAN_PATH, 'src'],\n    [FILE_PATH, 'dd']]\nSYLVAN_LINK = [[SYLVAN_PATH, 'src/.libs']]\n\n\ndef extensions(\n        args:\n            _arg.Namespace\n        ) -> list[\n            _extension.Extension]:\n    \"\"\"Return C extensions, cythonize as needed.\n\n    @param args:\n        known args from `argparse.parse_known_args`\n    \"\"\"\n    directives = dict(\n        language_level=3,\n        embedsignature=True)\n    cudd_cflags = list(CUDD_CFLAGS)\n    sylvan_cflags = list()\n    compile_time_env = dict()\n    if platform.system() != 'Darwin':\n        cudd_cflags.append('-mtune=native')\n    # tell gcc to compile line tracing\n    if args.linetrace:\n        print('compile Cython extensions with line tracing')\n        directives['linetrace'] = True\n        cudd_cflags.append('-DCYTHON_TRACE=1')\n        sylvan_cflags.append('-DCYTHON_TRACE=1')\n        # directives['binding'] = True\n    os.environ['CC'] = CC\n    path = args.cudd if args.cudd else CUDD_PATH\n    cudd_include = [(path, s) for s in CUDD_INCLUDE]\n    cudd_link = [(path, s) for s in CUDD_LINK]\n    try:\n        _copy_cudd_license(args)\n        _copy_extern_licenses(args)\n    except FileNotFoundError:\n        print('license files of build dependencies not found')\n    c_extensions = dict(\n        cudd=_extension.Extension(\n            'dd.cudd',\n            sources=[f'dd/cudd{pyx}'],\n            include_dirs=_join(cudd_include),\n            library_dirs=_join(cudd_link),\n            libraries=CUDD_LIB,\n            extra_compile_args=cudd_cflags),\n        cudd_zdd=_extension.Extension(\n            'dd.cudd_zdd',\n            sources=[f'dd/cudd_zdd{pyx}'],\n            include_dirs=_join(cudd_include),\n            library_dirs=_join(cudd_link),\n            libraries=CUDD_LIB,\n            extra_compile_args=cudd_cflags),\n        buddy=_extension.Extension(\n            'dd.buddy',\n            sources=[f'dd/buddy{pyx}'],\n            libraries=['bdd']),\n        sylvan=_extension.Extension(\n            'dd.sylvan',\n            sources=[f'dd/sylvan{pyx}'],\n            include_dirs=_join(SYLVAN_INCLUDE),\n            library_dirs=_join(SYLVAN_LINK),\n            libraries=['sylvan'],\n            extra_compile_args=sylvan_cflags))\n    for ext in EXTENSIONS:\n        if getattr(args, ext) is None:\n            c_extensions.pop(ext)\n    if pyx == '.pyx':\n        ext_modules = list()\n        for k, v in c_extensions.items():\n            c = _build.cythonize(\n                [v],\n                compiler_directives=directives,\n                compile_time_env=compile_time_env)\n            ext_modules.append(c[0])\n    else:\n        ext_modules = list(c_extensions.values())\n    return ext_modules\n\n\ndef _copy_cudd_license(\n        args:\n            _arg.Namespace\n        ) -> None:\n    \"\"\"Include CUDD's license in wheels.\"\"\"\n    path = args.cudd if args.cudd else CUDD_PATH\n    license = os.path.join(path, 'LICENSE')\n    included = os.path.join('dd', 'CUDD_LICENSE')\n    yes = (\n        args.bdist_wheel and\n        getattr(args, 'cudd') is not None)\n    if yes:\n        shutil.copyfile(license, included)\n    elif os.path.isfile(included):\n        os.remove(included)\n\n\ndef _copy_extern_licenses(\n        args:\n            _arg.Namespace\n        ) -> None:\n    \"\"\"Include in wheels licenses related to building CUDD.\n\n    To fetch the license files, invoke `make download_licenses`.\n    \"\"\"\n    licenses = [\n        'GLIBC_COPYING.LIB',\n        'GLIBC_LICENSES',\n        'PYTHON_LICENSE']\n    path = os.path.join(FILE_PATH, 'extern')\n    yes = (\n        args.bdist_wheel and\n        getattr(args, 'cudd') is not None)\n    for name in licenses:\n        license = os.path.join(path, name)\n        included = os.path.join('dd', name)\n        if yes and os.path.isfile(license):\n            shutil.copyfile(license, included)\n        elif yes and not os.path.isfile(license):\n            print(\n                f'WARNING: No file: `{license}`, '\n                'skipping file copy.')\n        elif os.path.isfile(included):\n            os.remove(included)\n\n\ndef _join(\n        paths:\n            _abc.Iterable[\n                _abc.Iterable[str]]\n        ) -> list[str]:\n    \"\"\"Return paths, after joining each.\n\n    Flattens a list-of-lists to a list.\n    \"\"\"\n    return [os.path.join(*x) for x in paths]\n\n\ndef fetch(\n        url:\n            str,\n        sha256:\n            str,\n        filename:\n            str\n        ) -> None:\n    \"\"\"Download file from `url`, and check its hashes.\n\n    @param sha256:\n        SHA-256 hash value of file that\n        will be downloaded\n    \"\"\"\n    if os.path.isfile(filename):\n        print(\n            f'File `{filename}` already present, '\n            'checking hash.')\n        _check_file_hash(filename, sha256)\n        return\n    print(f'Attempting to download file from URL:  {url}')\n    try:\n        response = urllib.request.urlopen(url)\n        if response is None:\n            raise urllib.error.URLError(\n                '`urllib.request.urlopen` returned `None` '\n                'when attempting to open the URL:  '\n                f'{url}')\n    except urllib.error.URLError as url_error:\n        raise RuntimeError(_tw.dedent(f'''\n            An exception was raised when attempting\n            to open the URL:\n                {url}\n\n            In case the error message from `urllib` is\n            about SSL certificates, please confirm that\n            your installation of Python has the required\n            SSL certificates. How to ensure this can differ,\n            depending on how Python is installed\n            (building from source or using an installer).\n\n            CPython's `--with-openssl` (of `configure`)\n            is relevant when building CPython from source.\n\n            When using an installer of CPython, a separate\n            post-installation step may be needed,\n            as described in CPython's documentation.\n\n            Relevant information:\n                <https://www.python.org/downloads/>\n\n            For downloading CUDD, an alternative is to\n            download by other means the file at the URL:\n                {url}\n            unpack it, and then run:\n\n            ```python\n            import download\n\n            download.make_cudd()\n            ```\n\n            Once CUDD compilation has completed, run:\n\n            ```\n            export DD_CUDD=1 DD_CUDD_ZDD=1;\n            pip install .\n            ```\n\n            i.e., without the option `DD_FETCH`.\n            ''')) from url_error\n    with response, open(filename, 'wb') as f:\n        f.write(response.read())\n    print(\n        'Completed downloading from URL '\n        '(may have resulted from redirection):  '\n        f'{response.url}\\n'\n        'Wrote the downloaded data to file:  '\n        f'`{filename}`\\n'\n        'Will now check the hash value (SHA-256) of '\n        f'the file:  `{filename}`')\n    _check_file_hash(filename, sha256)\n\n\ndef _check_file_hash(\n        filename:\n            str,\n        sha256:\n            str\n        ) -> None:\n    \"\"\"Assert `filename` has given hash.\"\"\"\n    with open(filename, 'rb') as f:\n        data = f.read()\n    _assert_sha(data, sha256, 256, filename)\n    print(\n        'Checked hash value (SHA-256) of '\n        f'file `{filename}`, and is as expected.')\n\n\ndef _assert_sha(\n        data:\n            bytes,\n        expected_sha_value:\n            str,\n        algo:\n            _ty.Literal[\n                256,\n                512],\n        filename:\n            str |\n            None=None\n        ) -> None:\n    \"\"\"Assert `data` hash is `expected_sha_value`.\n\n    If the hash of `data`, as computed using the algorithm\n    specified in `algo`, is not `expected_sha_value`,\n    then raise an `AssertionError`.\n\n    The hash value is computed using the functions:\n    - `hashlib.sha256()` if `algo == 256`\n    - `hashlib.sha512()` if `algo == 512`\n\n    @param data:\n        bytes, to compute the hash of them\n        (as accepted by `hashlib.sha512()`)\n    @param expected_sha_value:\n        hash value (SHA-256 or SHA-512),\n        must correspond to `algo`\n    @param algo:\n        hashing algorithm\n    @param filename:\n        name of file whose hash\n        is being checked, optional argument,\n        if present then it will be used\n        in message of the `AssertionError`\n    \"\"\"\n    match algo:\n        case 256:\n            h = hashlib.sha256(data)\n        case 512:\n            h = hashlib.sha512(data)\n        case _:\n            raise ValueError(\n                f'unknown algorithm:  {algo = }')\n    x = h.hexdigest()\n    if x == expected_sha_value:\n        return\n    if filename is None:\n        fs = ''\n    else:\n        fs = f'`{filename}` '\n    raise AssertionError(\n        f'The computed SHA-{algo} hash value '\n        f'of the downloaded file {fs}does not match '\n        'the expected hash value.'\n        f'\\nComputed SHA-{algo}:  {x}'\n        f'\\nExpected SHA-{algo}:  {expected_sha_value}')\n\n\ndef untar(\n        filename:\n            str\n        ) -> None:\n    \"\"\"Extract contents of tar file `filename`.\"\"\"\n    print(f'++ unpack: {filename}')\n    with tarfile.open(filename) as tar:\n        tar.extractall()\n    print('-- done unpacking.')\n\n\ndef make_cudd(\n        ) -> None:\n    \"\"\"Compile CUDD.\"\"\"\n    path = CUDD_PATH\n    cmd = [\"./configure\", \"CFLAGS=-fPIC -std=c99\"]\n    subprocess.call(cmd, cwd=path)\n    subprocess.call(['make', '-j4'], cwd=path)\n\n\ndef fetch_cudd(\n        ) -> None:\n    \"\"\"Retrieve, unpack, patch, and compile CUDD.\"\"\"\n    filename = CUDD_TARBALL\n    fetch(CUDD_URL, CUDD_SHA256, filename)\n    untar(filename)\n    make_cudd()\n\n\ndef download_licenses(\n        ) -> None:\n    \"\"\"Fetch licenses of dependencies.\n\n    These licenses are included in the wheel of `dd`.\n    The license files are placed in\n    the directory `extern/`.\n    \"\"\"\n    license_dir = 'extern'\n    if not os.path.isdir(license_dir):\n        os.mkdir(license_dir)\n    join = _ft.partial(os.path.join, license_dir)\n    # download GLIBC licenses\n    glibc_license_file = join('GLIBC_COPYING.LIB')\n    glibc_license_url = '''\n        https://sourceware.org/git/\n        ?p=glibc.git;a=blob_plain;f=COPYING.LIB;hb=HEAD\n        '''\n    _fetch_file(glibc_license_url, glibc_license_file)\n    glibc_licenses_file = join('GLIBC_LICENSES')\n    glibc_licenses_url = '''\n        https://sourceware.org/git/\n        ?p=glibc.git;a=blob_plain;f=LICENSES;hb=HEAD\n        '''\n    _fetch_file(glibc_licenses_url, glibc_licenses_file)\n    # download CPython license\n    py_license_file = join('PYTHON_LICENSE')\n    numbers = sys.version_info[:2]\n    python_version = '.'.join(map(str, numbers))\n    py_license_url = f'''\n        https://raw.githubusercontent.com/\n        python/cpython/{python_version}/LICENSE\n        '''\n    _fetch_file(py_license_url, py_license_file)\n    print('Downloaded license files.')\n\n\ndef _fetch_file(\n        url:\n            str,\n        filename:\n            str\n        ) -> None:\n    \"\"\"Dump `url` to `filename`.\n\n    Removes blankspace from `url`.\n    \"\"\"\n    url = ''.join(url.split())\n    response = urllib.request.urlopen(url)\n    if response is None:\n        raise urllib.error.URLError(\n            'Error when attempting to open '\n            f'the URL:  {url}')\n    with response, open(filename, 'wb') as fd:\n        fd.write(response.read())\n\n\nif __name__ == '__main__':\n    fetch_cudd()\n"
  },
  {
    "path": "examples/README.md",
    "content": "The examples are:\n\n- `variable_substitution.py`: rename variables that a BDD depends on\n\n- `boolean_satisfiability.py`: solving the\n  [propositional satisfiability problem](\n    https://en.wikipedia.org/wiki/Boolean_satisfiability_problem)\n\n- `reachability.py`: compute the states reachable from some\n  starting set of states.\n\n- `queens.py`: solve the [N-queens problem](\n    https://en.wikipedia.org/wiki/Eight_queens_puzzle).\n\n- `bdd_traversal.py`: breadth-first and depth-first iteration over nodes\n\n- `reordering.py`: activate dynamic variable reordering for the Python\n  implementation, invoke reordering explicitly, and permute variables to\n  a desired order.\n\n- `cudd_configure_reordering.py`: how to turn reordering off when using CUDD\n\n- `cudd_statistics.py`: read CUDD's activity in numbers.\n\n- `cudd_memory_limits.py`: bound how much memory CUDD is\n  allowed to use.\n\n- `cudd_zdd.py`: how to use ZDDs with CUDD.\n\n- `json_example.py`: how to write BDDs to JSON files,\n  and how to load BDDs from JSON files.\n\n\nThe shell scripts show how to install the Cython modules of `dd`:\n\n- `install_dd_cudd.sh`: how to install the modules:\n  - `dd.cudd` and\n  - `dd.cudd_zdd`\n- `install_dd_sylvan.sh`: how to install the module `dd.sylvan`\n- `install_dd_buddy.sh`: how to install the module `dd.buddy`\n\nTo install all the above modules, combine the steps contained in\nthe above shell scripts, and define all the relevant\nenvironment variables, i.e.,\n\n```shell\nexport \\\n    DD_BUDDY=1 \\\n    DD_FETCH=1 \\\n    DD_CUDD=1 \\\n    DD_CUDD_ZDD=1 \\\n    DD_SYLVAN=1\npip install dd\n```\n"
  },
  {
    "path": "examples/_test_examples.py",
    "content": "\"\"\"Run each example module.\"\"\"\nimport os\nimport subprocess as _sbp\nimport sys\n\n\ndef _main(\n        ) -> None:\n    \"\"\"Run each example module under `.`.\"\"\"\n    _, _, files = next(os.walk('.'))\n    this_file = os.path.basename(__file__)\n    files.remove(this_file)\n    for filename in files:\n        _, ext = os.path.splitext(filename)\n        if ext != '.py':\n            continue\n        cmd = [sys.executable, filename]\n        print(cmd)\n        retcode = _sbp.call(cmd)\n        if retcode == 0:\n            continue\n        raise RuntimeError(retcode)\n\n\nif __name__ == '__main__':\n    _main()\n"
  },
  {
    "path": "examples/bdd_traversal.py",
    "content": "\"\"\"Traversing binary decision diagrams.\n\nRead [1, Section 2.2].\n\n\nReference\n=========\n\n[1] Steven M. LaValle\n    Planning Algorithms\n    Cambridge University Press, 2006\n    <http://lavalle.pl/planning/>\n\"\"\"\nimport collections as _cl\nimport textwrap as _tw\n\nimport dd.autoref as _bdd\n\n\ndef traverse_breadth_first(u):\n    \"\"\"Return nodes encountered.\"\"\"\n    queue = _cl.deque([u])\n    visited = set()\n    while queue:\n        g = queue.popleft()\n        visited.add(int(g))\n        # is `g` a leaf ?\n        if g.var is None:\n            continue\n        queue.extend([g.low, g.high])\n    return visited\n\n\ndef traverse_depth_first(u):\n    \"\"\"Return nodes encountered.\"\"\"\n    stack = [u]\n    visited = set()\n    while stack:\n        g = stack.pop()\n        visited.add(int(g))\n        # is `g` a leaf ?\n        if g.var is None:\n            continue\n        stack.extend([g.low, g.high])\n    return visited\n\n\ndef run_traversals():\n    bdd = _bdd.BDD()\n    bdd.declare('x', 'y', 'z')\n    u = bdd.add_expr(\n        r' (x \\/ ~ y) /\\ z ')\n    print('breadth-first traversal')\n    visited_b = traverse_breadth_first(u)\n    print(visited_b)\n    print('depth-first traversal')\n    visited_d = traverse_depth_first(u)\n    print(visited_d)\n    if visited_b == visited_d:\n        return\n    raise AssertionError(_tw.dedent(f'''\n        Expected same set of nodes from\n        traversals, but:\n        {visited_b = }\n        and:\n        {visited_d = }\n        '''))\n\n\nif __name__ == '__main__':\n    run_traversals()\n"
  },
  {
    "path": "examples/boolean_satisfiability.py",
    "content": "\"\"\"Is a given Boolean formula satisfiable?\"\"\"\nimport dd\n\n\ndef example():\n    \"\"\"Demonstrate usage.\"\"\"\n    # a formula\n    names = ['x', 'y']\n    formula = r'x /\\ ~ y'\n    sat = is_satisfiable(formula, names)\n    _print_result(formula, sat)\n    # another formula\n    names = ['x']\n    formula = r'x /\\ ~ x'\n    sat = is_satisfiable(formula, names)\n    _print_result(formula, sat)\n\n\ndef is_satisfiable(formula, names):\n    \"\"\"Return `True` if `formula` is satisfiable.\n\n    A formula is satisfiable by Boolean values,\n    if there exist Boolean values such that,\n    when those values are substituted for the\n    variables that appear in the formula,\n    the result is equivalent to `TRUE`.\n    \"\"\"\n    bdd = dd.BDD()\n    bdd.declare(*names)\n    u = bdd.add_expr(formula)\n    return u != bdd.false\n\n\ndef _print_result(formula, sat):\n    \"\"\"Inform at stdout.\"\"\"\n    if sat:\n        neg = ''\n    else:\n        neg = 'not '\n    print(\n        f'The formula `{formula}` '\n        f'is {neg}satisfiable.')\n\n\nif __name__ == '__main__':\n    example()\n"
  },
  {
    "path": "examples/cudd_configure_reordering.py",
    "content": "\"\"\"How to configure reordering in CUDD via `dd.cudd`.\"\"\"\nimport pprint\n\nimport dd.cudd as _bdd\n\n\nbdd = _bdd.BDD()\nvrs = ['x', 'y', 'z']\nbdd.declare(*vrs)\n# get the variable order\nlevels = {var: bdd.level_of_var(var) for var in vrs}\nprint(levels)\n# change the levels\ndesired_levels = dict(x=2, y=0, z=1)\n_bdd.reorder(bdd, desired_levels)\n# confirm that variables are now where desired\nnew_levels = {var: bdd.level_of_var(var) for var in vrs}\nprint(new_levels)\n# dynamic reordering is initially turned on\nconfig = bdd.configure()\npprint.pprint(config)\n# turn off dynamic reordering\nbdd.configure(reordering=False)\n# confirm dynamic reordering is now off\nconfig = bdd.configure()\npprint.pprint(config)\n"
  },
  {
    "path": "examples/cudd_memory_limits.py",
    "content": "\"\"\"How to place an upper bound on the memory CUDD consumes.\"\"\"\nimport dd.cudd as _bdd\n\n\ndef configure():\n    GiB = 2**30\n    b = _bdd.BDD()\n    b.configure(\n        # number of bytes\n        max_memory=2 * GiB,\n        # number of entries, not memory units!\n        max_cache_hard=2**25)\n\n\nif __name__ == '__main__':\n    configure()\n"
  },
  {
    "path": "examples/cudd_statistics.py",
    "content": "\"\"\"How to print readable statistics.\"\"\"\nimport pprint\n\nimport dd.cudd\n\n\ndef print_statistics():\n    b = dd.cudd.BDD()\n    b.declare('x', 'y', 'z')\n    u = b.add_expr(r'x /\\ y /\\ z')\n    u = b.add_expr(r'x \\/ y \\/ ~ z')\n    stats = b.statistics()\n    pprint.pprint(format_dict(stats))\n\n\ndef format_dict(d):\n    \"\"\"Return `dict` with values readable by humans.\"\"\"\n    return {k: format_number(v) for k, v in d.items()}\n\n\ndef format_number(x):\n    \"\"\"Return readable string for `x`.\"\"\"\n    if 0 < x < 1:\n        return f'{x:1.2}'\n    return f'{x:_}'\n\n\nif __name__ == '__main__':\n    print_statistics()\n"
  },
  {
    "path": "examples/cudd_zdd.py",
    "content": "\"\"\"How to use ZDDs with CUDD.\"\"\"\nimport dd.cudd_zdd as _zdd\n\n\ndef zdd_example():\n    zdd = _zdd.ZDD()\n    zdd.declare('x', 'y', 'z')\n    u = zdd.add_expr(r'(x /\\ y) \\/ z')\n    let = dict(y=zdd.add_expr('~ x'))\n    v = zdd.let(let, u)\n    v_ = zdd.add_expr('z')\n    assert v == v_, (v, v_)\n\n\nif __name__ == '__main__':\n    zdd_example()\n"
  },
  {
    "path": "examples/good_vs_bad_variable_order.py",
    "content": "\"\"\"How the variable order in a BDD affects the number of nodes.\n\n\nReference\n=========\n\n[1] Randal Bryant\n    \"On the complexity of VLSI implementations and graph representations\n    of Boolean functions with application to integer multiplication\"\n    TOC, 1991\n    <https://doi.org/10.1109/12.73590>\n\"\"\"\nimport dd.autoref as _bdd\n\n\ndef comparing_two_variable_orders():\n    n = 6\n    # declare variables\n    vrs = [f'x{i}' for i in range(n)]\n    primed_vars = [prime(var) for var in vrs]\n    bdd = _bdd.BDD()\n    bdd.declare(*(vrs + primed_vars))\n    # equality constraints cause difficulties with BDD size\n    expr = r' /\\ '.join(\n        f\" {var} <=> {var}' \" for var in vrs)\n    u = bdd.add_expr(expr)\n    bdd.collect_garbage()\n    # an order that yields a small BDD for `expr`\n    good_order = list()\n    for var in vrs:\n        good_order.extend([var, prime(var)])\n    # an order that yields a large BDD for `expr`\n    bad_order = list(vrs)\n    bad_order.extend(prime(var) for var in vrs)\n    # plot\n    _bdd.reorder(bdd, list_to_dict(good_order))\n    bdd.dump('good.pdf')\n    _bdd.reorder(bdd, list_to_dict(bad_order))\n    bdd.dump('bad.pdf')\n\n\ndef list_to_dict(c):\n    return {var: level for level, var in enumerate(c)}\n\n\ndef prime(s):\n    return s + \"'\"\n\n\nif __name__ == '__main__':\n    comparing_two_variable_orders()\n"
  },
  {
    "path": "examples/install_dd_buddy.sh",
    "content": "#!/usr/bin/env bash\n#\n# Install `dd`, including the module\n# `dd.buddy`, which is written in Cython.\n#\n# To run this script, enter in\n# a command-line environment:\n#\n# ./install_dd_buddy.sh\n\n\nset -v\nset -e\n# Fetch and build BuDDy\nBUDDY_INSTALL_PREFIX=`pwd`\nBUDDY_ARCHIVE=buddy-2.4.tar.gz\nBUDDY_URL=https://sourceforge.net/projects/buddy/\\\nfiles/buddy/BuDDy%202.4/buddy-2.4.tar.gz/download\ncurl -L $BUDDY_URL -o $BUDDY_ARCHIVE\ntar -xzf $BUDDY_ARCHIVE\npushd buddy-*/\n./configure \\\n    --prefix=$BUDDY_INSTALL_PREFIX\n    # as described in\n    # the README file of BuDDy\nmake\nmake install\n    # by default installs to:\n    # `/usr/local/include/` and\n    # `/usr/local/lib/`\n    #\n    # The installation location can\n    # be changed with\n    # `./configure --prefix=/where/to/install`\nexport CFLAGS=\"-I$BUDDY_INSTALL_PREFIX/include\"\nexport LDFLAGS=\"-L$BUDDY_INSTALL_PREFIX/lib\"\nexport LD_LIBRARY_PATH=\\\n$BUDDY_INSTALL_PREFIX/lib:$LD_LIBRARY_PATH\necho $CFLAGS\necho $LDFLAGS\necho $LD_LIBRARY_PATH\npopd\n\n\n# Fetch and install `dd`\npip install cython\nexport DD_BUDDY=1\npip install dd \\\n    -vvv \\\n    --use-pep517 \\\n    --no-build-isolation\n    # passes `-lbdd` to the C compiler\n#\n# fetch `dd` source\npip download \\\n    --no-deps dd \\\n    --no-binary dd\ntar -xzf dd-*.tar.gz\n# confirm that `dd.buddy` did get installed\npushd dd-*/tests/\npython -c 'import dd.buddy'\npopd\n"
  },
  {
    "path": "examples/install_dd_cudd.sh",
    "content": "#!/usr/bin/env bash\n#\n# Install `dd`, including the modules\n# `dd.cudd` and `dd.cudd_zdd`\n# (which are written in Cython).\n#\n# To run this script, enter in\n# a command-line environment:\n#\n# ./install_dd_cudd.sh\n#\n# This script is unnecessary if you\n# want a pure-Python installation of `dd`.\n# If so, then `pip install dd`.\n#\n# This is script is unnecessary also\n# if a wheel file for your operating system\n# and CPython version is available on PyPI.\n# Wheel files (`*.whl`) can be found at:\n#     <https://pypi.org/project/dd/#files>\n#\n# If there *is* a wheel file on PyPI\n# that matches your operating system and\n# CPython version, then `pip install dd`\n# suffices.\n\n\nset -v\nset -e\npip install dd\n    # to first install\n    # dependencies of `dd`\npip uninstall -y dd\npip download \\\n    --no-deps dd \\\n    --no-binary dd\ntar -xzf dd-*.tar.gz\npushd dd-*/\nexport DD_FETCH=1 DD_CUDD=1 DD_CUDD_ZDD=1\npip install . \\\n    -vvv \\\n    --use-pep517 \\\n    --no-build-isolation\n# confirm that `dd.cudd` did get installed\npushd tests/\npython -c 'import dd.cudd'\npopd\npopd\n"
  },
  {
    "path": "examples/install_dd_sylvan.sh",
    "content": "#!/usr/bin/env bash\n#\n# Install `dd`, including\n# the module `dd.sylvan`.\n# (which is written in Cython).\n#\n# To run this script, enter in\n# a command-line environment:\n#\n# ./install_dd_sylvan.sh\n\n\nset -v\nset -e\n# check for Sylvan build dependencies\nif ! command -v autoreconf &> /dev/null\nthen\n    echo \"apt install autoconf\"\n    exit\nfi\nif ! command -v libtoolize &> /dev/null\nthen\n    echo \"apt install libtool\"\n    exit\nfi\n# Fetch and install Sylvan\nSYLVAN_ARCHIVE=sylvan.tar.gz\nSYLVAN_URL=https://github.com/\\\nutwente-fmt/sylvan/tarball/v1.0.0\ncurl -L $SYLVAN_URL -o $SYLVAN_ARCHIVE\n# checksum\necho \"9877fe07a8cfe9889152e29624a4c5b283\\\ncb34672ec524ccb3edb313b3057fbf8ef45622a4\\\n9796fae17aa24e0baea5ccfa18f1bc5923e3c552\\\n45ab3e3c1927c8  sylvan.tar.gz\" | shasum -a 512 -c -\n# unpack\nmkdir sylvan\ntar xzf sylvan.tar.gz -C sylvan --strip=1\npushd sylvan\nautoreconf -fi\n./configure\nmake\n# update the environment variable `LD_LIBRARY_PATH`\nexport CFLAGS=\"-I`pwd`/src\"\nexport LDFLAGS=\"-L`pwd`/src/.libs\"\nexport LD_LIBRARY_PATH=`pwd`/src/.libs:$LD_LIBRARY_PATH\necho $CFLAGS\necho $LDFLAGS\necho $LD_LIBRARY_PATH\npopd\n\n\n# Fetch and install `dd`\nexport DD_SYLVAN=1\npip install dd \\\n    -vvv \\\n    --use-pep517 \\\n    --no-build-isolation\n# fetch `dd` source\npip download \\\n    --no-deps dd \\\n    --no-binary dd\ntar -xzf dd-*.tar.gz\n# confirm that `dd.sylvan` did get installed\npushd dd-*/tests/\npython -c 'import dd.sylvan'\npopd\n"
  },
  {
    "path": "examples/json_example.py",
    "content": "\"\"\"How to write BDDs to JSON files, and load them.\"\"\"\nimport dd.cudd as _bdd\n\n\ndef json_example():\n    \"\"\"Entry point.\"\"\"\n    filename = 'storage.json'\n    dump_bdd_as_json(filename)\n    load_bdd_from_json(filename)\n\n\ndef dump_bdd_as_json(filename):\n    \"\"\"Write a BDD to a JSON file.\"\"\"\n    bdd = _bdd.BDD()\n    bdd.declare('x', 'y', 'z')\n    u = bdd.add_expr(r'(x /\\ y) \\/ ~ z')\n    roots = dict(u=u)\n    bdd.dump(filename, roots)\n    print(f'Dumped BDD: {u}')\n\n\ndef load_bdd_from_json(filename):\n    \"\"\"Load a BDD from a JSON file.\"\"\"\n    bdd = _bdd.BDD()\n    roots = bdd.load(filename)\n    print(f'Loaded BDD: {roots}')\n\n\nif __name__ == '__main__':\n    json_example()\n"
  },
  {
    "path": "examples/json_load.py",
    "content": "\"\"\"Loading BDD from JSON using the `json` module.\n\nThis example shows how to load from JSON,\nand convert to a `networkx` graph.\n\nBDDs can be loaded from JSON into BDD contexts\nusing the methods:\n- `dd.autoref.BDD.load()`\n- `dd.cudd.BDD.load()`\n\"\"\"\nimport json\nimport textwrap as _tw\n\nimport dd.autoref as _bdd\nimport networkx as _nx\n\n\ndef dump_load_example(\n        ) -> None:\n    \"\"\"Loading to `networkx` graph, using `json`.\"\"\"\n    filename = 'example_bdd.json'\n    create_and_dump_bdd(filename)\n    graph = load_and_map_to_nx(filename)\n    # print `graph`\n    print('The loaded graph is:')\n    for u, v in graph.edges():\n        print(f'edge: {u} -> {v}')\n    roots = graph.roots\n    level_of_var = graph.level_of_var\n    print(_tw.dedent(f'''\n        with graph roots: {roots}\n        and variable levels: {level_of_var}\n        '''))\n\n\ndef create_and_dump_bdd(\n        filename:\n            str\n        ) -> None:\n    \"\"\"Write BDD to JSON file.\"\"\"\n    bdd = _bdd.BDD()\n    bdd.declare('x', 'y', 'z')\n    u = bdd.add_expr(r'x /\\ (~ y \\/ z)')\n    roots = dict(u=u)\n    bdd.dump(\n        filename,\n        roots=roots)\n\n\nclass BDDGraph(\n        _nx.DiGraph):\n    \"\"\"Storing also roots and variable levels.\"\"\"\n\n    def __init__(\n            self,\n            *arg,\n            **kw):\n        super().__init__(*arg, **kw)\n        self.roots: dict | None = None\n        self.level_of_var: dict | None = None\n\n\ndef load_and_map_to_nx(\n        filename:\n            str\n        ) -> BDDGraph:\n    \"\"\"Return graph loaded from JSON.\"\"\"\n    with open(filename, 'r') as fd:\n        data = fd.read()\n    data = json.loads(data)\n    # map to nx\n    graph = BDDGraph()\n    for k, v in data.items():\n        print(k, v)\n        if k in ('roots', 'level_of_var'):\n            continue\n        node = int(k)\n        level, node_low, node_high = v\n        graph.add_edge(node, node_low)\n        graph.add_edge(node, node_high)\n    graph.roots = data['roots']\n    graph.level_of_var = data['level_of_var']\n    return graph\n\n\nif __name__ == '__main__':\n    dump_load_example()\n"
  },
  {
    "path": "examples/np.py",
    "content": "\"\"\"How the variable order in a BDD affects the number of nodes.\n\n\nReference\n=========\n\n[1] Randal Bryant\n    \"On the complexity of VLSI implementations and graph representations\n    of Boolean functions with application to integer multiplication\"\n    TOC, 1991\n    <https://doi.org/10.1109/12.73590>\n\"\"\"\nimport dd.autoref as _bdd\n\n\ndef comparing_two_variable_orders():\n    n = 6\n    # declare variables\n    vrs = [\n        f'x{i}'\n        for i in range(2 * n)]\n    bdd = _bdd.BDD()\n    bdd.declare(*vrs)\n    # equality constraints cause difficulties with BDD size\n    def eq(i):\n        j = (i + n + 1) % (2 * n)\n        return f' x{i} <=> x{j} '\n    expr_1 = r' /\\ '.join(map(\n        eq, range(n)))\n    u = bdd.add_expr(expr_1)\n    def eq(k):\n        i = 2 * k\n        j = 2 * k + 1\n        return f' x{i} <=> x{j} '\n    expr_2 = r' /\\ '.join(map(\n        eq, range(n)))\n    v = bdd.add_expr(expr_2)\n    bdd.collect_garbage()\n    # an order that yields a small BDD for `expr`\n    good_order = [\n        f'x{i - 1}'\n        for i in [\n            1, 7, 3, 9, 5, 11,\n            2, 8, 4, 10, 6, 12]]\n    # an order that yields a large BDD for `expr`\n    bad_order = list(vrs)\n    # plot\n    _bdd.reorder(bdd, list_to_dict(good_order))\n    bdd.dump('good.pdf')\n    _bdd.reorder(bdd, list_to_dict(bad_order))\n    bdd.dump('bad.pdf')\n\n\ndef list_to_dict(c):\n    return {\n        var: level\n        for level, var in enumerate(c)}\n\n\ndef prime(s):\n    return f\"{s}'\"\n\n\nif __name__ == '__main__':\n    comparing_two_variable_orders()\n"
  },
  {
    "path": "examples/queens.py",
    "content": "\"\"\"N-Queens problem using one-hot encoding.\n\n\nReference\n=========\n\n[1] Henrik R. Andersen\n    \"An introduction to binary decision diagrams\"\n    Lecture notes for \"Efficient Algorithms and Programs\", 1999\n    The IT University of Copenhagen\n    Section 6.1\n\"\"\"\nimport collections.abc as _abc\nimport pickle\nimport time\n\nimport dd.bdd as _bdd\n\n\ndef solve_queens(n):\n    \"\"\"Return set of models for the `n`-queens problem.\n\n    @rtype:\n        `int`, `BDD`\n    \"\"\"\n    vrs = [_var_str(i, j) for i in range(n) for j in range(n)]\n    bdd = _bdd.BDD()\n    bdd.declare(*vrs)\n    s = queens_formula(n)\n    u = bdd.add_expr(s)\n    return u, bdd\n\n\ndef queens_formula(n):\n    \"\"\"Return a non-trivial propositional formula for the problem.\"\"\"\n    # i = row index\n    # j = column index\n    present = at_least_one_queen_per_row(n)\n    rows = at_most_one_queen_per_line(True, n)\n    cols = at_most_one_queen_per_line(False, n)\n    slash = at_most_one_queen_per_diagonal(True, n)\n    backslash = at_most_one_queen_per_diagonal(False, n)\n    s = _conjoin([present, rows, cols, slash, backslash])\n    return s\n\n\ndef at_least_one_queen_per_row(n):\n    \"\"\"Return formula as `str`.\"\"\"\n    c = list()\n    for i in range(n):\n        xijs = [_var_str(i, j) for j in range(n)]\n        s = _disjoin(xijs)\n        c.append(s)\n    return _conjoin(c)\n\n\ndef at_most_one_queen_per_line(row, n):\n    \"\"\"Return formula as `str`.\n\n    @param row:\n        if `True`, then constrain rows, else columns.\n    \"\"\"\n    c = list()\n    for i in range(n):\n        if row:\n            xijs = [_var_str(i, j) for j in range(n)]\n        else:\n            xijs = [_var_str(j, i) for j in range(n)]\n        s = mutex(xijs)\n        c.append(s)\n    return _conjoin(c)\n\n\ndef at_most_one_queen_per_diagonal(slash, n):\n    \"\"\"Return formula as `str`.\n\n    @param slash:\n        if `True`, then constrain anti-diagonals,\n        else diagonals.\n    \"\"\"\n    c = list()\n    if slash:\n        a = -n\n        b = n\n    else:\n        a = 0\n        b = 2 * n\n    for k in range(a, b):\n        if slash:\n            ij = [(i, i + k) for i in range(n)]\n        else:\n            ij = [(i, k - i) for i in range(n)]\n        ijs = [(i, j) for i, j in ij if 0 <= i < n and 0 <= j < n]\n        if not ij:\n            continue\n        xijs = [_var_str(i, j) for i, j in ijs]\n        s = mutex(xijs)\n        c.append(s)\n    return _conjoin(c)\n\n\ndef mutex(v):\n    \"\"\"Return formula for at most one variable `True`.\n\n    @param v:\n        iterable of variables as `str`\n    \"\"\"\n    v = set(v)\n    c = list()\n    for x in v:\n        rest = _disjoin(y for y in v if y != x)\n        s = f'{x} => ~ ({rest})'\n        c.append(s)\n    return _conjoin(c)\n\n\ndef _var_str(i, j):\n    \"\"\"Return variable for occupancy of cell at {row: i, column: j}.\"\"\"\n    return f'x{i}{j}'\n\n\ndef _conjoin(\n        strings:\n            _abc.Iterable\n        ) -> str:\n    \"\"\"Return conjunction of `strings`.\"\"\"\n    expr = _apply_infix(\n        strings,\n        operator=r' /\\ ')\n    if not expr:\n        expr = 'FALSE'\n    return expr\n\n\ndef _disjoin(\n        strings:\n            _abc.Iterable\n        ) -> str:\n    \"\"\"Return disjunction of `strings`.\"\"\"\n    expr = _apply_infix(\n        strings,\n        operator=r' \\/ ')\n    if not expr:\n        expr = 'TRUE'\n    return expr\n\n\ndef _apply_infix(\n        strings:\n            _abc.Iterable,\n        operator:\n            str\n        ) -> str:\n    \"\"\"Apply infix `operator` to `strings`.\"\"\"\n    nonempty = filter(None, strings)\n    parenthesized = map(_parenthesize, nonempty)\n    return operator.join(parenthesized)\n\n\ndef _parenthesize(string) -> str:\n    \"\"\"Return `string` within parentheses.\"\"\"\n    return f'({string})'\n\n\ndef benchmark(n):\n    \"\"\"Run for `n` queens and print statistics.\"\"\"\n    t0 = time.perf_counter()\n    u, bdd = solve_queens(n)\n    t1 = time.perf_counter()\n    dt = t1 - t0\n    s = (\n        '------\\n'\n        f'queens: {n}\\n'\n        f'time: {dt} (sec)\\n'\n        f'node: {u}\\n'\n        f'total nodes: {len(bdd)}\\n'\n        '------\\n')\n    print(s)\n    return dt\n\n\ndef _example():\n    n_max = 9\n    fname = 'dd_times.p'\n    times = dict()\n    for n in range(n_max + 1):\n        t = benchmark(n)\n        times[n] = t\n    with open(fname, 'wb') as f:\n        pickle.dump(times, f)\n\n\nif __name__ == '__main__':\n    _example()\n"
  },
  {
    "path": "examples/reachability.py",
    "content": "\"\"\"Reachability computation over a graph.\n\n\nThis example is discussed in the documentation [1].\nPropositional variables are used.\nIf you are interested in using integers,\nthen take a look at the package `omega`.\n\n\n[1](https://github.com/tulip-control/dd/blob/\n    main/doc.md#example-reachability-analysis)\n\"\"\"\nimport dd.autoref as _bdd\n# uncomment if you have compiled `dd.cudd`\n# import dd.cudd as _bdd\n\n\ndef transition_system(bdd):\n    \"\"\"Return the transition relation of a graph.\"\"\"\n    dvars = [\"x0\", \"x0'\", \"x1\", \"x1'\"]\n    for var in dvars:\n        bdd.add_var(var)\n    s = r'''\n           ((~ x0 /\\ ~ x1) => ( (~ x0' /\\ ~ x1') \\/ (x0' /\\ ~ x1') ))\n        /\\ ((x0 /\\ ~ x1) => ~ (x0' /\\ x1'))\n        /\\ ((~ x0 /\\ x1) => ( (~ x0' /\\ x1') \\/ (x0' /\\ ~ x1') ))\n        /\\ ~ (x0 /\\ x1)\n        '''\n    transitions = bdd.add_expr(s)\n    return transitions\n\n\ndef least_fixpoint(transitions, bdd):\n    \"\"\"Return ancestor nodes.\"\"\"\n    # target is the set {2}\n    target = bdd.add_expr(r'~ x0 /\\ x1')\n    # start from empty set\n    q = bdd.false\n    qold = None\n    prime = {\"x0\": \"x0'\", \"x1\": \"x1'\"}\n    qvars = {\"x0'\", \"x1'\"}\n    # fixpoint reached ?\n    while q != qold:\n        qold = q\n        next_q = bdd.let(prime, q)\n        u = transitions & next_q\n        # existential quantification over x0', x1'\n        pred = bdd.exist(qvars, u)\n        # alternative: pred = bdd.quantify(u, qvars, forall=False)\n        q = q | pred | target\n    return q\n\n\ndef reachability_example():\n    bdd = _bdd.BDD()\n    transitions = transition_system(bdd)\n    q = least_fixpoint(transitions, bdd)\n    s = q.to_expr()\n    print(s)\n\n\nif __name__ == '__main__':\n    reachability_example()\n"
  },
  {
    "path": "examples/reordering.py",
    "content": "\"\"\"Activate dynamic reordering for the Python implementation `dd.autoref`.\"\"\"\nimport logging\n\nimport dd.autoref as _bdd\n\n\ndef demo_dynamic_reordering():\n    \"\"\"Activate dynamic reordering and add nodes until triggered.\"\"\"\n    print(\n        '\\n' + (50 * '-') +\n        '\\ndemo of dynamic reordering\\n' +\n        (50 * '-'))\n    show_logging()\n    bdd = create_manager()\n    # activate reordering\n    # (for the Python implementation `dd.autoref` reordering\n    # is disabled by default, whereas for `dd.cudd` reordering\n    # is enabled by default)\n    bdd.configure(reordering=True)\n    print_manager_size(bdd)  # nearly empty BDD manager\n    print_var_levels(bdd)\n    # add enough nodes to trigger reordering\n    nodes = trigger_reordering(bdd)\n    print_manager_size(bdd)\n    print_var_levels(bdd)  # variables have been reordered\n\n\ndef show_logging():\n    \"\"\"Display logging messages relevant to reordering.\n\n    To log more details, increase the verbosity level\n    to `logging.DEBUG`.\n    \"\"\"\n    logger = logging.getLogger('dd.bdd')\n    logger.setLevel(logging.INFO)\n    logger.addHandler(logging.StreamHandler())\n\n\ndef create_manager():\n    \"\"\"Return a BDD manager with plenty of variables declared.\"\"\"\n    bdd = _bdd.BDD()\n    vrs = [f'x{i}' for i in range(100)]\n    bdd.declare(*vrs)\n    return bdd\n\n\ndef trigger_reordering(bdd):\n    \"\"\"Add several nodes to the manager.\n\n    Dynamic reordering is triggered when the total number\n    of nodes that are in the manager reaches a certain threshold.\n    We add nodes in order to reach that that threshold,\n    and thus trigger reordering.\n\n    To witness the reordering happen,\n    look at the logging messages.\n    \"\"\"\n    nodes = list()\n    for i in range(25):\n        expr = (\n            r'(x{i1} /\\ x{i2}) \\/ (x{i3} /\\ x{i4})'\n            r' \\/ (x{i5} /\\ x{i6})').format(\n                i1=i, i2=i + 6, i3=i + 7,\n                i4=i + 8, i5=i + 9, i6=i + 10)\n        u = bdd.add_expr(expr)\n        nodes.append(u)\n    return nodes\n\n\ndef print_var_levels(bdd):\n    \"\"\"Print level of each variable.\"\"\"\n    n = len(bdd.vars)\n    levels = [\n        bdd.var_at_level(level)\n        for level in range(n)]\n    print(\n        'Variable order (starting at level 0):\\n'\n        f'{levels}')\n\n\ndef demo_static_reordering():\n    \"\"\"How to invoke reordering explicitly.\"\"\"\n    print(\n        '\\n' + (50 * '-') +\n        '\\ndemo of static reordering\\n' +\n        (50 * '-'))\n    bdd = _bdd.BDD()\n    bdd.declare('z1', 'z2', 'z3', 'y1', 'y2', 'y3')\n    expr = r'(z1 /\\ y1) \\/ (z2 /\\ y2) \\/ (z3 /\\ y3)'\n    u = bdd.add_expr(expr)\n    print_manager_size(bdd)\n    # invoke sifting\n    _bdd.reorder(bdd)\n    print_manager_size(bdd)\n\n\ndef demo_specific_var_order():\n    \"\"\"How to permute the variables to a desired order.\"\"\"\n    print(\n        '\\n' + (50 * '-') +\n        '\\ndemo of user-defined variable permutation\\n' +\n        (50 * '-'))\n    bdd = _bdd.BDD()\n    bdd.declare('a', 'b', 'c')\n    u = bdd.add_expr(r'(a \\/ b) /\\ ~ c')\n    print_var_levels(bdd)\n    # reorder\n    desired_order = dict(a=2, b=0, c=1)\n    _bdd.reorder(bdd, desired_order)\n    # confirm\n    print_var_levels(bdd)\n\n\ndef print_manager_size(bdd):\n    print(f'Nodes in manager: {len(bdd)}')\n\n\nif __name__ == '__main__':\n    demo_dynamic_reordering()\n    demo_static_reordering()\n    demo_specific_var_order()\n"
  },
  {
    "path": "examples/transfer_bdd.py",
    "content": "\"\"\"How to copy a BDD from one manager to another.\"\"\"\nimport dd.autoref as _bdd\n\n\ndef transfer():\n    \"\"\"Copy a BDD from one manager to another.\"\"\"\n    # create two BDD managers\n    source = _bdd.BDD()\n    target = _bdd.BDD()\n    # declare the variables in both BDD managers\n    vrs = ['a', 'b']\n    source.declare(*vrs)\n    target.declare(*vrs)\n    # create a BDD with root `u`\n    u = source.add_expr(r'a /\\ b')\n    # copy the BDD `u` to the BDD manager `target`\n    u_ = source.copy(u, target)\n\n\ndef copy_variable_order():\n    \"\"\"As in `transfer`, and copy variable order too.\"\"\"\n    source = _bdd.BDD()\n    target = _bdd.BDD()\n    # declare variables in the source BDD manager\n    source.declare('a', 'b')\n    # create a BDD with root `u`\n    u = source.add_expr(r'a /\\ b')\n    # copy the variables, and the variable order\n    target.declare(*source.vars)\n    target.reorder(source.var_levels)\n    # copy the BDD `u` to the BDD manager `target`\n    u_ = source.copy(u, target)\n\n\nif __name__ == '__main__':\n    transfer()\n    copy_variable_order()\n"
  },
  {
    "path": "examples/variable_substitution.py",
    "content": "\"\"\"Renaming variables.\"\"\"\nimport dd.autoref as _bdd\n# import dd.cudd as _bdd  # uncomment to use CUDD\n\n\ndef variable_substitution():\n    # instantiate a shared BDD manager\n    bdd = _bdd.BDD()\n    bdd.declare('x', 'y', 'u', 'v')\n    # create the BDD for the disjunction of x and y\n    u = bdd.add_expr(r'x \\/ y')\n    # Substitution of x' for x and y' for y.\n    # In TLA+ we can write this as:\n    #\n    # LET\n    #     x == u\n    #     y == v\n    # IN\n    #     x \\/ y\n    rename = dict(x='u', y='v')\n    v = bdd.let(rename, u)\n    # show the result\n    s = bdd.to_expr(v)\n    print(s)\n\n    # another way to confirm that the result is as expected\n    v_ = bdd.add_expr(r'u \\/ v')\n    assert v == v_\n\n\nif __name__ == '__main__':\n    variable_substitution()\n"
  },
  {
    "path": "examples/what_is_a_bdd.py",
    "content": "\"\"\"How BDDs are implemented.\n\nThis module describes the main characteristics of\nthe data structure used in the module `dd.bdd`.\nThe module `dd.autoref` is an interface to the\nimplementation that is in the module `dd.bdd`.\n\"\"\"\n\n\ndef bdd_implementation_example():\n    \"\"\"Main details of `dd.bdd.BDD`.\n\n    The module `dd.bdd.BDD` contains\n    the Python implementation of\n    binary decision diagrams.\n    \"\"\"\n    # the graph that represents\n    # the BDDs stored in memory\n    successors = {\n        1: (2, None, None),\n            # The node `1` represents the value `TRUE`.\n            # The node `-1` represents the value `FALSE`.\n            # Node `1` is used also as node `-1`.\n            #\n            # The number of \"-\" symbols on the edges\n            # along the path that reaches the node `1`\n            # determines whether `1` will be regarded\n            # as `-1` when reached.\n        2: (1, -1, 1),\n            # node_id:\n            # (level,\n            #  successor_if_false,\n            #  successor_if_true)\n        3: (0, -1, 2)}\n            # Keys are positive integers.\n            # Doing so reduces how much\n            # memory needs to be used.\n    var_to_level = dict(\n        x=0,\n        y=1)\n    values_to_substitute = dict(\n        x=False,\n        y=True)\n    bdd_reference = 3\n        # BDD that means `x /\\ y`,\n        # the conjunction of `x` and `y`.\n    # invert dictionary\n    level_to_var = {\n        level: varname\n        for varname, level in\n            var_to_level.items()}\n    # Exercise: change the implementation\n    # of the function `let()`, so that\n    # `values_to_substitute` can be\n    # an assignment to only some of\n    # the variables that occur in the\n    # BDD given to `let()`.\n    result = let(\n        values_to_substitute,\n        bdd_reference,\n        successors,\n        level_to_var)\n    print(f'{result = }')\n\n\ndef let(\n        values:\n            dict[str, bool],\n        bdd_ref:\n            int,\n        successors:\n            dict[\n                int,\n                tuple[\n                    int,\n                    int | None,\n                    int | None]],\n        level_to_var:\n            dict[\n                int, str]\n        ) -> int:\n    \"\"\"Recursively substitute values for variables.\n\n    Return a binary decision diagram that\n    represents the result of this substitution.\n\n    @param values:\n        assignment of Boolean values to\n        variable names\n    @param bdd_ref:\n        a node, key in successors\n    @param successors:\n        graph that stores nodes\n    @return:\n        BDD node,\n        which is a key in `successors`\n    \"\"\"\n    # leaf ?\n    if abs(bdd_ref) == 1:\n        return bdd_ref\n    # nonleaf node\n    key = abs(bdd_ref)\n    level, low, high = successors[key]\n    variable_name = level_to_var[level]\n    variable_value = values[variable_name]\n    if variable_value:\n        successor = high\n    else:\n        successor = low\n    result = let(\n        values, successor, successors,\n        level_to_var)\n    # copy sign\n    if bdd_ref < 0:\n        result = - result\n    return result\n\n\nif __name__ == '__main__':\n    bdd_implementation_example()\n"
  },
  {
    "path": "setup.py",
    "content": "\"\"\"Installation script.\"\"\"\nimport argparse as _arg\nimport logging\nimport os\nimport sys\n\nimport setuptools\n\nimport download\n\n\nPACKAGE_NAME = 'dd'\nDESCRIPTION = (\n    'Binary decision diagrams implemented in pure Python, '\n    'as well as Cython wrappers of CUDD, Sylvan, and BuDDy.')\nLONG_DESCRIPTION = (\n    'dd is a package for working with binary decision diagrams '\n    'that includes both a pure Python implementation and '\n    'Cython bindings to C libraries (CUDD, Sylvan, BuDDy). '\n    'The Python and Cython modules implement the same API, '\n    'so the same user code runs with both. '\n    'All the standard operations on BDDs are available, '\n    'including dynamic variable reordering using sifting, '\n    'garbage collection, dump/load from files, plotting, '\n    'and a parser of quantified Boolean expressions. '\n    'More details can be found in the README at: '\n    'https://github.com/tulip-control/dd')\nPACKAGE_URL = f'https://github.com/tulip-control/{PACKAGE_NAME}'\nPROJECT_URLS = {\n    'Bug Tracker':\n        'https://github.com/tulip-control/dd/issues',\n    'Documentation':\n        'https://github.com/tulip-control/dd/blob/main/doc.md',\n    'Source Code':\n        'https://github.com/tulip-control/dd'}\nVERSION_FILE = f'{PACKAGE_NAME}/_version.py'\nVERSION = '0.6.1'\nVERSION_FILE_TEXT = (\n    '# This file was generated from setup.py\\n'\n    \"version = '{version}'\\n\")\nPYTHON_REQUIRES = '>=3.11'\nINSTALL_REQUIRES = [\n    'astutils >= 0.0.5',\n    'networkx >= 2.4',\n    'ply >= 3.4, <= 3.10',\n    'setuptools >= 65.6.0']\nTESTS_REQUIRE = [\n    'pytest >= 4.6.11']\nCLASSIFIERS = [\n    'Development Status :: 2 - Pre-Alpha',\n    'Intended Audience :: Developers',\n    'Intended Audience :: Science/Research',\n    'License :: OSI Approved :: BSD License',\n    'Operating System :: OS Independent',\n    'Programming Language :: Cython',\n    'Programming Language :: Python :: 3 :: Only',\n    'Topic :: Scientific/Engineering',\n    'Topic :: Software Development']\nKEYWORDS = [\n    'bdd',\n    'binary decision diagram',\n    'decision diagram',\n    'boolean',\n    'networkx',\n    'dot',\n    'graphviz']\n\n\ndef git_version(\n        version:\n            str\n        ) -> str:\n    \"\"\"Return version with local version identifier.\"\"\"\n    import git as _git\n    repo = _git.Repo('.git')\n    repo.git.status()\n    # assert versions are increasing\n    latest_tag = repo.git.describe(\n        match='v[0-9]*', tags=True, abbrev=0)\n    latest_version = _parse_version(latest_tag[1:])\n    given_version = _parse_version(version)\n    if latest_version > given_version:\n        raise AssertionError(\n            (latest_tag, version))\n    sha = repo.head.commit.hexsha\n    if repo.is_dirty():\n        return f'{version}.dev0+{sha}.dirty'\n    # commit is clean\n    # is it release of `version` ?\n    try:\n        tag = repo.git.describe(\n            match='v[0-9]*', exact_match=True,\n            tags=True, dirty=True)\n    except _git.GitCommandError:\n        return f'{version}.dev0+{sha}'\n    if tag != f'v{version}':\n        raise AssertionError((tag, version))\n    return version\n\n\ndef _parse_version(\n        version:\n            str\n        ) -> tuple[\n            int, int, int]:\n    \"\"\"Return numeric version.\"\"\"\n    numerals = version.split('.')\n    if len(numerals) != 3:\n        raise ValueError(numerals)\n    return tuple(map(int, numerals))\n\n\ndef parse_args(\n        ) -> _arg.Namespace:\n    \"\"\"Return `args` irrelevant to `setuptools`.\"\"\"\n    parser = _arg.ArgumentParser()\n    parser.add_argument(\n        '--fetch',\n        action='store_true',\n        help='download cudd from its website')\n    parser.add_argument(\n        '--linetrace',\n        action='store_true',\n        help='use line tracing for Cython extensions')\n    for opt in download.EXTENSIONS:\n        parser.add_argument(\n            f'--{opt}',\n            default=None,\n            const='',\n            type=str,\n            nargs='?',\n            help=f'build Cython extension {opt}')\n    args, unknown = parser.parse_known_args()\n    args.sdist = 'sdist' in unknown\n    args.bdist_wheel = 'bdist_wheel' in unknown\n    # avoid confusing `setuptools`\n    sys.argv = [sys.argv[0], *unknown]\n    return args\n\n\ndef read_env_vars(\n        ) -> dict:\n    \"\"\"Read relevant environment variables.\"\"\"\n    keys = {\n        k: ''\n        for k in download.EXTENSIONS}\n    keys['fetch'] = True\n    env_vars = {\n        k: v\n        for k, v in keys.items()\n        if f'DD_{k.upper()}' in os.environ}\n    print('`setup.py` of `dd` read environment variables:')\n    print(env_vars)\n    return env_vars\n\n\ndef run_setup(\n        ) -> None:\n    \"\"\"Build parser, get version from `git`, install.\"\"\"\n    env_vars = read_env_vars()\n    args = parse_args()\n    dargs = vars(args)\n    for k, v in env_vars.items():\n        if dargs[k] in (None, False):\n            dargs[k] = v\n    if args.fetch:\n        download.fetch_cudd()\n    # build extensions ?\n    ext_modules = download.extensions(args)\n    # version\n    try:\n        version = git_version(VERSION)\n    except AssertionError:\n        raise\n    except:\n        print('No git info: Assume release.')\n        version = VERSION\n    s = VERSION_FILE_TEXT.format(version=version)\n    with open(VERSION_FILE, 'w') as f:\n        f.write(s)\n    _build_parsers()\n    setuptools.setup(\n        name=PACKAGE_NAME,\n        version=version,\n        description=DESCRIPTION,\n        long_description=LONG_DESCRIPTION,\n        author='Caltech Control and Dynamical Systems',\n        author_email='tulip@tulip-control.org',\n        url=PACKAGE_URL,\n        project_urls=PROJECT_URLS,\n        license='BSD-3-Clause',\n        license_files=['LICENSE'],\n        python_requires=PYTHON_REQUIRES,\n        install_requires=INSTALL_REQUIRES,\n        packages=[PACKAGE_NAME],\n        package_dir={PACKAGE_NAME: PACKAGE_NAME},\n        include_package_data=True,\n        zip_safe=False,\n        ext_modules=ext_modules,\n        classifiers=CLASSIFIERS,\n        keywords=KEYWORDS)\n\n\ndef _build_parsers(\n        ) -> None:\n    \"\"\"Cache each parser's state machine.\"\"\"\n    if not _parser_requirements_installed():\n        return\n    import dd.dddmp\n    import dd._parser\n    logging.getLogger('astutils').setLevel('ERROR')\n    dd.dddmp._rewrite_tables(outputdir=PACKAGE_NAME)\n    dd._parser._rewrite_tables(outputdir=PACKAGE_NAME)\n\n\ndef _parser_requirements_installed(\n        ) -> bool:\n    \"\"\"Return `True` if parser requirements found.\"\"\"\n    try:\n        import astutils\n        import ply\n    except ImportError:\n        print(\n            'WARNING: `dd` could not cache parser tables '\n            '(ignore this if running only for metadata information).')\n        return False\n    return True\n\n\nif __name__ == '__main__':\n    run_setup()\n"
  },
  {
    "path": "tests/.coveragerc",
    "content": "# config for `coverage.py`\n\n[run]\nplugins = Cython.Coverage\n"
  },
  {
    "path": "tests/README.md",
    "content": "Besides tests for the package `dd`, this directory contains the script\n`inspect_cython_signatures.py`, which checks the compliance of a Cython\nclass to a Python specification (motivated by the unavailability of `ABC`\nin Cython).\n"
  },
  {
    "path": "tests/autoref_test.py",
    "content": "\"\"\"Tests of the module `dd.autoref`.\"\"\"\n# This file is released in the public domain.\n#\nimport logging\n\nimport dd.autoref as _bdd\nimport dd.bdd\nimport dd._utils as _utils\nimport pytest\n\nimport common\nimport common_bdd\n\n\nlogging.getLogger('astutils').setLevel('ERROR')\n\n\nclass Tests(common.Tests):\n    def setup_method(self):\n        self.DD = _bdd.BDD\n\n\nclass BDDTests(common_bdd.Tests):\n    def setup_method(self):\n        self.DD = _bdd.BDD\n\n\ndef test_str():\n    bdd = _bdd.BDD()\n    s = str(bdd)\n    s + 'must be a string'\n\n\ndef test_find_or_add():\n    bdd = _bdd.BDD()\n    bdd.declare('x', 'y')\n    n = len(bdd)\n    u = bdd.find_or_add('x', bdd.true, bdd.false)\n    m = len(bdd)\n    assert n < m, (n, m)\n    u_ = bdd.find_or_add('x', bdd.true, bdd.false)\n    assert u == u_, (u, u_)\n    m_ = len(bdd)\n    assert m == m_, (m, m_)\n\n\ndef test_count():\n    bdd = _bdd.BDD()\n    bdd.declare('x', 'y')\n    u = bdd.add_expr('x')\n    n = bdd.count(u)\n    assert n == 1, n\n    u = bdd.add_expr(r'x /\\ y')\n    n = bdd.count(u)\n    assert n == 1, n\n    u = bdd.add_expr(r'x \\/ y')\n    n = bdd.count(u)\n    assert n == 3, n\n\n\ndef test_dump_load():\n    vrs = ['x', 'y', 'z']\n    s = r'x \\/ y \\/ ~ z'\n    fname = 'foo.p'\n    # dump\n    bdd = _bdd.BDD()\n    bdd.declare(*vrs)\n    u = bdd.add_expr(s)\n    bdd.dump(fname, roots=[u])\n    # load\n    other = _bdd.BDD()\n    roots_other = other.load(fname)\n    assert len(roots_other) == 1, roots_other\n    v, = roots_other\n    v_ = other.add_expr(s)\n    assert v == v_, (v, v_)\n\n\ndef test_dump_using_graphviz():\n    bdd = _bdd.BDD()\n    bdd.declare('x', 'y')\n    u = bdd.add_expr(r'x /\\ y')\n    bdd.dump('bdd.dot')\n    bdd.dump('bdd.dot', filetype='dot')\n    bdd.dump('bdd.pdf')\n    bdd.dump('bdd.pdf', filetype='pdf')\n    bdd.dump('bdd', filetype='pdf')\n    bdd.dump('bdd.png')\n    bdd.dump('bdd.png', filetype='png')\n    bdd.dump('bdd.svg')\n    bdd.dump('bdd.svg', filetype='svg')\n    bdd.dump('bdd.ext', filetype='pdf')\n    with pytest.raises(ValueError):\n        bdd.dump('bdd.ext')\n\n\ndef test_image():\n    bdd = _bdd.BDD()\n    bdd.declare('x', 'y', 'z')\n    action = bdd.add_expr('x => y')\n    source = bdd.add_expr('x')\n    qvars = {'x'}\n    rename = dict(y='x')\n    u = _bdd.image(action, source, rename, qvars)\n    u_ = bdd.add_expr('x')\n    assert u == u_\n\n\ndef test_preimage():\n    bdd = _bdd.BDD()\n    bdd.declare('x', 'y', 'z')\n    action = bdd.add_expr('x <=> y')\n    target = bdd.add_expr('x')\n    qvars = {'y'}\n    rename = dict(x='y')\n    u = _bdd.preimage(action, target, rename, qvars)\n    u_ = bdd.add_expr('x')\n    assert u == u_\n\n\ndef test_reorder_2():\n    bdd = _bdd.BDD()\n    vrs = [\n        'x', 'y', 'z', 'a', 'b', 'c', 'e',\n        'z1', 'z2', 'z3', 'y1', 'y2', 'y3']\n    bdd = _bdd.BDD()\n    bdd.declare(*vrs)\n    expr_1 = r'(~ z \\/ (c /\\ b)) /\\ e /\\ (a /\\ (~ x \\/ y))'\n    # Figs. 6.24, 6.25 Baier 2008\n    expr_2 = r'(z1 /\\ y1) \\/ (z2 /\\ y2) \\/ (z3 /\\ y3)'\n    u = bdd.add_expr(expr_1)\n    v = bdd.add_expr(expr_2)\n    bdd.collect_garbage()\n    n = len(bdd)\n    assert n == 23, n\n    bdd.reorder()\n    n_ = len(bdd)\n    assert n > n_, (n, n_)\n    bdd.assert_consistent()\n\n\ndef test_configure_dynamic_reordering():\n    bdd = _bdd.BDD()\n    vrs = [\n        'x', 'y', 'z', 'a', 'b', 'c', 'e',\n        'z1', 'z2', 'z3', 'y1', 'y2', 'y3']\n    expr_1 = r'(~ z \\/ (c /\\ b)) /\\ e /\\ (a /\\ (~ x \\/ y))'\n    expr_2 = r'(z1 /\\ y1) \\/ (z2 /\\ y2) \\/ (z3 /\\ y3)'\n    # without dynamic reordering\n    bdd = _bdd.BDD()\n    bdd.declare(*vrs)\n    u = bdd.add_expr(expr_1)\n    v = bdd.add_expr(expr_2)\n    bdd.collect_garbage()\n    n = len(bdd)\n    assert n == 23, n\n    # with dynamic reordering\n    del u, v, bdd\n    dd.bdd.REORDER_STARTS = 7\n    bdd = _bdd.BDD()\n    bdd.declare(*vrs)\n    bdd.configure(reordering=True)\n    u = bdd.add_expr(expr_1)\n    v = bdd.add_expr(expr_2)\n    bdd.collect_garbage()\n    n = len(bdd)\n    assert n < 23, n\n\n\ndef test_collect_garbage():\n    bdd = _bdd.BDD()\n    n = len(bdd)\n    assert n == 1, n\n    bdd.declare('x', 'y')\n    u = bdd.add_expr(r'x \\/ y')\n    bdd.collect_garbage()\n    n = len(bdd)\n    assert n > 1, n\n    del u\n    bdd.collect_garbage()\n    n = len(bdd)\n    assert n == 1, n\n\n\ndef test_copy_vars():\n    bdd = _bdd.BDD()\n    other = _bdd.BDD()\n    vrs = {'x', 'y', 'z'}\n    bdd.declare(*vrs)\n    _bdd.copy_vars(bdd, other)\n    assert vrs.issubset(other.vars)\n\n\ndef test_copy_bdd():\n    bdd = _bdd.BDD()\n    other = _bdd.BDD()\n    bdd.declare('x')\n    other.declare('x')\n    u = bdd.var('x')\n    v = _bdd.copy_bdd(u, other)\n    v_ = other.var('x')\n    assert v == v_, other.to_expr(v)\n    # involution\n    u_ = _bdd.copy_bdd(v, bdd)\n    assert u == u_, bdd.to_expr(u_)\n\n\ndef test_func_len():\n    bdd = _bdd.BDD()\n    bdd.declare('x', 'y')\n    u = bdd.add_expr('x')\n    n = len(u)\n    assert n == 2, n\n    u = bdd.add_expr(r'x /\\ y')\n    n = len(u)\n    assert n == 3, n\n\n\ndef test_dd_version():\n    import dd\n    assert hasattr(dd, '__version__')\n    version = dd.__version__\n    assert version is not None\n    assert isinstance(version, str), version\n\n\nif __name__ == '__main__':\n    test_str()\n"
  },
  {
    "path": "tests/bdd_test.py",
    "content": "\"\"\"Tests of the module `dd.bdd`.\"\"\"\n# This file is released in the public domain.\n#\nimport logging\nimport os\n\nimport dd.autoref\nimport dd.bdd as _bdd\nimport networkx as nx\nimport networkx.algorithms.isomorphism as iso\nimport pytest\n\n\nclass BDD(_bdd.BDD):\n    \"\"\"Disables refcount check upon shutdown.\n\n    This script tests the low-level manager, where\n    reference counting is not automated. For simplicity,\n    references are not cleared at the end of tests here.\n    Automated reference counting is in `dd.autoref`.\n    \"\"\"\n\n    def __del__(self):\n        pass\n\n\ndef test_add_var():\n    b = BDD()\n    #\n    # automated level selection\n    # first var\n    j = b.add_var('x')\n    assert len(b.vars) == 1, b.vars\n    assert 'x' in b.vars, b.vars\n    assert b.vars['x'] == 0, b.vars\n    assert j == 0, j\n    # second var\n    j = b.add_var('y')\n    assert len(b.vars) == 2, b.vars\n    assert 'y' in b.vars, b.vars\n    assert b.vars['y'] == 1, b.vars\n    assert j == 1, j\n    # third var\n    j = b.add_var('z')\n    assert len(b.vars) == 3, b.vars\n    assert 'z' in b.vars, b.vars\n    assert b.vars['z'] == 2, b.vars\n    assert j == 2, j\n    #\n    # explicit level selection\n    b = BDD()\n    j = b.add_var('x', level=35)\n    assert len(b.vars) == 1, b.vars\n    assert 'x' in b.vars, b.vars\n    assert b.vars['x'] == 35, b.vars\n    assert j == 35, j\n    j = b.add_var('y', level=5)\n    assert len(b.vars) == 2, b.vars\n    assert 'y' in b.vars, b.vars\n    assert b.vars['y'] == 5, b.vars\n    assert j == 5, j\n    # attempt to add var at an existing level\n    with pytest.raises(ValueError):\n        b.add_var('z', level=35)\n    with pytest.raises(ValueError):\n        b.add_var('z', level=5)\n    #\n    # mixing automated and\n    # explicit level selection\n    b = BDD()\n    b.add_var('x', level=2)\n    b.add_var('y')\n    assert len(b.vars) == 2, b.vars\n    assert 'x' in b.vars, b.vars\n    assert 'y' in b.vars, b.vars\n    assert b.vars['x'] == 2, b.vars\n    assert b.vars['y'] == 1, b.vars\n    with pytest.raises(ValueError):\n        b.add_var('z')\n    b.add_var('z', level=0)\n\n\ndef test_var():\n    b = BDD()\n    with pytest.raises(ValueError):\n        b.var('x')\n    j = b.add_var('x')\n    u = b.var('x')\n    assert u > 0, u\n    level, low, high = b.succ(u)\n    assert level == j, (level, j)\n    assert low == b.false, low\n    assert high == b.true, high\n\n\ndef test_assert_consistent():\n    g = two_vars_xy()\n    g.assert_consistent()\n    g = x_or_y()\n    g.assert_consistent()\n    g._succ[2] = (5, 1, 2)\n    with pytest.raises(AssertionError):\n        g.assert_consistent()\n    g = x_or_y()\n    g.roots.add(2)\n    g._succ[4] = (0, 10, 1)\n    with pytest.raises(AssertionError):\n        g.assert_consistent()\n    g = x_or_y()\n    g.roots.add(2)\n    g._succ[1] = (2, None, 1)\n    with pytest.raises(AssertionError):\n        g.assert_consistent()\n    g = x_and_y()\n    g.assert_consistent()\n\n\ndef test_level_to_variable():\n    ordering = {'x': 0, 'y': 1}\n    g = BDD(ordering)\n    assert g.var_at_level(0) == 'x'\n    assert g.var_at_level(1) == 'y'\n    with pytest.raises(ValueError):\n        g.var_at_level(10)\n\n\ndef test_var_levels_attr():\n    bdd = BDD()\n    bdd.declare('x', 'y')\n    var_levels = bdd.var_levels\n    assert len(var_levels) == 2, var_levels\n    assert {'x', 'y'} == set(var_levels), var_levels\n    assert {0, 1} == set(var_levels.values()), var_levels\n\n\ndef test_descendants():\n    ordering = dict(x=0, y=1)\n    b = BDD(ordering)\n    u = b.add_expr(r'x /\\ y')\n    v = b.add_expr(r'x \\/ y')\n    roots = [u, v]\n    nodes = b.descendants(roots)\n    nodes_u = b.descendants([u])\n    nodes_v = b.descendants([v])\n    assert u in nodes_u, nodes_u\n    assert v in nodes_v, nodes_v\n    assert u in nodes, nodes\n    assert v in nodes, nodes\n    assert 1 in nodes_u, nodes_u\n    assert 1 in nodes_v, nodes_v\n    assert 1 in nodes, nodes\n    assert len(nodes_u) == 3, nodes_u\n    assert len(nodes_v) == 3, nodes_v\n    assert nodes_u != nodes_v, (nodes_u, nodes_v)\n    assert len(nodes) == 4, nodes\n    assert nodes == nodes_u.union(nodes_v), (\n        nodes, b._succ)\n    # no roots\n    roots = []\n    nodes = b.descendants(roots)\n    assert len(nodes) == 0, nodes\n    # empty iterator\n    roots = iter(tuple())\n    reachable = b.descendants(roots)\n    assert reachable == set(), reachable\n    # nonempty iterator\n    roots = iter([u, v])\n    reachable = b.descendants(roots)\n    assert u in reachable, reachable\n    assert v in reachable, reachable\n    assert 1 in reachable, reachable\n\n\ndef test_is_essential():\n    g = two_vars_xy()\n    assert g.is_essential(2, 'x')\n    assert not g.is_essential(2, 'y')\n    assert g.is_essential(3, 'y')\n    assert not g.is_essential(3, 'x')\n    g = x_and_y()\n    assert g.is_essential(2, 'x')\n    assert g.is_essential(3, 'y')\n    assert g.is_essential(4, 'x')\n    assert g.is_essential(4, 'y')\n    assert not g.is_essential(3, 'x')\n    assert not g.is_essential(-1, 'x')\n    assert not g.is_essential(-1, 'y')\n    assert not g.is_essential(1, 'x')\n    assert not g.is_essential(1, 'y')\n    # variable not in the ordering\n    assert not g.is_essential(2, 'z')\n\n\ndef test_support():\n    g = two_vars_xy()\n    assert g.support(2) == {'x'}\n    assert g.support(3) == {'y'}\n    g = x_and_y()\n    assert g.support(4) == {'x', 'y'}\n    assert g.support(3) == {'y'}\n    g = x_or_y()\n    assert g.support(4) == {'x', 'y'}\n    assert g.support(3) == {'y'}\n\n\ndef test_count():\n    g = x_and_y()\n    assert g.count(4) == 1\n    g = x_or_y()\n    r = g.count(4)\n    assert r == 3, r\n    r = g.count(4, nvars=2)\n    assert r == 3, r\n    r = g.count(-4)\n    assert r == 1, r\n    r = g.count(-4, nvars=2)\n    assert r == 1, r\n    r = g.count(4, 3)\n    assert r == 6, r\n    r = g.count(-4, 3)\n    assert r == 2, r\n    with pytest.raises(Exception):\n        g.count()\n    r = g.count(4)\n    assert r == 3, r\n    g = _bdd.BDD()\n    r = g.count(g.false)\n    assert r == 0, r\n    r = g.count(g.true)\n    assert r == 1, r\n    g.add_var('x')\n    x = g.var('x')\n    r = g.count(x)\n    assert r == 1, r\n    neg_x = g.add_expr('~x')\n    r = g.count(neg_x)\n    assert r == 1, r\n    g.add_var('y')\n    u = g.add_expr(r'x /\\ y ')\n    r = g.count(u)\n    assert r == 1, r\n\n\ndef test_pick_iter():\n    # x /\\ y\n    g = x_and_y()\n    u = 4\n    bits = {'x', 'y'}\n    s = [{'x': 1, 'y': 1}]\n    compare_iter_to_list_of_sets(u, g, s, bits)\n    # care_bits == support (default)\n    bits = None\n    compare_iter_to_list_of_sets(u, g, s, bits)\n    #\n    # x \\/ y\n    g = x_or_y()\n    u = 4\n    # support\n    bits = None\n    s = [{'x': 1, 'y': 0}, {'x': 1, 'y': 1},\n         {'x': 0, 'y': 1}]\n    compare_iter_to_list_of_sets(u, g, s, bits)\n    # only what appears along traversal\n    bits = set()\n    s = [{'x': 1}, {'x': 0, 'y': 1}]\n    compare_iter_to_list_of_sets(u, g, s, bits)\n    # bits < support\n    bits = {'x'}\n    s = [{'x': 1}, {'x': 0, 'y': 1}]\n    compare_iter_to_list_of_sets(u, g, s, bits)\n    bits = {'y'}\n    s = [{'x': 1, 'y': 0},{'x': 1, 'y': 1},\n         {'x': 0, 'y': 1}]\n    compare_iter_to_list_of_sets(u, g, s, bits)\n    #\n    # x /\\ ~ y\n    g = x_and_not_y()\n    u = -2\n    bits = {'x', 'y'}\n    s = [{'x': 1, 'y': 0}]\n    compare_iter_to_list_of_sets(u, g, s, bits)\n    # gaps in order\n    order = {'x': 0, 'y': 1, 'z': 2}\n    bdd = BDD(order)\n    u = bdd.add_expr(r'x /\\ z')\n    (m,) = bdd.pick_iter(u)\n    assert m == {'x': 1, 'z': 1}, m\n\n\ndef compare_iter_to_list_of_sets(u, g, s, care_bits):\n    s = list(s)\n    for d in g.pick_iter(u, care_bits):\n        assert d in s, d\n        s.remove(d)\n    assert not s, s\n\n\ndef test_enumerate_minterms():\n    # non-empty cube\n    cube = dict(x=False)\n    bits = ['x', 'y', 'z']\n    r = _bdd._enumerate_minterms(cube, bits)\n    p = set_from_generator_of_dict(r)\n    q = set()\n    for y in (False, True):\n        for z in (False, True):\n            m = (('x', False), ('y', y), ('z', z))\n            q.add(m)\n    assert p == q, (p, q)\n    # empty cube\n    cube = dict()\n    bits = ['x', 'y', 'z']\n    r = _bdd._enumerate_minterms(cube, bits)\n    p = set_from_generator_of_dict(r)\n    q = set()\n    for x in (False, True):\n        for y in (False, True):\n            for z in (False, True):\n                m = (('x', x), ('y', y), ('z', z))\n                q.add(m)\n    assert p == q, (p, q)\n    # fewer bits than cube\n    cube = dict(x=False, y=True)\n    bits = set()\n    r = _bdd._enumerate_minterms(cube, bits)\n    p = set_from_generator_of_dict(r)\n    q = {(('x', False), ('y', True))}\n    assert p == q, (p, q)\n\n\ndef set_from_generator_of_dict(gen):\n    r = list(gen)\n    p = {tuple(sorted(m.items(), key=lambda x: x[0]))\n         for m in r}\n    return p\n\n\ndef test_isomorphism():\n    ordering = {'x': 0}\n    g = BDD(ordering)\n    g.roots.update([2, 3])\n    g._succ[2] = (0, -1, 1)\n    g._succ[3] = (0, -1, 1)\n    h = g.reduction()\n    assert set(h) == {1, 2}, set(h)\n    assert 0 not in h\n    assert h._succ[1] == (1, None, None)\n    assert h._succ[2] == (0, -1, 1)\n    assert h.roots == {2}\n\n\ndef test_elimination():\n    ordering = {'x': 0, 'y': 1}\n    g = BDD(ordering)\n    g.roots.add(2)\n    # high == low, so node 2 is redundant\n    g._succ[2] = (0, 3, 3)\n    g._succ[3] = (1, -1, 1)\n    h = g.reduction()\n    assert set(h) == {1, 2}\n\n\ndef test_reduce_combined():\n    \"\"\"Fig.5 in 1986 Bryant TOC\"\"\"\n    ordering = {'x': 0, 'y': 1, 'z': 2}\n    g = BDD(ordering)\n    g.roots.add(2)\n    g._succ[2] = (0, 3, 4)\n    g._succ[3] = (1, -1, 5)\n    g._succ[4] = (1, 5, 6)\n    g._succ[5] = (2, -1, 1)\n    g._succ[6] = (2, -1, 1)\n    h = g.reduction()\n    assert 1 in h\n    assert ordering == h.vars\n\n    r = nx.MultiDiGraph()\n    r.add_node(1, level=3)\n    r.add_node(2, level=0)\n    r.add_node(3, level=1)\n    r.add_node(4, level=2)\n\n    r.add_edge(2, 3, value=False, complement=False)\n    r.add_edge(2, 4, value=True, complement=False)\n    r.add_edge(3, 4, value=True, complement=False)\n    r.add_edge(3, 1, value=False, complement=True)\n    r.add_edge(4, 1, value=False, complement=True)\n    r.add_edge(4, 1, value=True, complement=False)\n\n    (u, ) = h.roots\n    compare(u, h, r)\n\n\ndef test_reduction_complemented_edges():\n    bdd = BDD()\n    bdd.add_var('x', level=0)\n    bdd.add_var('y', level=1)\n    a, b = map(bdd.level_of_var, ['x', 'y'])\n    assert a < b, (a, b)\n    # complemented edge from internal node to\n    # non-terminal node\n    expr = r'~ x /\\ y'\n    _test_reduction_complemented_edges(expr, bdd)\n    # complemented edge from external reference to\n    # non-terminal node\n    expr = r'x /\\ ~ y'\n    u = bdd.add_expr(expr)\n    assert u < 0, u\n    _test_reduction_complemented_edges(expr, bdd)\n\n\ndef _test_reduction_complemented_edges(expr, bdd):\n    u = bdd.add_expr(expr)\n    bdd.roots.add(u)\n    bdd_r = bdd.reduction()\n    v, = bdd_r.roots\n    v_ = bdd_r.add_expr(expr)\n    assert v == v_, (v, v_)\n    bdd_r.assert_consistent()\n    bdd.roots.remove(u)\n\n\ndef test_find_or_add():\n    ordering = {'x': 0, 'y': 1}\n    g = BDD(ordering)\n    # init\n    n = len(g)\n    m = g._min_free\n    assert n == 1, n\n    assert m == 2, m\n    # elimination rule\n    i = 0\n    v = -1\n    w = 1\n    n = len(g)\n    u = g.find_or_add(i, v, v)\n    n_ = len(g)\n    assert n == n_, (n, n_)\n    assert u == v, (u, v)\n    assert len(g._pred) == 1, g._pred\n    t = (2, None, None)\n    assert t in g._pred, g._pred\n    assert g._pred[t] == 1, g._pred\n    # unchanged min_free\n    v = 1\n    m = g._min_free\n    g.find_or_add(i, v, v)\n    m_ = g._min_free\n    assert m_ == m, (m_, m)\n    # add new node\n    g = BDD(ordering)\n    v = -1\n    w = 1\n    n = len(g)\n    m = g._min_free\n    assert n == 1, n\n    u = g.find_or_add(i, v, w)\n    n_ = len(g)\n    m_ = g._min_free\n    assert u != v, (u, v)\n    assert n_ == n + 1, (n, n_)\n    assert m_ == m + 1, (m, m_)\n    assert g._succ[u] == (i, -1, 1)\n    assert (i, v, w) in g._pred\n    assert abs(u) in g._ref\n    assert g._ref[abs(u)] == 0\n    # terminal node `v`: 2 refs + 1 ref by manager\n    assert g._ref[abs(v)] == 3, g._ref\n    # independent increase of reference counters\n    v = u\n    w = w\n    refv = g._ref[abs(v)]\n    refw = g._ref[w]\n    u = g.find_or_add(i, v, w)\n    refv_ = g._ref[abs(v)]\n    refw_ = g._ref[w]\n    assert refv + 1 == refv_, (refv, refv_)\n    assert refw + 1 == refw_, (refw, refw_)\n    # add existing\n    n = len(g)\n    m = g._min_free\n    refv = g._ref[abs(v)]\n    refw = g._ref[w]\n    r = g.find_or_add(i, v, w)\n    n_ = len(g)\n    m_ = g._min_free\n    refv_ = g._ref[abs(v)]\n    refw_ = g._ref[w]\n    assert n == n_, (n, n_)\n    assert m == m_, (m, m_)\n    assert u == r, u\n    assert refv == refv_, (refv, refv_)\n    assert refw == refw_, (refw, refw_)\n    # only non-terminals can be added\n    with pytest.raises(ValueError):\n        g.find_or_add(2, -1, 1)\n    # low and high must already exist\n    with pytest.raises(ValueError):\n        g.find_or_add(0, 3, 4)\n    # canonicity of complemented edges\n    # v < 0, w > 0\n    g = BDD(ordering)\n    i = 0\n    v = -1\n    w = 1\n    u = g.find_or_add(i, v, w)\n    assert u > 0, u\n    # v > 0, w < 0\n    v = 1\n    w = -1\n    u = g.find_or_add(i, v, w)\n    assert u < 0, u\n    assert abs(u) in g._succ, u\n    _, v, w = g._succ[abs(u)]\n    assert v < 0, v\n    assert w > 0, w\n    # v < 0, w < 0\n    v = -1\n    w = -2\n    u = g.find_or_add(i, v, w)\n    assert u < 0, u\n    _, v, w = g._succ[abs(u)]\n    assert v > 0, v\n    assert w > 0, w\n\n\ndef test_next_free_int():\n    g = BDD()\n    # contiguous\n    g._succ = {1, 2, 3}\n    start = 1\n    n = g._next_free_int(start)\n    _assert_smaller_are_nodes(start, g)\n    assert n == 4, n\n    start = 3\n    n = g._next_free_int(start)\n    _assert_smaller_are_nodes(start, g)\n    assert n == 4, n\n    # with blanks\n    g._succ = {1, 3}\n    start = 1\n    n = g._next_free_int(start)\n    _assert_smaller_are_nodes(start, g)\n    assert n == 2, n\n    n = g._next_free_int(start=3)\n    assert n == 4, n\n    # full\n    g._succ = {1, 2, 3}\n    g.max_nodes = 3\n    with pytest.raises(Exception):\n        g._next_free_int(start=1)\n\n\ndef _assert_smaller_are_nodes(start, bdd):\n    for i in range(1, start + 1):\n        assert i in bdd, i\n\n\ndef test_collect_garbage():\n    # all nodes are garbage\n    g = BDD({'x': 0, 'y': 1})\n    u = g.add_expr(r'x /\\ y')\n    n = len(g)\n    assert n == 4, n\n    uref = g._ref[abs(u)]\n    assert uref == 0, uref\n    _, v, w = g._succ[abs(u)]\n    vref = g._ref[abs(v)]\n    wref = g._ref[w]\n    # terminal node `v`: 6 refs + 1 ref by manager\n    assert vref == 6, vref\n    assert wref == 1, wref\n    g.collect_garbage()\n    n = len(g)\n    assert n == 1, n\n    assert u not in g, g._succ\n    assert w not in g, g._succ\n    # some nodes not garbage\n    # projection of x is garbage\n    g = BDD({'x': 0, 'y': 1})\n    u = g.add_expr(r'x /\\ y')\n    n = len(g)\n    assert n == 4, n\n    g._ref[abs(u)] += 1\n    uref = g._ref[abs(u)]\n    assert uref == 1, uref\n    g.collect_garbage()\n    n = len(g)\n    assert n == 3, n\n\n\ndef test_top_cofactor():\n    ordering = {'x': 0, 'y': 1}\n    g = BDD(ordering)\n    x = ordering['x']\n    y = ordering['y']\n    u = g.find_or_add(y, -1, 1)\n    assert g._top_cofactor(u, x) == (u, u)\n    assert g._top_cofactor(u, y) == (-1, 1)\n    u = g.find_or_add(x, -1, 1)\n    assert g._top_cofactor(u, x) == (-1, 1)\n    assert g._top_cofactor(-u, x) == (1, -1)\n\n\ndef test_ite():\n    ordering = {'x': 0, 'y': 1}\n    g = BDD(ordering)\n    # x\n    ix = ordering['x']\n    x = g.find_or_add(ix, -1, 1)\n    h = ref_var(ix)\n    compare(x, g, h)\n    # y\n    iy = ordering['y']\n    y = g.find_or_add(iy, -1, 1)\n    h = ref_var(iy)\n    compare(y, g, h)\n    # x and y\n    u = g.ite(x, y, -1)\n    h = ref_x_and_y()\n    compare(u, g, h)\n    # x or y\n    u = g.ite(x, 1, y)\n    h = ref_x_or_y()\n    compare(u, g, h)\n    # negation\n    assert g.ite(x, -1, 1) == -x, g._succ\n    assert g.ite(-x, -1, 1) == x, g._succ\n\n\ndef test_add_expr():\n    ordering = {'x': 0, 'y': 1}\n    g = BDD(ordering)\n    # x\n    ix = ordering['x']\n    u = g.add_expr('x')\n    h = ref_var(ix)\n    compare(u, g, h)\n    # x and y\n    u = g.add_expr(r'x /\\ y')\n    h = ref_x_and_y()\n    compare(u, g, h)\n\n\ndef test_expr_comments():\n    bdd = BDD()\n    bdd.declare('x', 'y', 'z', 'w')\n    expr = r'''\n        \\* trailing comment\n        (x \\/ (\n        (* doubly-delimited comment *)\n            y /\\ ~ z))\n        (* multiline\n        comment *)\n        /\\ w\n        '''\n    expr_ = r'(x \\/ (y /\\ ~ z)) /\\ w'\n    u = bdd.add_expr(expr)\n    u_ = bdd.add_expr(expr_)\n    assert u == u_, (u, u_)\n\n\ndef test_compose():\n    ordering = {'x': 0, 'y': 1, 'z': 2}\n    g = BDD(ordering)\n    # x /\\ (x \\/ z)\n    a = g.add_expr(r'x /\\ y')\n    b = g.add_expr(r'x \\/ z')\n    c = g.let({'y': b}, a)\n    d = g.add_expr(r'x /\\ (x \\/ z)')\n    assert c == d, (c, d)\n    # (y \\/ z) /\\ x\n    ordering = {'x': 0, 'y': 1, 'z': 2, 'w': 3}\n    g = BDD(ordering)\n    a = g.add_expr(r'(x /\\ y) \\/ z')\n    b = g.add_expr(r'(y \\/ z) /\\ x')\n    c = g.let({'z': b}, a)\n    assert c == b, (c, b)\n    # long expr\n    ordering = {'x': 0, 'y': 1, 'z': 2, 'w': 3}\n    g = BDD(ordering)\n    a = g.add_expr(r'(x /\\ y) \\/ (~ z \\/ (w /\\ y /\\ x))')\n    b = g.add_expr(r'(y \\/ ~ z) /\\ x')\n    c = g.let({'y': b}, a)\n    d = g.add_expr(\n        r'(x /\\ ((y \\/ ~ z) /\\ x)) \\/ '\n        r' (~ z \\/ (w /\\ ((y \\/ ~ z) /\\ x) /\\ x))')\n    assert c == d, (c, d)\n    # complemented edges\n    ordering = {'x': 0, 'y': 1}\n    g = BDD(ordering)\n    f = g.add_expr('x <=> y')\n    var = 'y'\n    new_level = 0\n    var_node = g.find_or_add(new_level, -1, 1)\n    u = g.let({var: var_node}, f)\n    assert u == 1, g.to_expr(u)\n\n\ndef test_vector_compose():\n    bdd = _bdd.BDD()\n    bdd.declare('w', 'z', 'x', 'y')\n    u = bdd.add_expr(\n        r'(x /\\ y) \\/ (z /\\ y)')\n    x = bdd.var('x')\n    not_y = bdd.add_expr('~ y')\n    defs = dict(w=bdd.false, y=not_y)\n    v = bdd.let(defs, u)\n        # cache test:\n        # repeated expression\n        # changes edge sign, and\n        # is found in cache\n    v_ = bdd.add_expr(\n        r'(x /\\ ~ y) \\/ (z /\\ ~ y)')\n    assert v == v_, (v, v_)\n\n\ndef test_cofactor():\n    ordering = {'x': 0, 'y': 1, 'z': 2}\n    g = BDD(ordering)\n    # u not in g\n    with pytest.raises(ValueError):\n        g.let({'x': False, 'y': True, 'z': False}, 5)\n    # x /\\ y\n    e = g.add_expr(r'x /\\ y')\n    x = g.add_expr('x')\n    assert g.let({'x': False}, x) == -1\n    assert g.let({'x': True}, x) == 1\n    assert g.let({'x': False}, -x) == 1\n    assert g.let({'x': True}, -x) == -1\n    y = g.add_expr('y')\n    assert g.let({'x': True}, e) == y\n    assert g.let({'x': False}, e) == -1\n    assert g.let({'y': True}, e) == x\n    assert g.let({'y': False}, e) == -1\n\n    assert g.let({'x': False}, -e) == 1\n    assert g.let({'x': True}, -e) == -y\n    assert g.let({'y': False}, -e) == 1\n    assert g.let({'y': True}, -e) == -x\n\n\ndef test_swap():\n    # x, y\n    g = BDD({'x': 0, 'y': 1})\n    x = g.add_expr('x')\n    y = g.add_expr('y')\n    g.incref(x)\n    g.incref(y)\n    n = len(g)\n    assert n == 3, n\n    nold, n = g.swap('x', 'y')\n    assert n == 3, n\n    assert nold == n, nold\n    assert g.vars == {'y': 0, 'x': 1}, g.vars\n    g.assert_consistent()\n    # functions remain invariant\n    x_ = g.add_expr('x')\n    y_ = g.add_expr('y')\n    assert x == x_, (x, x_, g._succ)\n    assert y == y_, (y, y_, g._succ)\n    # external reference counts remain unchanged\n    assert g._ref[abs(x)] == 1\n    assert g._ref[abs(y)] == 1\n    # x /\\ y\n    g = BDD({'x': 0, 'y': 1})\n    u = g.add_expr(r'x /\\ y')\n    g.incref(u)\n    nold, n = g.swap('x', 'y')\n    assert nold == n, (nold, n)\n    assert g.vars == {'y': 0, 'x': 1}, g.vars\n    u_ = g.add_expr(r'x /\\ y')\n    assert u == u_, (u, u_)\n    g.assert_consistent()\n    # reference counts unchanged\n    assert g._ref[abs(u)] == 1\n    # x /\\ ~ y\n    # tests handling of complement edges\n    e = r'x /\\ ~ y'\n    g = x_and_not_y()\n    u = g.add_expr(e)\n    g.incref(u)\n    g.collect_garbage()\n    n = len(g)\n    assert n == 3, n\n    nold, n = g.swap('x', 'y')\n    assert n == 3, n\n    assert nold == n, nold\n    assert g.vars == {'x': 1, 'y': 0}\n    g.assert_consistent()\n    u_ = g.add_expr(e)\n    # function u must have remained unaffected\n    assert u_ == u, (u, u_, g._succ)\n    # invert swap of:\n    # x /\\ ~ y\n    nold, n = g.swap('x', 'y')\n    assert n == 3, n\n    assert nold == n, nold\n    assert g.vars == {'x': 0, 'y': 1}\n    g.assert_consistent()\n    u_ = g.add_expr(e)\n    assert u_ == u, (u, u_, g._succ)\n    # Figs. 6.24, 6.25 Baier 2008\n    g = BDD({'z1': 0, 'y1': 1, 'z2': 2,\n             'y2': 3, 'z3': 4, 'y3': 5})\n    u = g.add_expr(r'(z1 /\\ y1) \\/ (z2 /\\ y2) \\/ (z3 /\\ y3)')\n    g.incref(u)\n    n = len(g)\n    assert n == 16, n\n    g.collect_garbage()\n    n = len(g)\n    assert n == 7, n\n    # sift to inefficient order\n    g.swap('y1', 'z2')  # z1, z2, y1, y2, z3, y3\n    g.swap('y2', 'z3')  # z1, z2, y1, z3, y2, y3\n    g.swap('y1', 'z3')  # z1, z2, z3, y1, y2, y3\n    n = len(g)\n    assert n == 15, n\n    g.assert_consistent()\n    new_ordering = {\n        'z1': 0, 'z2': 1, 'z3': 2,\n        'y1': 3, 'y2': 4, 'y3': 5}\n    assert g.vars == new_ordering, g.vars\n    u_ = g.add_expr(r'(z1 /\\ y1) \\/ (z2 /\\ y2) \\/ (z3 /\\ y3)')\n    assert u_ == u, (u, u_, g._succ)\n    # g.dump('g.pdf')\n\n\ndef test_sifting():\n    # Figs. 6.24, 6.25 Baier 2008\n    g = BDD({'z1': 0, 'z2': 1, 'z3': 2,\n             'y1': 3, 'y2': 4, 'y3': 5})\n    u = g.add_expr(r'(z1 /\\ y1) \\/ (z2 /\\ y2) \\/ (z3 /\\ y3)')\n    g.incref(u)\n    g.collect_garbage()\n    n = len(g)\n    assert n == 15, n\n    _bdd.reorder(g)\n    n_ = len(g)\n    assert n > n_, (n, n_)\n    u_ = g.add_expr(r'(z1 /\\ y1) \\/ (z2 /\\ y2) \\/ (z3 /\\ y3)')\n    g.incref(u)\n    g.collect_garbage()\n    g.assert_consistent()\n    assert u == u_, (u, u_)\n\n\ndef test_request_reordering():\n    ctx = Dummy()\n    # reordering off\n    n = ctx._last_len\n    assert n is None, n\n    _bdd._request_reordering(ctx)\n    # reordering on\n    ctx._last_len = 1\n    ctx.length = 3  # >= 2 = 2 * _last_len\n    # large growth\n    with pytest.raises(_bdd._NeedsReordering):\n        _bdd._request_reordering(ctx)\n    ctx._last_len = 2\n    ctx.length = 3  # < 4 = 2 * _last_len\n    # small growth\n    _bdd._request_reordering(ctx)\n\n\ndef test_reordering_context():\n    ctx = Dummy()\n    # top context\n    ctx.assert_(False)\n    with _bdd._ReorderingContext(ctx):\n        ctx.assert_(True)\n        raise _bdd._NeedsReordering()\n    ctx.assert_(False)\n    # nested context\n    ctx._reordering_context = True\n    with pytest.raises(_bdd._NeedsReordering):\n        with _bdd._ReorderingContext(ctx):\n            ctx.assert_(True)\n            raise _bdd._NeedsReordering()\n    ctx.assert_(True)\n    # other exception\n    ctx._reordering_context = False\n    with pytest.raises(AssertionError):\n        with _bdd._ReorderingContext(ctx):\n            ctx.assert_(True)\n            raise AssertionError()\n    ctx.assert_(False)\n    ctx._reordering_context = True\n    with pytest.raises(Exception):\n        with _bdd._ReorderingContext(ctx):\n            raise Exception()\n    ctx.assert_(True)\n\n\nclass Dummy:\n    \"\"\"To test state machine for nesting context.\"\"\"\n\n    def __init__(self):\n        self._reordering_context = False\n        self._last_len = None\n        self.length = 1\n\n    def __len__(self):\n        return self.length\n\n    def assert_(self, value):\n        c = self._reordering_context\n        assert c is value, c\n\n\ndef test_dynamic_reordering():\n    b = TrackReorderings()\n    [b.add_var(var) for var in ['x', 'y', 'z', 'a', 'b', 'c', 'e']]\n    # add expr with reordering off\n    assert not b.reordering_is_on()\n    assert b.n_swaps == 0, b.n_swaps\n    u = b.add_expr(r'x /\\ y /\\ z')\n    assert b.n_swaps == 0, b.n_swaps\n    b.incref(u)\n    n = len(b)\n    assert n == 7, n\n    # add expr with reordering on\n    b._last_len = 6\n    assert b.reordering_is_on()\n    v = b.add_expr(r'a /\\ b')\n    assert b.reordering_is_on()\n    assert b.n_swaps == 0, b.n_swaps\n    b.incref(v)\n    n = len(b)\n    assert n == 10, n\n    # add an expr that triggers reordering\n    assert b.reordering_is_on()\n    w = b.add_expr(r'z \\/ (~ a /\\ x /\\ ~ y)')\n    assert b.reordering_is_on()\n    n_swaps = b.n_swaps\n    assert n_swaps > 0, n_swaps\n    b.incref(w)\n    assert u in b, (w, b._succ)\n    assert v in b, (v, b._succ)\n    assert w in b, (w, b._succ)\n    # add another expr that triggers reordering\n    old_n_swaps = n_swaps\n    assert b.reordering_is_on()\n    r = b.add_expr(r'(~ z \\/ (c /\\ b)) /\\ e /\\ (a /\\ (~x \\/ y))')\n    b.add_expr(r'(e \\/ ~ a) /\\ x /\\ (b \\/ ~ y)')\n    n_swaps = b.n_swaps\n    assert n_swaps > old_n_swaps, (n_swaps, old_n_swaps)\n    assert b.reordering_is_on()\n\n\nclass TrackReorderings(BDD):\n    \"\"\"To record invocations of reordering.\"\"\"\n\n    def __init__(self, *arg, **kw):\n        self.n_swaps = 0\n        super().__init__(*arg, **kw)\n\n    def swap(self, *arg, **kw):\n        self.n_swaps += 1\n        return super().swap(*arg, **kw)\n\n    def reordering_is_on(self):\n        d = self.configure()\n        r = d['reordering']\n        return r is True\n\n\ndef test_undeclare_vars():\n    bdd = BDD()\n    bdd.declare('x', 'y', 'z', 'w')\n    # empty arg `vrs`\n    u = bdd.add_expr(r'x /\\ y /\\ w')\n    rm_vars = bdd.undeclare_vars()\n    rm_vars_ = {'z'}\n    assert rm_vars == rm_vars_, (rm_vars, rm_vars_)\n    bdd_vars_ = dict(x=0, y=1, w=2)\n    assert bdd.vars == bdd_vars_, bdd.vars\n    bdd.assert_consistent()\n    # nonempty `vrs` with all empty levels\n    bdd = BDD()\n    bdd.declare('x', 'y', 'z', 'w')\n    u = bdd.add_expr(r'y /\\ w')\n    rm_vars = bdd.undeclare_vars('x', 'z')\n    rm_vars_ = {'x', 'z'}\n    assert rm_vars == rm_vars_, (rm_vars, rm_vars_)\n    bdd_vars_ = dict(y=0, w=1)\n    assert bdd.vars == bdd_vars_, bdd.vars\n    bdd.assert_consistent()\n    # nonempty `vrs` without all empty levels\n    bdd = BDD()\n    bdd.declare('x', 'y', 'z', 'w')\n    u = bdd.add_expr(r'y /\\ w')\n    rm_vars = bdd.undeclare_vars('z')\n    rm_vars_ = {'z'}\n    assert rm_vars == rm_vars_, (rm_vars, rm_vars_)\n    bdd_vars_ = dict(x=0, y=1, w=2)\n    assert bdd.vars == bdd_vars_, bdd.vars\n    bdd.assert_consistent()\n    # remove only unused variables\n    bdd = BDD()\n    bdd.declare('x', 'y', 'z', 'w')\n    u = bdd.add_expr(r'y /\\ w')\n    with pytest.raises(ValueError):\n        bdd.undeclare_vars('z', 'y')\n\n\ndef test_del_repeated_calls():\n    bdd = _bdd.BDD()\n    bdd.declare('x', 'y', 'z')\n    u = bdd.add_expr(r'x /\\ y')\n    v = bdd.add_expr(r'y /\\ ~ z')\n    assert _references_exist(bdd._ref)\n    bdd.__del__()\n    assert not _references_exist(bdd._ref)\n    assert bdd._ref == {1: 0}, bdd._ref\n    assert set(bdd._succ) == {1}, bdd._succ\n    bdd.__del__()\n    assert bdd._ref == {1: 0}, bdd._ref\n    assert set(bdd._succ) == {1}, bdd._succ\n    bdd.__del__()\n    assert bdd._ref == {1: 0}, bdd._ref\n    assert set(bdd._succ) == {1}, bdd._succ\n\n\ndef _references_exist(refs):\n    return any(\n        v != 0\n        for v in refs.values())\n\n\ndef test_dump_load():\n    prefix = 'test_dump_load'\n    fname = f'{prefix}.p'\n    dvars = dict(x=0, y=1)\n    # dump\n    b = BDD(dvars)\n    e = r'x /\\ ~ y'\n    u_dumped = b.add_expr(e)\n    b.dump(fname, [u_dumped])\n    # load\n    b = BDD(dvars)\n    b.add_expr(r'x \\/ y')\n    u_new = b.add_expr(e)\n    u_loaded, = b.load(fname)\n    assert u_loaded == u_new, (\n        u_dumped, u_loaded, u_new)\n    b.assert_consistent()\n\n\ndef test_dump_load_manager():\n    prefix = 'test_dump_load_manager'\n    g = BDD({'x': 0, 'y': 1})\n    e = r'x /\\ ~ y'\n    u = g.add_expr(e)\n    g.incref(u)\n    fname = f'{prefix}.p'\n    g._dump_manager(fname)\n    h = g._load_manager(fname)\n    g.assert_consistent()\n    u_ = h.add_expr(e)\n    assert u == u_, (u, u_)\n    # h.dump(f'{prefix}.pdf')\n\n\ndef test_dump_using_graphviz():\n    bdd = BDD()\n    bdd.declare('x', 'y', 'z')\n    u = bdd.add_expr(r'x /\\ y')\n    v = bdd.add_expr(r'y /\\ ~ z')\n    roots = [u, v]\n    filename_noext = 'bdd'\n    filetypes = ['pdf', 'png', 'svg', 'dot']\n    for filetype in filetypes:\n        _dump_bdd_roots_as_filetype(\n            roots, bdd, filename_noext, filetype)\n\n\ndef _dump_bdd_roots_as_filetype(\n        roots, bdd, filename_noext, filetype):\n    fname = f'{filename_noext}.{filetype}'\n    if os.path.isfile(fname):\n        os.remove(fname)\n    bdd.dump(fname, roots)\n    assert os.path.isfile(fname)\n\n\ndef test_quantify():\n    ordering = {'x': 0, 'y': 1, 'z': 2}\n    g = BDD(ordering)\n    # x /\\ y\n    e = g.add_expr(r'x /\\ ~ y')\n    x = g.add_expr('x')\n    not_y = g.add_expr('~ y')\n    assert g.quantify(e, {'x'}) == not_y\n    assert g.quantify(e, {'x'}, forall=True) == -1\n    assert g.quantify(e, {'y'}) == x\n    assert g.quantify(e, {'x'}, forall=True) == -1\n    # x \\/ y \\/ z\n    e = g.add_expr(r'x \\/ y \\/ z')\n    xy = g.add_expr(r'x \\/ y')\n    yz = g.add_expr(r'y \\/ z')\n    zx = g.add_expr(r'z \\/ x')\n    assert g.quantify(e, {'x'})\n    assert g.quantify(e, {'y'})\n    assert g.quantify(e, {'z'})\n    assert g.quantify(e, {'z'}, forall=True) == xy\n    assert g.quantify(e, {'x'}, forall=True) == yz\n    assert g.quantify(e, {'y'}, forall=True) == zx\n    # complement edges\n    u = -x\n    v = g.quantify(u, {'y'}, forall=True)\n    assert v == -x, g.to_expr(v)\n    # multiple values: test recursion\n    e = g.add_expr(r'x /\\ y /\\ z')\n    x = g.add_expr('x')\n    r = g.quantify(e, {'y', 'z'})\n    assert r == x, r\n\n\ndef test_quantifier_syntax():\n    b = BDD()\n    [b.add_var(var) for var in ['x', 'y']]\n    # constants\n    u = b.add_expr(r'\\E x:  TRUE')\n    assert u == b.true, u\n    u = b.add_expr(r'\\E x, y:  TRUE')\n    assert u == b.true, u\n    u = b.add_expr(r'\\E x:  FALSE')\n    assert u == b.false, u\n    u = b.add_expr(r'\\A x:  TRUE')\n    assert u == b.true, u\n    u = b.add_expr(r'\\A x:  FALSE')\n    assert u == b.false, u\n    u = b.add_expr(r'\\A x, y:  FALSE')\n    assert u == b.false, u\n    # variables\n    u = b.add_expr(r'\\E x:  x')\n    assert u == b.true, u\n    u = b.add_expr(r'\\A x:  x')\n    assert u == b.false, u\n    u = b.add_expr(r'\\E x, y:  x')\n    assert u == b.true, u\n    u = b.add_expr(r'\\E x, y:  y')\n    assert u == b.true, u\n    u = b.add_expr(r'\\A x:  y')\n    assert u == b.var('y'), u\n    u = b.add_expr(r'\\A x:  ~ y')\n    u_ = b.apply('not', b.var('y'))\n    assert u == u_, (u, u_)\n\n\ndef test_rename():\n    ordering = {'x': 0, 'xp': 1}\n    g = BDD(ordering)\n    x = g.add_expr('x')\n    xp = g.add_expr('xp')\n    dvars = {'x': 'xp'}\n    xrenamed = g.let(dvars, x)\n    assert xrenamed == xp, xrenamed\n    ordering = {'x': 0, 'xp': 1,\n                'y': 2, 'yp': 3,\n                'z': 4, 'zp': 5}\n    g = BDD(ordering)\n    u = g.add_expr(r'x /\\ y /\\ ~ z')\n    dvars = {'x': 'xp', 'y': 'yp', 'z': 'zp'}\n    urenamed = g.let(dvars, u)\n    up = g.add_expr(r'xp /\\ yp /\\ ~ zp')\n    assert urenamed == up, urenamed\n    # assertion violations\n    # non-neighbors\n    dvars = {'x': 'yp'}\n    r = g.let(dvars, u)\n    r_ = g.add_expr(r'yp /\\ y /\\ ~ z')\n    assert r == r_, (r, r_)\n    # u not in bdd\n    dvars = {'x': 'xp'}\n    with pytest.raises(ValueError):\n        g.let(dvars, 1000)\n    # y essential for u\n    dvars = {'x': 'y'}\n    v = g.let(dvars, u)\n    v_ = g.add_expr(r'y /\\ ~ z')\n    assert v == v_, (v, v_)\n    # old and new vars intersect\n    dvars = {'x': 'x'}\n    v = g.let(dvars, u)\n    assert v == u, (v, u)\n\n\ndef test_rename_syntax():\n    b = BDD()\n    [b.add_var(var) for var in ['x', 'y', 'z', 'w']]\n    # single substitution\n    u = b.add_expr(r'\\S y / x:  TRUE')\n    assert u == b.true, u\n    u = b.add_expr(r'\\S y / x:  FALSE')\n    assert u == b.false, u\n    u = b.add_expr(r'\\S y / x:  x')\n    u_ = b.add_expr('y')\n    assert u == u_, (u, u_)\n    u = b.add_expr(r'\\S y / x:  z')\n    u_ = b.add_expr('z')\n    assert u == u_, (u, u_)\n    u = b.add_expr(r'\\S y / x:  x /\\ z')\n    u_ = b.add_expr(r'y /\\ z')\n    assert u == u_, (u, u_)\n    # multiple substitution\n    u = b.add_expr(r'\\S y / x,  w / z:  x /\\ z')\n    u_ = b.add_expr(r'y /\\ w')\n    assert u == u_, (u, u_)\n    u = b.add_expr(r'\\S y / x,  w / z:  z \\/ ~ x')\n    u_ = b.add_expr(r'w \\/ ~ y')\n    assert u == u_, (u, u_)\n\n\ndef test_image_rename_map_checks():\n    ordering = {'x': 0, 'xp': 1,\n                'y': 2, 'yp': 3,\n                'z': 4, 'zp': 5}\n    bdd = BDD(ordering)\n    # non-adjacent\n    rename = {0: 2, 3: 4}\n    qvars = set()\n    r = _bdd.image(1, 1, rename, qvars, bdd)\n    assert r == 1, r\n    r = _bdd.preimage(1, 1, rename, qvars, bdd)\n    assert r == 1, r\n    # overlapping keys and values\n    rename = {0: 1, 1: 2}\n    with pytest.raises(AssertionError):\n        _bdd.image(1, 1, rename, qvars, bdd)\n    with pytest.raises(AssertionError):\n        _bdd.preimage(1, 1, rename, qvars, bdd)\n    # may be in support after quantification ?\n    trans = bdd.add_expr('x => xp')\n    source = bdd.add_expr(r'x /\\ y')\n    qvars = {0}\n    rename = {1: 0, 3: 2}\n    with pytest.raises(AssertionError):\n        _bdd.image(trans, source, rename, qvars, bdd)\n    # in support of `target` ?\n    qvars = set()\n    trans = bdd.add_expr('y')\n    target = bdd.add_expr(r'x /\\ y')\n    rename = {0: 2}\n    r = _bdd.preimage(trans, target, rename, qvars, bdd)\n    assert r == bdd.var('y'), r\n\n\ndef test_preimage():\n    # exists: x, y\n    # forall: z\n    ordering = {'x': 0, 'xp': 1,\n                'y': 2, 'yp': 3,\n                'z': 4, 'zp': 5}\n    rename = {0: 1, 2: 3, 4: 5}\n    g = BDD(ordering)\n    f = g.add_expr('~ x')\n    t = g.add_expr('x <=> ~ xp')\n    qvars = {1, 3}\n    p = _bdd.preimage(t, f, rename, qvars, g)\n    x = g.add_expr('x')\n    assert x == p, (x, p)\n    # a cycle\n    # (x /\\ y) --> (~ x /\\ y) -->\n    # (~ x /\\ ~ y) --> (x /\\ ~ y) --> wrap around\n    t = g.add_expr(\n        r'((x /\\ y) => (~ xp /\\ yp)) /\\ '\n        r'((~ x /\\ y) => (~ xp /\\ ~ yp)) /\\ '\n        r'((~ x /\\ ~ y) => (xp /\\ ~ yp)) /\\ '\n        r'((x /\\ ~ y) => (xp /\\ yp))')\n    f = g.add_expr(r'x /\\ y')\n    p = _bdd.preimage(t, f, rename, qvars, g)\n    assert p == g.add_expr(r'x /\\ ~ y')\n    f = g.add_expr(r'x /\\ ~ y')\n    p = _bdd.preimage(t, f, rename, qvars, g)\n    assert p == g.add_expr(r'~ x /\\ ~ y')\n    # backward reachable set\n    f = g.add_expr(r'x /\\ y')\n    oldf = None\n    while oldf != f:\n        p = _bdd.preimage(t, f, rename, qvars, g)\n        oldf = f\n        f = g.apply('or', p, oldf)\n    assert f == 1\n    # go around once\n    f = g.add_expr(r'x /\\ y')\n    start = f\n    for i in range(4):\n        f = _bdd.preimage(t, f, rename, qvars, g)\n    end = f\n    assert start == end\n    # forall z exists x, y\n    t = g.add_expr(\n        r'('\n        r'    ((x /\\ y) => (zp /\\ xp /\\ ~ yp)) \\/ '\n        r'    ((x /\\ y) => (~ zp /\\ ~ xp /\\ yp))'\n        r') /\\ '\n        r'(~ (x /\\ y) => False)')\n    f = g.add_expr(r'x /\\ ~ y')\n    ep = _bdd.preimage(t, f, rename, qvars, g)\n    p = g.quantify(ep, {'zp'}, forall=True)\n    assert p == -1\n    f = g.add_expr(r'(x /\\ ~ y) \\/ (~ x /\\ y)')\n    ep = _bdd.preimage(t, f, rename, qvars, g)\n    p = g.quantify(ep, {'zp'}, forall=True)\n    assert p == g.add_expr(r'x /\\ y')\n\n\ndef test_assert_valid_ordering():\n    ordering = {'x': 0, 'y': 1}\n    _bdd._assert_valid_ordering(ordering)\n    incorrect_ordering = {'x': 0, 'y': 2}\n    with pytest.raises(AssertionError):\n        _bdd._assert_valid_ordering(incorrect_ordering)\n\n\ndef test_assert_refined_ordering():\n    ordering = {'x': 0, 'y': 1}\n    new_ordering = {'z': 0, 'x': 1, 'w': 2, 'y': 3}\n    _bdd._assert_isomorphic_orders(ordering, new_ordering, ordering)\n\n\ndef test_to_graphviz_dot():\n    def fmt(x):\n        return str(abs(x))\n    # with roots\n    g = x_and_y()\n    dot = _bdd._to_dot([4, 2], g)\n    r = _graph_from_dot(dot)\n    for u in g:\n        assert fmt(u) in r, (u, r)\n    for u, (_, v, w) in g._succ.items():\n        su = fmt(u)\n        assert su in r, (su, r)\n        if v is None or w is None:\n            assert v is None, v\n            assert w is None, w\n            continue\n        sv = fmt(v)\n        sw = fmt(w)\n        assert sv in r[su], (su, sv, r)\n        assert sw in r[su], (su, sw, r)\n    # no roots\n    dot = _bdd._to_dot(None, g)\n    r = _graph_from_dot(dot)\n    # `r` has 3 hidden nodes,\n    # used to layout variable levels\n    assert len(r) == 8, r\n\n\ndef _graph_from_dot(dot_graph):\n    \"\"\"Return `dict` of `set` for graph.\"\"\"\n    g = dict()\n    return _graph_from_dot_recurse(dot_graph, g)\n\n\ndef _graph_from_dot_recurse(dot_graph, g):\n    for h in dot_graph.subgraphs:\n        _graph_from_dot_recurse(h, g)\n    for u in dot_graph.nodes:\n        g[u] = set()\n    for u, v in dot_graph.edges:\n        assert u in g, (u, g)\n        g[u].add(v)\n    return g\n\n\ndef test_function_wrapper():\n    levels = dict(x=0, y=1, z=2)\n    bdd = dd.autoref.BDD(levels)\n    u = bdd.add_expr(r'x /\\ y')\n    assert u.bdd is bdd, (repr(u.bdd), repr(bdd))\n    assert abs(u.node) in bdd._bdd, (u.node, bdd._bdd._succ)\n    # operators\n    x = bdd.add_expr('x')\n    z = bdd.add_expr('z')\n    v = x.implies(z)\n    w = u & ~ v\n    w_ = bdd.add_expr(r'(x /\\ y) /\\ ~ ((~ x) \\/ z)')\n    assert w_ == w, (w_, w)\n    r = ~ (u | v).equiv(w)\n    r_ = bdd.add_expr(\n        r'( (x /\\ y) \\/ ((~ x) \\/ z) ) ^'\n        r'( (x /\\ y) /\\ ~ ((~ x) \\/ z) )')\n    assert r_ == r, (r_, r)\n    p = bdd.add_expr('y')\n    q = p.equiv(x)\n    q_ = bdd.add_expr('x <=> y')\n    assert q_ == q, (q_, q)\n    # to_expr\n    s = q.to_expr()\n    assert s == 'ite(x, y, (~ y))', s\n    # equality\n    p_ = bdd.add_expr('y')\n    assert p_ == p, p_\n    # decref and collect garbage\n    bdd.collect_garbage()\n    n = len(bdd)\n    assert n > 1, bdd._bdd._ref\n    del p\n    del q, q_\n    del r, r_\n    bdd.collect_garbage()\n    m = len(bdd)\n    assert m > 1, bdd._bdd._ref\n    assert m < n, (m, n)\n    del u\n    del v\n    del w, w_\n    del x\n    del z\n    bdd.collect_garbage()\n    n = len(bdd)\n    assert n == 2, bdd._bdd._ref\n    del p_\n    bdd.collect_garbage()\n    n = len(bdd)\n    assert n == 1, bdd._bdd._ref\n    # properties\n    bdd = dd.autoref.BDD({'x': 0, 'y': 1, 'z': 2})\n    u = bdd.add_expr(r'x \\/ ~ y')\n    assert u.level == 0, u.level\n    assert u.var == 'x', u.var\n    y = bdd.add_expr('~ y')\n    assert u.low == y, (u.low.node, y.node)\n    assert u.high.node == 1, u.high.node\n    assert u.ref == 1, u.ref\n\n\ndef x_or_y():\n    g = two_vars_xy()\n    u = 4\n    t = (0, 3, 1)\n    assert_valid_succ_pred(u, t, g)\n    g._succ[u] = t\n    g._pred[t] = u\n    g._ref[u] = 1\n    g._min_free = u + 1\n    g.assert_consistent()\n    return g\n\n\ndef x_and_y():\n    g = two_vars_xy()\n    u = 4\n    t = (0, -1, 3)\n    assert_valid_succ_pred(u, t, g)\n    g._succ[u] = t\n    g._pred[t] = u\n    g._ref[u] = 1\n    g._min_free = u + 1\n    return g\n\n\ndef two_vars_xy():\n    ordering = {'x': 0, 'y': 1}\n    g = BDD(ordering)\n    u = 2\n    t = (0, -1, 1)\n    assert_valid_succ_pred(u, t, g)\n    g._succ[u] = t\n    g._pred[t] = u\n    g._ref[u] = 1\n    u = 3\n    t = (1, -1, 1)\n    assert_valid_succ_pred(u, t, g)\n    g._succ[u] = t\n    g._pred[t] = u\n    g._ref[u] = 1\n    g._min_free = u + 1\n    return g\n\n\ndef x_and_not_y():\n    # remember:\n    # 2 = ~ (x /\\ ~ y)\n    # -2 = x /\\ ~ y\n    ordering = {'x': 0, 'y': 1}\n    g = BDD(ordering)\n    u = 3\n    v = -1\n    w = 1\n    t = (1, v, w)\n    assert_valid_succ_pred(u, t, g)\n    g._succ[u] = t\n    g._pred[t] = u\n    g._ref[abs(v)] += 1\n    g._ref[abs(w)] += 1\n    g._ref[abs(u)] = 0\n    u = 2\n    v = 1\n    w = 3\n    t = (0, v, w)\n    assert_valid_succ_pred(u, t, g)\n    g._succ[u] = t\n    g._pred[t] = u\n    g._ref[abs(v)] += 1\n    g._ref[abs(w)] += 1\n    g._ref[abs(u)] = 0\n    g._min_free = 4\n    return g\n\n\ndef assert_valid_succ_pred(u, t, g):\n    assert u > 1, u\n    assert isinstance(t, tuple), t\n    assert len(t) == 3, t\n    assert t[0] >= 0, t\n    assert u not in g._succ, g._succ\n    assert t not in g._pred, g._pred\n\n\ndef ref_var(i):\n    h = nx.MultiDiGraph()\n    h.add_node(1, level=2)\n    h.add_node(2, level=i)\n    h.add_edge(2, 1, value=False, complement=True)\n    h.add_edge(2, 1, value=True, complement=False)\n    return h\n\n\ndef ref_x_and_y():\n    h = nx.MultiDiGraph()\n    h.add_node(1, level=2)\n    h.add_node(2, level=0)\n    h.add_node(3, level=1)\n    h.add_edge(2, 1, value=False, complement=True)\n    h.add_edge(2, 3, value=True, complement=False)\n    h.add_edge(3, 1, value=False, complement=True)\n    h.add_edge(3, 1, value=True, complement=False)\n    return h\n\n\ndef ref_x_or_y():\n    h = nx.MultiDiGraph()\n    h.add_node(1, level=2)\n    h.add_node(2, level=0)\n    h.add_node(3, level=1)\n    h.add_edge(2, 3, value=False, complement=False)\n    h.add_edge(2, 1, value=True, complement=False)\n    h.add_edge(3, 1, value=False, complement=True)\n    h.add_edge(3, 1, value=True, complement=False)\n    return h\n\n\ndef compare(u, bdd, h):\n    g = _bdd.to_nx(bdd, [u])\n    post = nx.descendants(g, u)\n    post.add(u)\n    r = g.subgraph(post)\n    gm = iso.MultiDiGraphMatcher(\n        r, h,\n        node_match=_nm,\n        edge_match=_em)\n    assert gm.is_isomorphic()\n    d = gm.mapping\n    assert d[1] == 1\n\n\ndef _nm(x, y):\n    return x['level'] == y['level']\n\n\ndef _em(x, y):\n    return (\n        bool(x[0]['value']) == bool(y[0]['value']) and\n        bool(x[0]['complement']) == bool(y[0]['complement']))\n\nif __name__ == '__main__':\n    log = logging.getLogger('astutils')\n    log.setLevel(logging.ERROR)\n    log = logging.getLogger('dd.bdd')\n    log.setLevel(logging.INFO)\n    log.addHandler(logging.StreamHandler())\n    test_dynamic_reordering()\n"
  },
  {
    "path": "tests/common.py",
    "content": "\"\"\"Common tests for `autoref`, `cudd`, `cudd_zdd`.\"\"\"\n# This file is released in the public domain.\n#\nimport os\n\nimport pytest\n\n\nclass Tests:\n    def setup_method(self):\n        self.DD = None  # `autoref.BDD` or `cudd.BDD` or\n            # `cudd_zdd.ZDD`\n\n    def test_true_false(self):\n        bdd = self.DD()\n        true = bdd.true\n        false = bdd.false\n        assert false.low is None\n        assert false.high is None\n        assert false != true\n        assert false == ~ true\n        assert false == false & true\n        assert true == true | false\n\n    def test_configure_reordering(self):\n        zdd = self.DD()\n        zdd.declare('x', 'y', 'z')\n        u = zdd.add_expr(r'x \\/ y')\n        cfg = zdd.configure(reordering=False)\n        cfg = zdd.configure()\n        assert cfg['reordering'] == False\n        cfg = zdd.configure(reordering=True)\n        assert cfg['reordering'] == False\n        cfg = zdd.configure()\n        assert cfg['reordering'] == True\n\n    def test_succ(self):\n        bdd = self.DD()\n        bdd.declare('x')\n        u = bdd.var('x')\n        level, low, high = bdd.succ(u)\n        assert level == 0, level\n        assert low == bdd.false, low\n        # The next line applies to only BDDs\n        # assert high == bdd.true, high\n\n    def test_add_var(self):\n        bdd = self.DD()\n        bdd.add_var('x')\n        bdd.add_var('y')\n        assert set(bdd.vars) == {'x', 'y'}, bdd.vars\n        x = bdd.var('x')\n        y = bdd.var('y')\n        assert x != y, (x, y)\n\n    def test_var_cofactor(self):\n        bdd = self.DD()\n        bdd.add_var('x')\n        x = bdd.var('x')\n        values = dict(x=False)\n        u = bdd.let(values, x)\n        assert u == bdd.false, u\n        values = dict(x=True)\n        u = bdd.let(values, x)\n        assert u == bdd.true, u\n\n    def test_richcmp(self):\n        bdd = self.DD()\n        assert bdd == bdd\n        other = self.DD()\n        assert bdd != other\n\n    def test_len(self):\n        bdd = self.DD()\n        u = bdd.true\n        assert len(bdd) == 1, len(bdd)\n\n    def test_contains(self):\n        bdd = self.DD()\n        true = bdd.true\n        assert true in bdd\n        bdd.add_var('x')\n        x = bdd.var('x')\n        assert x in bdd\n        # undefined `__contains__`\n        other_bdd = self.DD()\n        other_true = other_bdd.true\n        with pytest.raises(ValueError):\n            other_true in bdd\n\n    def test_var_levels(self):\n        bdd = self.DD()\n        # single variable\n        bdd.declare('x')\n        level = bdd.level_of_var('x')\n        assert level == 0, level\n        var = bdd.var_at_level(0)\n        assert var == 'x', var\n        # two variables\n        bdd.declare('y')\n        x_level = bdd.level_of_var('x')\n        var = bdd.var_at_level(x_level)\n        assert var == 'x', var\n        y_level = bdd.level_of_var('y')\n        var = bdd.var_at_level(y_level)\n        assert var == 'y', var\n        assert x_level != y_level, (x_level, y_level)\n        assert x_level >= 0, x_level\n        assert y_level >= 0, y_level\n\n    def test_var_levels_attr(self):\n        bdd = self.DD()\n        bdd.declare('x', 'y')\n        var_levels = bdd.var_levels\n        assert len(var_levels) == 2, var_levels\n        assert {'x', 'y'} == set(var_levels), var_levels\n        assert {0, 1} == set(var_levels.values()), var_levels\n\n    def test_levels(self):\n        bdd = self.DD()\n        bdd.add_var('x')\n        bdd.add_var('y')\n        bdd.add_var('z')\n        ix = bdd.level_of_var('x')\n        iy = bdd.level_of_var('y')\n        iz = bdd.level_of_var('z')\n        # before any reordering, levels are unchanged\n        assert ix == 0, ix\n        assert iy == 1, iy\n        assert iz == 2, iz\n        x = bdd.var_at_level(0)\n        y = bdd.var_at_level(1)\n        z = bdd.var_at_level(2)\n        assert x == 'x', x\n        assert y == 'y', y\n        assert z == 'z', z\n\n    def test_copy(self):\n        bdd = self.DD()\n        other = self.DD()\n        bdd.declare('x')\n        other.declare('x')\n        u = bdd.add_expr('~ x')\n        v = bdd.copy(u, other)\n        v_ = other.add_expr('~ x')\n        assert v == v_, (v, v_)\n        # copy to same manager\n        w = bdd.copy(u, bdd)\n        assert u == w, (u, w)\n\n    def test_compose(self):\n        bdd = self.DD()\n        for var in ['x', 'y', 'z']:\n            bdd.add_var(var)\n        u = bdd.add_expr(r'x /\\ ~ y')\n        # x |-> y\n        sub = dict(x=bdd.var('y'))\n        v = bdd.let(sub, u)\n        v_ = bdd.false\n        assert v == v_, len(v)\n        # x |-> y, y |-> x\n        sub = dict(x=bdd.var('y'),\n                   y=bdd.var('x'))\n        v = bdd.let(sub, u)\n        v_ = bdd.add_expr(r'y /\\ ~ x')\n        assert v == v_, v\n        # x |-> z\n        sub = dict(x=bdd.var('z'))\n        v = bdd.let(sub, u)\n        v_ = bdd.add_expr(r'z /\\ ~ y')\n        assert v == v_, v\n        # x |-> z, y |-> x\n        sub = dict(x=bdd.var('z'),\n                   y=bdd.var('x'))\n        v = bdd.let(sub, u)\n        v_ = bdd.add_expr(r'z /\\ ~ x')\n        assert v == v_, v\n        # x |-> (y \\/ z)\n        sub = dict(x=bdd.add_expr(r'y \\/ z'))\n        v = bdd.let(sub, u)\n        v_ = bdd.add_expr(r'(y \\/ z) /\\ ~ y')\n        assert v == v_, v\n        # LET x == ~ y IN ~ x\n        u = bdd.add_expr('~ x')\n        v = bdd.add_expr('~ y')\n        let = dict(x=v)\n        w = bdd.let(let, u)\n        w_ = bdd.var('y')\n        assert w == w_, len(w)\n        # LET x == y IN x /\\ y\n        u = bdd.add_expr(r'x /\\ y')\n        v = bdd.add_expr('y')\n        let = dict(x=v)\n        w = bdd.let(let, u)\n        w_ = bdd.var('y')\n        assert w == w_, len(w)\n        # LET x == ~ y IN x /\\ y\n        v = bdd.add_expr('~ y')\n        let = dict(x=v)\n        w = bdd.let(let, u)\n        w_ = bdd.false\n        assert w == w_, len(w)\n\n    def test_cofactor(self):\n        bdd = self.DD()\n        for var in ['x', 'y']:\n            bdd.add_var(var)\n        x = bdd.var('x')\n        y = bdd.var('y')\n        # x /\\ y\n        u = bdd.apply('and', x, y)\n        r = bdd.let(dict(x=False, y=False), u)\n        assert r == bdd.false, r\n        r = bdd.let(dict(x=True, y=False), u)\n        assert r == bdd.false, r\n        r = bdd.let(dict(x=False, y=True), u)\n        assert r == bdd.false, r\n        r = bdd.let(dict(x=True, y=True), u)\n        assert r == bdd.true, r\n        # x=False\n        let = dict(x=False)\n        r = bdd.let(let, u)\n        r_ = bdd.false\n        assert r == r_, len(r)\n        # x=True\n        let = dict(x=True)\n        r = bdd.let(let, u)\n        r_ = bdd.var('y')\n        assert r == r_, len(r)\n        # x /\\ ~ y\n        not_y = bdd.apply('not', y)\n        u = bdd.apply('and', x, not_y)\n        r = bdd.let(dict(x=False, y=False), u)\n        assert r == bdd.false, r\n        r = bdd.let(dict(x=True, y=False), u)\n        assert r == bdd.true, r\n        r = bdd.let(dict(x=False, y=True), u)\n        assert r == bdd.false, r\n        r = bdd.let(dict(x=True, y=True), u)\n        assert r == bdd.false, r\n        # y=False\n        let = dict(y=False)\n        r = bdd.let(let, u)\n        r_ = bdd.add_expr('x')\n        assert r == r_, len(r)\n        # y=True\n        let = dict(y=True)\n        r = bdd.let(let, u)\n        r_ = bdd.false\n        assert r == r_, len(r)\n        # ~ x \\/ y\n        not_x = bdd.apply('not', x)\n        u = bdd.apply('or', not_x, y)\n        r = bdd.let(dict(x=False, y=False), u)\n        assert r == bdd.true, r\n        r = bdd.let(dict(x=True, y=False), u)\n        assert r == bdd.false, r\n        r = bdd.let(dict(x=False, y=True), u)\n        assert r == bdd.true, r\n        r = bdd.let(dict(x=True, y=True), u)\n        assert r == bdd.true, r\n\n    def test_count(self):\n        b = self.DD()\n        # x\n        b.declare('x')\n        u = b.add_expr('x')\n        with pytest.raises(ValueError):\n            b.count(u, 0)\n        n = b.count(u, 1)\n        assert n == 1, n\n        n = b.count(u, 2)\n        assert n == 2, n\n        n = b.count(u, 3)\n        assert n == 4, n\n        n = b.count(u)\n        assert n == 1, n\n        # x /\\ y\n        b.declare('y')\n        u = b.add_expr(r'x /\\ y')\n        with pytest.raises(ValueError):\n            b.count(u, 0)\n        with pytest.raises(ValueError):\n            b.count(u, 1)\n        n = b.count(u, 2)\n        assert n == 1, n\n        n = b.count(u, 3)\n        assert n == 2, n\n        n = b.count(u, 5)\n        assert n == 8, n\n        n = b.count(u)\n        assert n == 1, n\n        # x \\/ ~ y\n        u = b.add_expr(r'x \\/ ~ y')\n        with pytest.raises(ValueError):\n            b.count(u, 0)\n        with pytest.raises(ValueError):\n            b.count(u, 1)\n        n = b.count(u, 2)\n        assert n == 3, n\n        n = b.count(u, 3)\n        assert n == 6, n\n        n = b.count(u, 4)\n        assert n == 12, n\n        n = b.count(u)\n        assert n == 3, n\n\n    def test_pick_iter(self):\n        b = self.DD()\n        b.add_var('x')\n        b.add_var('y')\n        # FALSE\n        u = b.false\n        m = list(b.pick_iter(u))\n        assert not m, m\n        # TRUE, no care vars\n        u = b.true\n        m = list(b.pick_iter(u))\n        assert m == [{}], m\n        # x\n        u = b.add_expr('x')\n        m = list(b.pick_iter(u))\n        m_ = [dict(x=True)]\n        assert m == m_, (m, m_)\n        # ~ x /\\ y\n        s = r'~ x /\\ y'\n        u = b.add_expr(s)\n        g = b.pick_iter(u, care_vars=set())\n        m = list(g)\n        m_ = [dict(x=False, y=True)]\n        assert m == m_, (m, m_)\n        u = b.add_expr(s)\n        g = b.pick_iter(u)\n        m = list(g)\n        assert m == m_, (m, m_)\n        # x /\\ y\n        u = b.add_expr(r'x /\\ y')\n        m = list(b.pick_iter(u))\n        m_ = [dict(x=True, y=True)]\n        assert m == m_, m\n        # x\n        s = '~ y'\n        u = b.add_expr(s)\n        # partial\n        g = b.pick_iter(u)\n        m = list(g)\n        m_ = [dict(y=False)]\n        self.equal_list_contents(m, m_)\n        # partial\n        g = b.pick_iter(u, care_vars=['x', 'y'])\n        m = list(g)\n        m_ = [\n            dict(x=True, y=False),\n            dict(x=False, y=False)]\n        self.equal_list_contents(m, m_)\n        # care bits x, y\n        b.add_var('z')\n        s = r'x \\/ y'\n        u = b.add_expr(s)\n        g = b.pick_iter(u, care_vars=['x', 'y'])\n        m = list(g)\n        m_ = [\n            dict(x=True, y=False),\n            dict(x=False, y=True),\n            dict(x=True, y=True)]\n        self.equal_list_contents(m, m_)\n\n    def equal_list_contents(self, x, y):\n        for u in x:\n            assert u in y, (u, x, y)\n        for u in y:\n            assert u in x, (u, x, y)\n\n    def test_apply(self):\n        bdd = self.DD()\n        for var in ['x', 'y', 'z']:\n            bdd.add_var(var)\n        x = bdd.var('x')\n        y = bdd.var('y')\n        z = bdd.var('z')\n        # (x \\/ ~ x) \\equiv TRUE\n        not_x = bdd.apply('not', x)\n        true = bdd.apply('or', x, not_x)\n        assert true == bdd.true, true\n        # x /\\ ~ x \\equiv FALSE\n        false = bdd.apply('and', x, not_x)\n        assert false == bdd.false, false\n        # x /\\ y \\equiv ~ (~ x \\/ ~ y)\n        u = bdd.apply('and', x, y)\n        not_y = bdd.apply('not', y)\n        v = bdd.apply('or', not_x, not_y)\n        v = bdd.apply('not', v)\n        assert u == v, (u, v)\n        # xor\n        u = bdd.apply('xor', x, y)\n        r = bdd.let(dict(x=False, y=False), u)\n        assert r == bdd.false, r\n        r = bdd.let(dict(x=True, y=False), u)\n        assert r == bdd.true, r\n        r = bdd.let(dict(x=False, y=True), u)\n        assert r == bdd.true, r\n        r = bdd.let(dict(x=True, y=True), u)\n        assert r == bdd.false, r\n        # (z \\/ ~ y) /\\ x = (z /\\ x) \\/ (~ y /\\ x)\n        u = bdd.apply('or', z, not_y)\n        u = bdd.apply('and', u, x)\n        v = bdd.apply('and', z, x)\n        w = bdd.apply('and', not_y, x)\n        v = bdd.apply('or', v, w)\n        assert u == v, (u, v)\n        # symbols\n        u = bdd.apply('and', x, y)\n        v = bdd.apply('&', x, y)\n        assert u == v, (u, v)\n        u = bdd.apply('or', x, y)\n        v = bdd.apply('|', x, y)\n        assert u == v, (u, v)\n        u = bdd.apply('not', x)\n        v = bdd.apply('!', x)\n        assert u == v, (u, v)\n        u = bdd.apply('xor', x, y)\n        v = bdd.apply('^', x, y)\n        assert u == v, (u, v)\n        # ternary\n        u = bdd.apply('ite', x, y, ~ z)\n        u_ = bdd.add_expr(r'(x /\\ y) \\/ (~ x /\\ ~ z)')\n        assert u == u_, (u, u_)\n\n    def test_quantify(self):\n        bdd = self.DD()\n        for var in ['x', 'y', 'z']:\n            bdd.add_var(var)\n        x = bdd.var('x')\n        # (\\E x:  x) \\equiv TRUE\n        r = bdd.quantify(x, ['x'], forall=False)\n        assert r == bdd.true, r\n        # (\\A x:  x) \\equiv FALSE\n        r = bdd.quantify(x, ['x'], forall=True)\n        assert r == bdd.false, r\n        # (\\E y:  x) \\equiv x\n        r = bdd.quantify(x, ['y'], forall=False)\n        assert r == x, (r, x)\n        # (\\A y:  x) \\equiv x\n        r = bdd.quantify(x, ['y'], forall=True)\n        assert r == x, (r, x)\n        # (\\E x:  x /\\ y) \\equiv y\n        y = bdd.var('y')\n        u = bdd.apply('and', x, y)\n        r = bdd.quantify(u, ['x'], forall=False)\n        assert r == y, (r, y)\n        assert r != x, (r, x)\n        # (\\A x:  x /\\ y) \\equiv FALSE\n        r = bdd.quantify(u, ['x'], forall=True)\n        assert r == bdd.false, r\n        # (\\A x:  ~ x \\/ y) \\equiv y\n        not_x = bdd.apply('not', x)\n        u = bdd.apply('or', not_x, y)\n        r = bdd.quantify(u, ['x'], forall=True)\n        assert r == y, (r, y)\n        # \\E x:  ((x /\\ ~ y) \\/ ~ z)\n        u = bdd.add_expr(r'(x /\\ ~ y) \\/ ~ z')\n        qvars = ['x']\n        r = bdd.exist(qvars, u)\n        r_ = bdd.add_expr(r'\\E x:  ((x /\\ ~ y) \\/ ~ z)')\n        assert r == r_, (r, r_)\n        r_ = bdd.add_expr(r'(~ y) \\/ ~ z')\n        assert r == r_, (r, r_)\n        # \\E y:  x /\\ ~ y /\\ ~ z\n        u = bdd.add_expr(r'x /\\ ~ y /\\ ~ z')\n        qvars = ['y']\n        r = bdd.exist(qvars, u)\n        r_ = bdd.add_expr(r'x /\\ ~ z')\n        assert r == r_, len(r)\n\n    def test_exist_forall(self):\n        bdd = self.DD()\n        for var in ['x', 'y']:\n            bdd.add_var(var)\n        x = bdd.var('x')\n        # \\E x: x = 1\n        r = bdd.exist(['x'], x)\n        assert r == bdd.true, r\n        # \\A x: x = 0\n        r = bdd.forall(['x'], x)\n        assert r == bdd.false, r\n        # \\E y: x = x\n        r = bdd.exist(['y'], x)\n        assert r == x, (r, x)\n        # \\A y: x = x\n        r = bdd.forall(['y'], x)\n        assert r == x, (r, x)\n        # (\\E x:  x /\\ y) \\equiv y\n        y = bdd.var('y')\n        u = bdd.apply('and', x, y)\n        r = bdd.exist(['x'], u)\n        assert r == y, (r, y)\n        assert r != x, (r, x)\n        # (\\A x:  x /\\ y) \\equiv FALSE\n        r = bdd.forall(['x'], u)\n        assert r == bdd.false, r\n        # (\\A x:  ~ x \\/ y) \\equiv y\n        not_x = bdd.apply('not', x)\n        u = bdd.apply('or', not_x, y)\n        r = bdd.forall(['x'], u)\n        assert r == y, (r, y)\n\n    def test_cube(self):\n        bdd = self.DD()\n        for var in ['x', 'y', 'z']:\n            bdd.add_var(var)\n        # x\n        x = bdd.var('x')\n        c = bdd.cube(['x'])\n        assert x == c, (x, c)\n        # x /\\ y\n        y = bdd.var('y')\n        u = bdd.apply('and', x, y)\n        c = bdd.cube(['x', 'y'])\n        assert u == c, (u, c)\n        # x /\\ ~ y\n        not_y = bdd.apply('not', y)\n        u = bdd.apply('and', x, not_y)\n        d = dict(x=True, y=False)\n        c = bdd.cube(d)\n        assert u == c, (u, c)\n\n    def test_add_expr(self):\n        bdd = self.DD()\n        for var in ['x', 'y']:\n            bdd.add_var(var)\n        # ((FALSE \\/ TRUE) /\\ x) \\equiv x\n        s = r'(True \\/ FALSE) /\\ x'\n        u = bdd.add_expr(s)\n        x = bdd.var('x')\n        assert u == x, (u, x)\n        # ((x \\/ ~ y) /\\ x) \\equiv x\n        s = r'(x \\/ ~ y) /\\ x'\n        u = bdd.add_expr(s)\n        assert u == x, (u, x)\n        # x /\\ y /\\ z\n        bdd.add_var('z')\n        z = bdd.var('z')\n        u = bdd.add_expr(r'x /\\ y /\\ z')\n        u_ = bdd.cube(dict(x=True, y=True, z=True))\n        assert u == u_, (u, u_)\n        # x /\\ ~ y /\\ z\n        u = bdd.add_expr(r'x /\\ ~ y /\\ z')\n        u_ = bdd.cube(dict(x=True, y=False, z=True))\n        assert u == u_, (u, u_)\n        # (\\E x:  x /\\ y) \\equiv y\n        y = bdd.var('y')\n        u = bdd.add_expr(r'\\E x:  x /\\ y')\n        assert u == y, (str(u), str(y))\n        # (\\A x:  x \\/ ~ x) \\equiv TRUE\n        u = bdd.add_expr(r'\\A x:  ~ x \\/ x')\n        assert u == bdd.true, u\n\n    def test_to_expr(self):\n        bdd = self.DD()\n        bdd.declare('x', 'y')\n        u = bdd.var('x')\n        r = bdd.to_expr(u)\n        r_ = 'x'\n        assert r == r_, (r, r_)\n        u = bdd.add_expr(r'x /\\ y')\n        r = bdd.to_expr(u)\n        r_ = 'ite(x, y, FALSE)'\n        assert r == r_, (r, r_)\n        u = bdd.add_expr(r'x \\/ y')\n        r = bdd.to_expr(u)\n        r_ = 'ite(x, TRUE, y)'\n        assert r == r_, (r, r_)\n\n    def test_support(self):\n        zdd = self.DD()\n        # declared at the start, for ZDDs to work\n        zdd.declare('x', 'y', 'z')\n        # FALSE\n        u = zdd.false\n        s = zdd.support(u)\n        assert s == set(), s\n        # TRUE\n        u = zdd.true\n        s = zdd.support(u)\n        assert s == set(), s\n        # x\n        u = zdd.add_expr('x')\n        s = zdd.support(u)\n        assert s == {'x'}, s\n        # ~ x\n        u = zdd.add_expr('x')\n        s = zdd.support(u)\n        assert s == {'x'}, s\n        # ~ y\n        u = zdd.add_expr('~ y')\n        s = zdd.support(u)\n        assert s == {'y'}, s\n        # x /\\ y\n        u = zdd.add_expr(r'x /\\ y')\n        s = zdd.support(u)\n        assert s == {'x', 'y'}, s\n        # x \\/ y\n        u = zdd.add_expr(r'x \\/ y')\n        s = zdd.support(u)\n        assert s == {'x', 'y'}, s\n        # x /\\ ~ y\n        u = zdd.add_expr(r'x /\\ ~ y')\n        s = zdd.support(u)\n        assert s == {'x', 'y'}, s\n\n    def test_rename(self):\n        bdd = self.DD()\n        bdd.declare('x', 'y', 'z', 'w')\n        # LET x == y IN x\n        x = bdd.var('x')\n        supp = bdd.support(x)\n        assert supp == set(['x']), supp\n        d = dict(x='y')\n        f = bdd.let(d, x)\n        supp = bdd.support(f)\n        assert supp == set(['y']), supp\n        y = bdd.var('y')\n        assert f == y, (f, y)\n        # x, y -> z, w\n        not_y = bdd.apply('not', y)\n        u = bdd.apply('or', x, not_y)\n        supp = bdd.support(u)\n        assert supp == set(['x', 'y']), supp\n        d = dict(x='z', y='w')\n        f = bdd.let(d, u)\n        supp = bdd.support(f)\n        assert supp == set(['z', 'w']), supp\n        z = bdd.var('z')\n        w = bdd.var('w')\n        not_w = bdd.apply('not', w)\n        f_ = bdd.apply('or', z, not_w)\n        assert f == f_, (f, f_)\n        # ensure substitution, not swapping\n        #\n        # f == LET x == y\n        #      IN x /\\ y\n        u = bdd.apply('and', x, y)\n        # replace x with y, but leave y as is\n        d = dict(x='y')\n        f = bdd.let(d, u)\n        # THEOREM f <=> y\n        assert f == y, (f, y)\n        # f == LET x == y\n        #      IN x /\\ ~ y\n        u = bdd.apply('and', x, ~ y)\n        d = dict(x='y')\n        f = bdd.let(d, u)\n        # THEOREM f <=> FALSE\n        assert f == bdd.false, f\n        # simultaneous substitution\n        #\n        # f == LET x == y1  (* x1, y1 correspond to *)\n        #          y == x1  (* x, y after substitution *)\n        #      IN x /\\ ~ y\n        u = bdd.apply('and', x, ~ y)\n        # replace x with y, and simultaneously, y with x\n        d = dict(x='y', y='x')\n        f = bdd.let(d, u)\n        f_ = bdd.apply('and', y, ~ x)\n        # THEOREM f <=> (~ x /\\ y)\n        assert f == f_, (f, f_)\n        del x, y, not_y, z, w, not_w, u, f, f_\n        # as method\n        x = bdd.var('x')\n        y_ = bdd.var('y')\n        d = dict(x='y')\n        y = bdd.let(d, x)\n        assert y == y_, (y, y_)\n        del x, y, y_\n\n    def test_ite(self):\n        b = self.DD()\n        for var in ['x', 'y', 'z']:\n            b.add_var(var)\n        x = b.var('x')\n        u = b.ite(x, b.true, b.false)\n        assert u == x, (u, x)\n        u = b.ite(x, b.false, b.true)\n        assert u == ~ x, (u, x)\n        y = b.var('y')\n        u = b.ite(x, y, b.false)\n        u_ = b.add_expr(r'x /\\ y')\n        assert u == u_, (u, u_)\n\n    def test_reorder_with_args(self):\n        bdd = self.DD()\n        dvars = ['x', 'y', 'z']\n        for var in dvars:\n            bdd.add_var(var)\n        self._confirm_var_order(dvars, bdd)\n        order = dict(y=0, z=1, x=2)\n        bdd.reorder(order)\n        for var in order:\n            level_ = order[var]\n            level = bdd.level_of_var(var)\n            assert level == level_, (var, level, level_)\n\n    def test_reorder_without_args(self):\n        bdd = self.DD()\n        # Figs. 6.24, 6.25 Baier 2008\n        vrs = ['z1', 'z2', 'z3', 'y1', 'y2', 'y3']\n        bdd.declare(*vrs)\n        self._confirm_var_order(vrs, bdd)\n        expr = r'(z1 /\\ y1) \\/ (z2 /\\ y2) \\/ (z3 /\\ y3)'\n        u = bdd.add_expr(expr)\n        n_before = u.dag_size\n        bdd.reorder()\n        n_after = u.dag_size\n        assert n_after < n_before, (n_after, n_before)\n        # optimal:  n_after == 6\n        #\n        # assert that each pair zi, yi is of\n        # variables at adjacent levels\n        # levels = {var: bdd.level_of_var(var) for var in vrs}\n        # for i in range(1, 4):\n        #     a = levels[f'z{i}']\n        #     b = levels[f'y{i}']\n        #     assert abs(a - b) == 1, levels\n\n    def _confirm_var_order(self, vrs, bdd):\n        for i, var in enumerate(vrs):\n            level = bdd.level_of_var(var)\n            assert level == i, (var, level, i)\n\n    def test_reorder_contains(self):\n        bdd = self.DD()\n        bdd.declare('x', 'y', 'z')\n        u = bdd.add_expr(r'(x /\\ y) \\/ z')\n        bdd.reorder()\n        assert u in bdd\n\n    def test_comparators(self):\n        bdd = self.DD()\n        # `None`\n        assert not (bdd.false == None)\n        assert not (bdd.true == None)\n        assert bdd.false != None\n        assert bdd.true != None\n        # constant\n        assert bdd.false < bdd.true\n        assert bdd.false <= bdd.true\n        assert bdd.false != bdd.true\n        assert bdd.true >= bdd.false\n        assert bdd.true > bdd.false\n        assert bdd.true == bdd.true\n        assert bdd.false == bdd.false\n        # non-constant\n        bdd.declare('x', 'y')\n        u = bdd.add_expr('x')\n        # compared to false\n        assert u > bdd.false\n        assert u >= bdd.false\n        assert u != bdd.false\n        assert bdd.false <= u\n        assert bdd.false < u\n        assert u == u\n        # compared to true\n        assert u < bdd.true\n        assert u <= bdd.true\n        assert u != bdd.true\n        assert bdd.true >= u\n        assert bdd.true > u\n        # x /\\ y\n        x = bdd.var('x')\n        y = bdd.var('y')\n        assert (x & y) == ~ (~ x | ~ y)\n        assert (x & y) != ~ (~ x | y)\n\n    def test_function_support(self):\n        bdd = self.DD()\n        bdd.add_var('x')\n        u = bdd.var('x')\n        r = u.support\n        assert r == {'x'}, r\n        bdd.add_var('y')\n        u = bdd.add_expr(r'y /\\ x')\n        r = u.support\n        assert r == {'x', 'y'}, r\n\n    def test_node_hash(self):\n        bdd = self.DD()\n        bdd.declare('z')\n        u = bdd.add_expr('z')\n        n = hash(u)\n        m = hash(bdd.true)\n        assert n != m, (n, m)\n\n    def test_add_int(self):\n        bdd = self.DD()\n        bdd.declare('x', 'y')\n        u = bdd.add_expr(r'x \\/ ~ y')\n        node_id = int(u)\n        u_ = bdd._add_int(node_id)\n        assert u == u_, (u, u_)\n        id2 = int(u_)\n        assert node_id == id2, (node_id, id2)\n        # test string form\n        node_str = str(u)\n        s = f'@{node_id}'\n        assert node_str == s, (node_str, s)\n\n    def test_dump_using_graphviz(\n            self):\n        bdd = self.DD()\n        bdd.declare('x', 'y')\n        u = bdd.add_expr(r'x /\\ y')\n        for ext in ('dot', 'pdf', 'png', 'svg', 'ext'):\n            filename = f'bdd.{ext}'\n            if os.path.isfile(filename):\n                os.remove(filename)\n        # dot\n        bdd.dump('bdd.dot', [u])\n        assert os.path.isfile('bdd.dot')\n        os.remove('bdd.dot')\n        bdd.dump(\n            'bdd.dot', [u],\n            filetype='dot')\n        assert os.path.isfile('bdd.dot')\n        # pdf\n        bdd.dump('bdd.pdf', [u])\n        assert os.path.isfile('bdd.pdf')\n        os.remove('bdd.pdf')\n        bdd.dump(\n            'bdd.pdf', [u],\n            filetype='pdf')\n        assert os.path.isfile('bdd.pdf')\n        # no ext\n        if os.path.isfile('bdd'):\n            os.remove('bdd')\n        bdd.dump(\n            'bdd', [u],\n            filetype='pdf')\n        assert os.path.isfile('bdd')\n        # png\n        bdd.dump(\n            'bdd.png', [u])\n        assert os.path.isfile('bdd.png')\n        os.remove('bdd.png')\n        bdd.dump(\n            'bdd.png', [u],\n            filetype='png')\n        assert os.path.isfile('bdd.png')\n        # svg\n        bdd.dump(\n            'bdd.svg', [u])\n        assert os.path.isfile('bdd.svg')\n        os.remove('bdd.svg')\n        bdd.dump(\n            'bdd.svg', [u],\n            filetype='svg')\n        assert os.path.isfile('bdd.svg')\n        # ext\n        bdd.dump(\n            'bdd.ext', [u],\n            filetype='pdf')\n        assert os.path.isfile('bdd.ext')\n        with pytest.raises(ValueError):\n            bdd.dump(\n                'bdd.ext', [u])\n"
  },
  {
    "path": "tests/common_bdd.py",
    "content": "\"\"\"Common tests for `autoref`, `cudd`.\"\"\"\n# This file is released in the public domain.\n#\nimport os\n\nimport pytest\n\n\nclass Tests:\n    def setup_method(self):\n        self.DD = None  # `autoref.BDD` or `cudd.BDD`\n\n    def test_succ(self):\n        bdd = self.DD()\n        bdd.declare('x')\n        u = bdd.var('x')\n        level, low, high = bdd.succ(u)\n        assert level == 0, level\n        assert low == bdd.false, low\n        assert high == bdd.true, high\n\n    def test_find_or_add(self):\n        b = self.DD()\n        for var in ['x', 'y', 'z']:\n            b.add_var(var)\n        u = b.find_or_add('x', b.false, b.true)\n        u_ = b.var('x')\n        assert u == u_, b.to_expr(u)\n        u = b.find_or_add('y', b.false, b.true)\n        u_ = b.var('y')\n        assert u == u_, b.to_expr(u)\n        v = b.var('y')\n        w = b.var('z')\n        u = b.find_or_add('x', v, w)\n        u_ = b.add_expr(r'(~ x /\\ y)  \\/  (x /\\ z)')\n        assert b.apply('<=>', u, u_) == b.true\n        assert u == u_, (b.to_expr(u), b.to_expr(u_))\n\n    def test_function(self):\n        bdd = self.DD()\n        bdd.add_var('x')\n        # x\n        x = bdd.var('x')\n        # assert not x.negated\n        low = x.low\n        assert low == bdd.false, low\n        high = x.high\n        assert high == bdd.true, high\n        assert x.var == 'x', x.var\n        # ~ x\n        not_x = ~x\n        # assert not_x.negated\n        low = not_x.low\n        assert low == bdd.false, low\n        high = not_x.high\n        assert high == bdd.true, high\n        assert not_x.var == 'x', not_x.var\n        # constant nodes\n        false = bdd.false\n        assert false.var is None, false.var\n        true = bdd.true\n        assert true.var is None, true.var\n        not_x_ = bdd.add_expr('~ x')\n        assert not_x == not_x_, (\n            not_x, not_x_)\n        # y\n        bdd.add_var('y')\n        y = bdd.var('y')\n        # x & y\n        x_and_y = x & y\n        negated = x_and_y.negated\n        assert not negated, negated\n        var = x_and_y.var\n        assert var == 'x', var\n        low = x_and_y.low\n        assert low == bdd.false, low.var\n        y_ = x_and_y.high\n        assert y == y_, y_.var\n        x_and_y_ = bdd.add_expr(r'x /\\ y')\n        assert x_and_y == x_and_y_, (\n            x_and_y, x_and_y_)\n        # x | y\n        x_or_y = x | y\n        negated = x_or_y.negated\n        assert not negated, negated\n        var = x_or_y.var\n        assert var == 'x', var\n        low = x_or_y.low\n        assert low == y, low.var\n        high = x_or_y.high\n        assert high == bdd.true, high.var\n        x_or_y_ = bdd.add_expr(r'x \\/ y')\n        assert x_or_y == x_or_y_, (\n            x_or_y, x_or_y_)\n        # x ^ y\n        x_xor_y = x ^ y\n        negated = x_xor_y.negated\n        assert negated, negated\n        var = x_xor_y.var\n        assert var == 'x', var\n        high = x_xor_y.high\n        assert high == y, high.var\n        neg_y = x_xor_y.low\n        negated = neg_y.negated\n        assert negated, negated\n        var = neg_y.var\n        assert var == 'y', var\n        low = neg_y.low\n        assert low == bdd.false, low.var\n        high = neg_y.high\n        assert high == bdd.true, high.var\n        x_xor_y_ = bdd.add_expr('x ^ y')\n        assert x_xor_y == x_xor_y_, (\n            x_xor_y, x_xor_y_)\n\n    def test_function_properties(self):\n        bdd = self.DD()\n        bdd.declare('x', 'y')\n        order = dict(x=0, y=1)\n        bdd.reorder(order)\n        u = bdd.add_expr(r'x \\/ y')\n        y = bdd.add_expr('y')\n        # Assigned first because in presence of a bug\n        # different property calls could yield\n        # different values.\n        level = u.level\n        assert level == 0, level\n        var = u.var\n        assert var == 'x', var\n        low = u.low\n        assert low == y, low\n        high = u.high\n        assert high == bdd.true, high\n        ref = u.ref\n        assert ref == 1, ref\n        assert not u.negated\n        support = u.support\n        assert support == {'x', 'y'}, support\n        # terminal\n        u = bdd.false\n        assert u.var is None, u.var\n        assert u.low is None, u.low\n        assert u.high is None, u.high\n\n    def test_negated(self):\n        bdd = self.DD()\n        bdd.declare('x')\n        u = bdd.add_expr('x')\n        neg_u = bdd.add_expr('~ x')\n        a = u.negated\n        b = neg_u.negated\n        assert a or b, (a, b)\n        assert not (a and b), (a, b)\n\n    def test_dump_pdf(self):\n        bdd = self.DD()\n        bdd.declare('x', 'y', 'z')\n        u = bdd.add_expr(r'x /\\ y')\n        v = bdd.add_expr(r'y /\\ ~ z')\n        fname = 'bdd.pdf'\n        roots = [u, v]\n        self.rm_file(fname)\n        bdd.dump(fname, roots)\n        assert os.path.isfile(fname)\n\n    def test_dump_load_json(self):\n        bdd = self.DD()\n        bdd.declare('x', 'y', 'z')\n        u = bdd.add_expr(r'(z /\\ x /\\ y) \\/ x \\/ ~ y')\n        fname = 'foo.json'\n        bdd.dump(fname, [u])\n        u_, = bdd.load(fname)\n        assert u == u_, len(u_)\n        # test `ValueError`\n        with pytest.raises(ValueError):\n            bdd.dump(fname, None)\n        with pytest.raises(ValueError):\n            bdd.dump(fname, list())\n        with pytest.raises(ValueError):\n            bdd.dump(fname, dict())\n\n    def rm_file(self, fname):\n        if os.path.isfile(fname):\n            os.remove(fname)\n"
  },
  {
    "path": "tests/common_cudd.py",
    "content": "\"\"\"Common tests for `cudd`, `cudd_zdd`.\"\"\"\n# This file is released in the public domain.\n#\nimport pytest\n\n\nclass Tests:\n    def setup_method(self):\n        self.DD = None\n            # `cudd.BDD` or `cudd_zdd.ZDD`\n        self.MODULE = None\n            # `cudd` or `cudd_zdd`\n\n    def test_add_var(self):\n        bdd = self.DD()\n        bdd.add_var('x')\n        bdd.add_var('y')\n        jx = bdd._index_of_var['x']\n        jy = bdd._index_of_var['y']\n        assert jx == 0, jx\n        assert jy == 1, jy\n        x = bdd._var_with_index[0]\n        y = bdd._var_with_index[1]\n        assert x == 'x', x\n        assert y == 'y', y\n        assert bdd.vars == {'x', 'y'}, bdd.vars\n        x = bdd.var('x')\n        y = bdd.var('y')\n        assert x != y, (x, y)\n\n    def test_len(self):\n        bdd = self.DD()\n        assert len(bdd) == 0, len(bdd)\n        u = bdd.true\n        assert len(bdd) == 1, len(bdd)\n        del u\n        assert len(bdd) == 0, len(bdd)\n\n    def test_levels(self):\n        bdd = self.DD()\n        bdd.add_var('x', index=0)\n        bdd.add_var('y', index=2)\n        bdd.add_var('z', index=10)\n        ix = bdd.level_of_var('x')\n        iy = bdd.level_of_var('y')\n        iz = bdd.level_of_var('z')\n        # before any reordering, levels match var indices\n        assert ix == 0, ix\n        assert iy == 2, iy\n        assert iz == 10, iz\n        x = bdd.var_at_level(0)\n        y = bdd.var_at_level(2)\n        z = bdd.var_at_level(10)\n        assert x == 'x', x\n        assert y == 'y', y\n        assert z == 'z', z\n\n    def test_var_at_level_exceptions(self):\n        bdd = self.DD()\n        # no variables\n        with pytest.raises(ValueError):\n            bdd.var_at_level(-1)\n        with pytest.raises(ValueError):\n            bdd.var_at_level(0)\n        with pytest.raises(ValueError):\n            bdd.var_at_level(1)\n        with pytest.raises(ValueError):\n            # no var at level CUDD_CONST_INDEX\n            bdd.var_at_level(bdd.false.level)\n        with pytest.raises(OverflowError):\n            bdd.var_at_level(bdd.false.level + 1)\n        # 1 declared variable\n        bdd.declare('x')\n        level = bdd.level_of_var('x')\n        assert level == 0, level\n        var = bdd.var_at_level(0)\n        assert var == 'x', var\n        with pytest.raises(ValueError):\n            bdd.var_at_level(-1)\n        with pytest.raises(ValueError):\n            bdd.var_at_level(1)\n        with pytest.raises(ValueError):\n            # no var at level CUDD_CONST_INDEX\n            bdd.var_at_level(bdd.false.level)\n        with pytest.raises(OverflowError):\n            bdd.var_at_level(bdd.false.level + 1)\n        bdd._var_with_index = dict()\n        with pytest.raises(ValueError):\n            bdd.var_at_level(0)\n\n    def test_incref_decref_locally_inconsistent(self):\n        # \"Locally inconsistent\" here means that\n        # from the viewpoint of some `Function` instance,\n        # the calls to `incref` and `decref` would result\n        # in an incorrect reference count.\n        #\n        # In this example overall the calls to `incref`\n        # and `decref` result in an incorrect reference count.\n        bdd = self.DD()\n        bdd.declare('x', 'y')\n        u = bdd.add_expr(r'x /\\ y')  # ref cnt = 1\n        v = bdd.add_expr(r'x /\\ y')  # ref cnt = 2\n        bdd.incref(u)  # ref cnt = 3\n        bdd.decref(v)  # ref cnt = 2\n        del u, v  # ref cnt = 1\n        # this assertion implies that `DD.__dealloc__`\n        # would raise an exception (that would be\n        # ignored)\n        assert len(bdd) > 0, len(bdd)\n        u = bdd.add_expr(r'x /\\ y')  # ref cnt = 2\n        u._ref = 2\n        bdd.decref(u)  # ref cnt = 1\n\n    def test_decref_incref_locally_inconsistent(self):\n        # \"Locally inconsistent\" here means the\n        # same as described in the previous method.\n        #\n        # The difference with the previous method\n        # is that here `decref` is called before\n        # `incref`, not after.\n        #\n        # In this example overall the calls to `incref`\n        # and `decref` result in an incorrect reference count.\n        bdd = self.DD()\n        bdd.declare('x', 'y')\n        u = bdd.add_expr(r'x /\\ y')  # ref cnt = 1\n        v = bdd.add_expr(r'x /\\ y')  # ref cnt = 2\n        bdd.decref(v)  # ref cnt = 1\n        bdd.incref(u)  # ref cnt = 2\n        del u, v  # ref cnt = 1\n        # this assertion implies that `DD.__dealloc__`\n        # would raise an exception (that would be\n        # ignored)\n        assert len(bdd) > 0, len(bdd)\n        u = bdd.add_expr(r'x /\\ y')  # ref cnt = 2\n        u._ref = 2\n        bdd.decref(u)  # ref cnt = 1\n\n    def test_double_incref_decref_locally_inconsistent(self):\n        # \"Locally inconsistent\" here means the\n        # same as described in a method above.\n        #\n        # The main difference with the previous method\n        # is that here `decref` is called twice.\n        #\n        # Overall, the calls to `incref` and `decref`\n        # would have resulted in a correct reference count\n        # with an earlier implementation.\n        #\n        # In any case, this pattern of calls to\n        # `incref` and `decref` now raises\n        # a `RuntimeError`.\n        bdd = self.DD()\n        bdd.declare('x', 'y')\n        u = bdd.add_expr(r'x /\\ y')  # ref cnt = 1\n        v = bdd.add_expr(r'x /\\ y')  # ref cnt = 2\n        bdd.incref(u)  # ref cnt = 3\n        bdd.incref(u)  # ref cnt = 4\n        bdd.decref(v)  # ref cnt = 3\n        with pytest.raises(RuntimeError):\n            bdd.decref(v)\n        del u, v  # ref cnt = 2\n        assert len(bdd) > 0, len(bdd)\n        u = bdd.add_expr(r'x /\\ y')  # ref cnt = 3\n        u._ref = 3\n        bdd.decref(u)  # ref cnt = 2\n        bdd.decref(u)  # ref cnt = 1\n\n    def test_decref_and_dealloc(self):\n        bdd = self.DD()\n        bdd.declare('x', 'y')\n        u = bdd.add_expr(r'x /\\ ~ y')\n        assert u.ref == 1, u.ref\n        s = int(u)\n        bdd.decref(u, recursive=True)\n        del u  # calls method `Function.__dealloc__`\n        # the execution of `decref` and then\n        # `__dealloc__` should result in\n        # reference count 0,\n        # not a negative value\n        v = bdd._add_int(s)\n        assert v.ref == 1, v.ref\n        bdd.decref(v)\n        del v\n        # the following check passes when\n        # `u.ref = 0 - 1`,\n        # because the reference count is an\n        # unsigned integer, so subtracting 1\n        # results in a saturated positive value,\n        # which is ignored by the function\n        # `Cudd_CheckZeroRef` (which checks\n        # `node->ref != 0 && node->ref != DD_MAXREF`)\n        assert len(bdd) == 0, len(bdd)\n\n    def test_decref(self):\n        bdd = self.DD()\n        # Turn off garbage collection to prevent the\n        # memory for the CUDD BDD/ZDD node below from\n        # being deallocated when the reference count\n        # of the node reaches 0.\n        # If that happened, then further access to\n        # the attribute `u.ref` would have been unsafe.\n        bdd.configure(\n            reordering=False,\n            garbage_collection=False)\n        bdd.declare('x', 'y')\n        u = bdd.add_expr(r'x /\\ ~ y')\n        assert u.ref == 1, u.ref\n        assert u._ref == 1, u._ref\n        bdd.decref(u, recursive=True)\n        # CAUTION: `u.node is NULL` hereafter\n        assert u._ref == 0, u._ref\n        # Ensure that `decref` decrements\n        # only positive reference counts.\n        # No need for `recursive=True`,\n        # because this call should have\n        # no effect at all.\n        with pytest.raises(RuntimeError):\n            bdd.decref(u)\n        assert u._ref == 0, u._ref\n        assert len(bdd) == 0, len(bdd)\n\n    def test_decref_ref_lower_bound(self):\n        bdd = self.DD()\n        bdd.declare('x', 'y')\n        u = bdd.add_expr(r'x /\\ ~ y')\n        assert u._ref == 1, u._ref\n        assert u.ref == 1, u.ref\n        # `recursive=True` is necessary here\n        # because after `u._ref` becomes `0`,\n        # we cannot any more dereference\n        # the successor nodes of `u.node`.\n        #\n        # The reason is that memory for the\n        # BDD/ZDD node pointed to by `u.node`\n        # may be deallocated after its\n        # reference count becomes 0.\n        bdd.decref(u, recursive=True)\n        # `u` should not be used after\n        # the reference count of the BDD/ZDD node\n        # pointed to by `u` becomes 0.\n        # This avoids accessing deallocated memory.\n        assert u._ref == 0, u._ref\n        # Ensure that the method `decref` decrements\n        # only positive reference counts.\n        # `recursive=True` is irrelevant here,\n        # because this call should have\n        # no effect at all.\n        #\n        # Again, `u` is not used in any way\n        # that could access deallocated memory.\n        with pytest.raises(RuntimeError):\n            bdd.decref(u)\n        assert u._ref == 0, u._ref\n        # check also with `recursive=True`\n        with pytest.raises(RuntimeError):\n            bdd.decref(u, recursive=True)\n        # check also `incref`\n        with pytest.raises(RuntimeError):\n            bdd.incref(u)\n        assert len(bdd) == 0, len(bdd)\n\n    def test_dealloc_wrong_ref_lower_bound(self):\n        bdd = self.DD()\n        bdd.declare('x', 'y')\n        u = bdd.add_expr(r'x /\\ ~ y')\n        # make an erroneous external modification\n        assert u.ref == 1, u.ref\n        u._ref = -1\n            # erroneous value\n        with pytest.raises(AssertionError):\n            self.MODULE._test_call_dealloc(u)\n        assert u.ref == 1, u.ref\n        assert u._ref == -1, u._ref\n        u._ref = 1\n            # restore\n        del u\n        assert len(bdd) == 0, len(bdd)\n\n    def test_dealloc_multiple_calls(self):\n        bdd = self.DD()\n        bdd.declare('x', 'y')\n        u = bdd.add_expr(r'x /\\ ~ y')\n        assert u.ref == 1, u.ref\n        assert u._ref == 1, u._ref\n        self.MODULE._test_call_dealloc(u)\n        self.MODULE._test_call_dealloc(u)\n        assert u._ref == 0, u._ref\n        assert len(bdd) == 0, len(bdd)\n"
  },
  {
    "path": "tests/copy_test.py",
    "content": "\"\"\"Tests of the module `dd._copy`.\"\"\"\n# This file is released in the public domain.\n#\nimport dd.autoref as _autoref\nimport dd.cudd as _cudd\nimport dd._copy as _copy\n\n\ndef test_involution():\n    _test_involution(_autoref)\n    _test_involution(_cudd)\n\n\ndef _test_involution(mod):\n    bdd_1, bdd_2 = _setup(mod)\n    u = bdd_1.add_expr(r'x /\\ ~ y')\n    v = _copy.copy_bdd(u, bdd_2)\n    u_ = _copy.copy_bdd(v, bdd_1)\n    assert u == u_, (u, u_)\n\n\ndef test_bdd_mapping():\n    _test_bdd_mapping(_autoref)\n    _test_bdd_mapping(_cudd)\n\n\ndef _test_bdd_mapping(mod):\n    bdd_1, bdd_2 = _setup(mod)\n    u = bdd_1.add_expr(r'x /\\ ~ y')\n    cache = dict()\n    u_ = _copy.copy_bdd(u, bdd_2, cache)\n    d = {bdd_1._add_int(k): v for k, v in cache.items()}\n    _check_bdd_mapping(d, bdd_1, bdd_2)\n\n\ndef _check_bdd_mapping(umap, old_bdd, new_bdd):\n    \"\"\"Raise `AssertionError` if `umap` is inconsistent.\n\n    Asserts that each variable is declared in both managers,\n    and at the same level.\n    \"\"\"\n    # add terminal to map\n    umap[old_bdd.true] = new_bdd.true\n    for u, v in umap.items():\n        assert u in old_bdd, u\n        assert v in new_bdd, v\n        assert u.var == v.var\n        assert u.level == v.level\n        assert u.negated == v.negated\n        # terminal ?\n        if u.var is None:\n            continue\n        # non-terminal\n        low = _map_node(u.low, umap)\n        high = _map_node(u.high, umap)\n        assert low == v.low\n        assert high == v.high\n\n\ndef _map_node(u, umap):\n    \"\"\"Map node, accounting for complement.\"\"\"\n    z = _copy._flip(u, u)\n    r = umap[z]\n    return _copy._flip(r, u)\n\n\ndef _setup(mod):\n    bdd_1 = mod.BDD()\n    bdd_2 = mod.BDD()\n    bdd_1.declare('x', 'y')\n    bdd_2.declare('x', 'y')\n    return bdd_1, bdd_2\n\n\ndef test_dump_load_same_order():\n    _test_dump_load_same_order(_autoref)\n    _test_dump_load_same_order(_cudd)\n\n\ndef _test_dump_load_same_order(mod):\n    b = mod.BDD()\n    b.declare('x', 'y', 'z')\n    expr = r'x /\\ ~ y'\n    u = b.add_expr(expr)\n    # dump\n    fname = 'hoho.json'\n    nodes = [u]\n    _copy.dump_json(nodes, fname)\n    # load\n    target = mod.BDD()\n    roots = _copy.load_json(\n        fname, target, load_order=True)\n    # assert\n    v, = roots\n    v_ = target.add_expr(expr)\n    assert v == v_, (v, v_)\n    # copy to source BDD manager\n    u_ = target.copy(v, b)\n    assert u == u_, (u, u_)\n\n\ndef test_dump_load_different_order():\n    _test_dump_load_different_order(_autoref)\n    _test_dump_load_different_order(_cudd)\n\n\ndef _test_dump_load_different_order(mod):\n    source = mod.BDD()\n    source.declare('x', 'y')\n    expr = ' x <=> y '\n    u = source.add_expr(expr)\n    # dump\n    fname = 'hoho.json'\n    nodes = [u]\n    _copy.dump_json(nodes, fname)\n    # load\n    target = mod.BDD()\n    target.declare('y', 'x')\n    roots = _copy.load_json(\n        fname, target, load_order=False)\n    # assert\n    v, = roots\n    v_ = target.add_expr(expr)\n    assert v == v_, (v, v_)\n"
  },
  {
    "path": "tests/cudd_test.py",
    "content": "\"\"\"Tests of the module `dd.cudd`.\"\"\"\n# This file is released in the public domain.\n#\nimport logging\n\nimport dd.cudd as _cudd\nimport pytest\n\nimport common\nimport common_bdd\nimport common_cudd\n\n\nlogging.getLogger('astutils').setLevel('ERROR')\n\n\nclass Tests(common.Tests):\n    def setup_method(self):\n        self.DD = _cudd.BDD\n\n\nclass BDDTests(common_bdd.Tests):\n    def setup_method(self):\n        self.DD = _cudd.BDD\n\n\nclass CuddTests(common_cudd.Tests):\n    def setup_method(self):\n        self.DD = _cudd.BDD\n        self.MODULE = _cudd\n\n\ndef test_str():\n    bdd = _cudd.BDD()\n    with pytest.warns(UserWarning):\n        s = str(bdd)\n    s + 'must be a string'\n\n\ndef test_insert_var():\n    bdd = _cudd.BDD()\n    level = 0\n    j = bdd.insert_var('x', level)\n    assert j == 0, j  # initially indices = levels\n    x = bdd.var_at_level(level)\n    assert x == 'x', x\n    level = 101\n    bdd.insert_var('y', level)\n    y = bdd.var_at_level(level)\n    assert y == 'y', y\n\n\ndef test_refs():\n    _cudd._test_incref()\n    _cudd._test_decref()\n\n\ndef test_len():\n    bdd = _cudd.BDD()\n    assert len(bdd) == 0, len(bdd)\n    u = bdd.true\n    assert len(bdd) == 1, len(bdd)\n    del u\n    assert len(bdd) == 0, len(bdd)\n    u = bdd.true\n    v = bdd.false\n    assert len(bdd) == 1, len(bdd)\n    bdd.add_var('x')\n    x = bdd.var('x')\n    assert len(bdd) == 2, len(bdd)\n    not_x = ~x\n    # len(bdd) is the number of referenced nodes\n    # a node is used both for the positive and\n    # negative literals of its variable\n    assert len(bdd) == 2, len(bdd)\n    del x\n    assert len(bdd) == 2, len(bdd)\n    del not_x\n    assert len(bdd) == 1, len(bdd)\n    del u, v\n    assert len(bdd) == 0, len(bdd)\n\n\ndef test_cube_array():\n    _cudd._test_dict_to_cube_array()\n    _cudd._test_cube_array_to_dict()\n\n\ndef test_dump_load_dddmp():\n    bdd = _cudd.BDD()\n    for var in ['x', 'y', 'z', 'w']:\n        bdd.add_var(var)\n    u = bdd.add_expr(r'(x /\\ ~ w) \\/ z')\n    fname = 'bdd.dddmp'\n    bdd.dump(fname, [u], filetype='dddmp')\n    u_, = bdd.load(fname)\n    assert u == u_\n\n\ndef test_load_sample0():\n    bdd = _cudd.BDD()\n    names = ['a', 'b', 'c']\n    for var in names:\n        bdd.add_var(var)\n    fname = 'sample0.dddmp'\n    u, = bdd.load(fname)\n    n = len(u)\n    assert n == 5, n\n    s = r'~ ( (a /\\ (b \\/ c)) \\/ (~ a /\\ (b \\/ ~ c)) )'\n    u_ = bdd.add_expr(s)\n    assert u == u_, (u, u_)\n\n\ndef test_and_exists():\n    bdd = _cudd.BDD()\n    for var in ['x', 'y']:\n        bdd.add_var(var)\n    # (\\E x:  x /\\ y) \\equiv y\n    x = bdd.add_expr('x')\n    y = bdd.add_expr('y')\n    qvars = ['x']\n    r = _cudd.and_exists(x, y, qvars)\n    assert r == y, (r, y)\n    # (\\E x:  x /\\ ~ x) \\equiv FALSE\n    not_x = bdd.apply('not', x)\n    r = _cudd.and_exists(x, not_x, qvars)\n    assert r == bdd.false\n\n\ndef test_or_forall():\n    bdd = _cudd.BDD()\n    for var in ['x', 'y']:\n        bdd.add_var(var)\n    # (\\A x, y:  x \\/ ~ y) \\equiv FALSE\n    x = bdd.var('x')\n    not_y = bdd.add_expr('~ y')\n    qvars = ['x', 'y']\n    r = _cudd.or_forall(x, not_y, qvars)\n    assert r == bdd.false, r\n\n\ndef test_swap():\n    bdd = _cudd.BDD()\n    bdd.declare('x', 'y')\n    x = bdd.var('x')\n    y = bdd.var('y')\n    # swap x and y\n    #\n    # This swap returns the same result as `bdd.let`\n    # with the same arguments, because `d` contains\n    # both `'x'` and `'y'` as keys.\n    #\n    # This result is obtained when the arguments are\n    # not checked for overlapping key-value pairs.\n    u = bdd.apply('and', x, ~ y)\n    d = dict(x='y', y='x')\n    with pytest.raises(ValueError):\n        f = bdd._swap(u, d)\n    # f_ = bdd.apply('and', ~ x, y)\n    # assert f == f_, (f, f_)\n    #\n    # swap x and y\n    # ensure swapping, not simultaneous substitution\n    #\n    # This swap returns a different result than\n    # `bdd.let` when given the same arguments,\n    # because `d` contains only `'x'` as key,\n    # so `let` does not result in simultaneous\n    # substitution.\n    u = bdd.apply('and', x, ~ y)\n    d = dict(x='y')  # swap x with y\n    f = bdd._swap(u, d)\n    f_ = bdd.apply('and', y, ~ x)\n    # compare to the corresponding test of `bdd.let`\n    assert f == f_, (f, f_)\n    #\n    # each variable should in at most one\n    # key-value pair of `d`\n    #\n    # 1) keys and values are disjoint sets\n    bdd.declare('z')\n    z = bdd.var('z')\n    u = bdd.apply('and', x, ~ y)\n    d = dict(x='y', y='z')\n    with pytest.raises(ValueError):\n        f = bdd._swap(u, d)\n    # The following result is obtained if the\n    # assertions are removed from `BDD._swap`.\n    # f_ = bdd.apply('and', y, ~ z)\n    # assert f == f_, bdd.to_expr(f)\n    #\n    # 2) each value appears once among values\n    u = bdd.apply('and', x, ~ y)\n    d = dict(x='y', z='y')\n    with pytest.raises(ValueError):\n        f = bdd._swap(u, d)\n    # The following result is obtained if the\n    # assertions are removed from `BDD._swap`.\n    # f_ = bdd.apply('and', y, ~ z)\n    # assert f == f_, bdd.to_expr(f)\n\n\ndef test_copy_bdd_same_indices():\n    # each va has same index in each `BDD`\n    bdd = _cudd.BDD()\n    other = _cudd.BDD()\n    assert bdd != other\n    dvars = ['x', 'y', 'z']\n    for var in dvars:\n        bdd.add_var(var)\n        other.add_var(var)\n    s = r'(x /\\ y) \\/ ~ z'\n    u0 = bdd.add_expr(s)\n    u1 = _cudd.copy_bdd(u0, other)\n    u2 = _cudd.copy_bdd(u1, bdd)\n    # involution\n    assert u0 == u2, (u0, u2)\n    # confirm\n    w = other.add_expr(s)\n    assert w == u1, (w, u1)\n    # different nodes\n    u3 = _cudd.copy_bdd(other.true, bdd)\n    assert u3 != u2, (u3, u2)\n\n\ndef test_copy_bdd_different_indices():\n    # each var has different index in each `BDD`\n    bdd = _cudd.BDD()\n    other = _cudd.BDD()\n    assert bdd != other\n    dvars = ['x', 'y', 'z']\n    for var in dvars:\n        bdd.add_var(var)\n    for var in reversed(dvars):\n        other.add_var(var)\n    s = r'(x \\/ ~ y) /\\ ~ z'\n    u0 = bdd.add_expr(s)\n    u1 = _cudd.copy_bdd(u0, other)\n    u2 = _cudd.copy_bdd(u1, bdd)\n    # involution\n    assert u0 == u2, (u0, u2)\n    # confirm\n    w = other.add_expr(s)\n    assert w == u1, (w, u1)\n    # different nodes\n    u3 = _cudd.copy_bdd(other.true, bdd)\n    assert u3 != u2, (u3, u2)\n\n\ndef test_copy_bdd_different_order():\n    bdd = _cudd.BDD()\n    other = _cudd.BDD()\n    assert bdd != other\n    dvars = ['x', 'y', 'z', 'w']\n    for index, var in enumerate(dvars):\n        bdd.add_var(var, index=index)\n        other.add_var(var, index=index)\n    # reorder\n    order = dict(w=0, x=1, y=2, z=3)\n    other.reorder(order)\n    # confirm resultant order\n    for var in order:\n        level_ = order[var]\n        level = other.level_of_var(var)\n        assert level == level_, (var, level, level_)\n    # same indices\n    for var in dvars:\n        i = bdd._index_of_var[var]\n        j = other._index_of_var[var]\n        assert i == j, (i, j)\n    # but different levels\n    for var in dvars:\n        i = bdd.level_of_var(var)\n        j = other.level_of_var(var)\n        assert i != j, (i, j)\n    # copy\n    s = r'(x \\/ ~ y) /\\ w /\\ (z \\/ ~ w)'\n    u0 = bdd.add_expr(s)\n    u1 = _cudd.copy_bdd(u0, other)\n    u2 = _cudd.copy_bdd(u1, bdd)\n    assert u0 == u2, (u0, u2)\n    u3 = _cudd.copy_bdd(other.false, bdd)\n    assert u3 != u2, (u3, u2)\n    # verify\n    w = other.add_expr(s)\n    assert w == u1, (w, u1)\n\n\ndef test_count_nodes():\n    bdd = _cudd.BDD()\n    [bdd.add_var(var) for var in ['x', 'y', 'z']]\n    u = bdd.add_expr(r'x /\\ y')\n    v = bdd.add_expr(r'x /\\ z')\n    assert len(u) == 3, len(u)\n    assert len(v) == 3, len(v)\n    bdd.reorder(dict(x=0, y=1, z=2))\n    n = _cudd.count_nodes([u, v])\n    assert n == 5, n\n    bdd.reorder(dict(z=0, y=1, x=2))\n    n = _cudd.count_nodes([u, v])\n    assert n == 4, n\n\n\ndef test_function():\n    bdd = _cudd.BDD()\n    bdd.add_var('x')\n    # x\n    x = bdd.var('x')\n    assert not x.negated\n    # ~ x\n    not_x = ~x\n    assert not_x.negated\n\n\nif __name__ == '__main__':\n    test_function()\n"
  },
  {
    "path": "tests/cudd_zdd_test.py",
    "content": "\"\"\"Tests of the module `dd.cudd_zdd`.\"\"\"\n# This file is released in the public domain.\n#\nimport inspect\nimport os\nimport subprocess\nimport sys\n\nimport dd.cudd as _cudd\nimport dd.cudd_zdd as _cudd_zdd\nimport dd._copy as _copy\nimport pytest\n\nimport common\nimport common_cudd\n\n\nclass Tests(common.Tests):\n    def setup_method(self):\n        self.DD = _cudd_zdd.ZDD\n\n\nclass CuddTests(common_cudd.Tests):\n    def setup_method(self):\n        self.DD = _cudd_zdd.ZDD\n        self.MODULE = _cudd_zdd\n\n\ndef test_str():\n    bdd =  _cudd_zdd.ZDD()\n    with pytest.warns(UserWarning):\n        s = str(bdd)\n    s + 'must be a string'\n\n\ndef test_false():\n    zdd = _cudd_zdd.ZDD()\n    u = zdd.false\n    assert len(u) == 0, len(u)\n\n\ndef test_true():\n    zdd = _cudd_zdd.ZDD()\n    zdd.declare('x', 'y', 'z', 'w')\n    u = zdd.true\n    assert u.low is not None\n    assert u.high is not None\n    assert len(u) == 4, len(u)\n\n\ndef test_true_node():\n    zdd = _cudd_zdd.ZDD()\n    zdd.declare('x', 'y')\n    u = zdd.true_node\n    assert u.low is None\n    assert u.high is None\n    assert len(u) == 0, len(u)\n\n\ndef test_index_at_level():\n    zdd = _cudd_zdd.ZDD()\n    zdd.add_var('x', 1)\n    level = zdd.level_of_var('x')\n    assert level == 1, (\n        level, zdd.index_of_var, zdd.vars)\n    level_to_index = {\n        -20: None,\n        -1: None,\n        0: 0,\n        1: 1,\n        2: None,\n        3: None,\n        100: None}\n    for level, index_ in level_to_index.items():\n        index = zdd._index_at_level(level)\n        assert index == index_, (\n            level, index, index_,\n            zdd.index_of_var, zdd.vars)\n    # no `dd.cudd_zdd.ZDD` variable declared at level 0\n    # CUDD indices range from 0 to 1\n    with pytest.raises(ValueError):\n        zdd.level_of_var(0)\n    # no CUDD variable at level 2\n    with pytest.raises(ValueError):\n        zdd.level_of_var(2)\n\n\ndef test_var_level_gaps():\n    zdd = _cudd_zdd.ZDD()\n    zdd.add_var('x', 2)\n    n_vars = len(zdd.vars)\n    assert n_vars == 1, n_vars\n    max_var_level = _max_var_level(zdd)\n    assert max_var_level == 2, max_var_level\n\n\ndef _max_var_level(zdd):\n    \"\"\"Return the maximum level in `zdd`.\n\n    The indices of variables in CUDD can span more\n    levels than the variables declared in `zdd`.\n    This happens when declaring variables with\n    noncontiguous levels, using `ZDD.add_var()`.\n\n    Nonetheless, `ZDD.add_var()` ensures that there\n    exists a variable in `ZDD.vars` whose level equals\n    the maximum level over CUDD indices.\n    \"\"\"\n    if not zdd.vars:\n        return None\n    return max(\n        zdd.level_of_var(var)\n        for var in zdd.vars)\n\n\ndef test_gt_var_levels():\n    zdd = _cudd_zdd.ZDD()\n    zdd.add_var('x', 1)\n    level_to_value = {\n        0: False,\n        1: False,\n        2: True,\n        3: True,\n        100: True}\n    for level, value_ in level_to_value.items():\n        value = zdd._gt_var_levels(level)\n        assert value == value_, (\n            level, value, value_,\n            zdd.index_of_var, zdd.vars)\n    with pytest.raises(ValueError):\n        zdd._gt_var_levels(-1)\n\n\ndef test_number_of_cudd_vars_without_gaps():\n    zdd = _cudd_zdd.ZDD()\n    # no variables\n    _assert_n_vars_max_level(0, 0, None, zdd)\n    # 1 declared variable\n    # 1 variable index in CUDD\n    zdd.declare('x')\n    _assert_n_vars_max_level(1, 1, 0, zdd)\n    # 2 declared variables\n    # 2 variable indices in CUDD\n    zdd.declare('y')\n    _assert_n_vars_max_level(2, 2, 1, zdd)\n\n\ndef test_number_of_cudd_vars_with_gaps():\n    zdd = _cudd_zdd.ZDD()\n    # no variables\n    _assert_n_vars_max_level(0, 0, None, zdd)\n    # 1 declared variable\n    # 2 variable indices in CUDD\n    zdd.add_var('x', 1)\n    _assert_n_vars_max_level(2, 1, 1, zdd)\n    # 2 declared variables\n    # 15 variable indices in CUDD\n    zdd.add_var('y', 14)\n    _assert_n_vars_max_level(15, 2, 14, zdd)\n\n\ndef _assert_n_vars_max_level(\n        n_cudd_vars:\n            int,\n        n_zdd_vars:\n            int,\n        max_var_level:\n            int,\n        zdd):\n    n_cudd_vars_ = zdd._number_of_cudd_vars()\n    assert n_cudd_vars_ == n_cudd_vars, (\n        n_cudd_vars_, n_cudd_vars)\n    n_zdd_vars_ = len(zdd.vars)\n    assert n_zdd_vars_ == n_zdd_vars, (\n        zdd.vars, n_zdd_vars)\n    max_var_level_ = _max_var_level(zdd)\n    assert max_var_level_ == max_var_level, (\n        max_var_level_, max_var_level)\n\n\ndef test_var():\n    zdd = _cudd_zdd.ZDD()\n    zdd.declare('x', 'y', 'z')\n    x = zdd.var('x')\n    x_ = zdd._var_cudd('x')\n    assert x == x_, len(x)\n    y = zdd.var('y')\n    y_ = zdd._var_cudd('y')\n    assert y == y_, len(y)\n    z = zdd.var('z')\n    z_ = zdd._var_cudd('z')\n    assert z == z_, len(z)\n\n\ndef test_support_cudd():\n    # support implemented by CUDD\n    zdd = _cudd_zdd.ZDD()\n    zdd.declare('x', 'y')\n    zdd._add_bdd_var(0)\n    zdd._add_bdd_var(1)\n    u = zdd.add_expr('~ x')\n    s = zdd._support_cudd(u)\n    assert s == {'y'}, s  # `{'x'}` is expected\n\n\ndef test_cudd_cofactor():\n    zdd = _cudd_zdd.ZDD()\n    zdd.declare('x', 'y')\n    u = zdd.add_expr(r'x /\\ ~ y')\n    r = zdd._cofactor_cudd(u, 'y', False)\n    r_ = zdd.add_expr(r'x /\\ ~ y')\n    assert r == r_, len(r)\n    u = zdd.add_expr(r'x /\\ y')\n    r = zdd._cofactor_cudd(u, 'x', True)\n    r_ = zdd.add_expr(r'~ x /\\ y')  # no node at x\n    assert r == r_\n\n\ndef test_find_or_add():\n    bdd = _cudd_zdd.ZDD()\n    bdd.declare('x', 'y', 'z')\n    v = bdd.add_expr(r'~ x /\\ y /\\ ~ z')\n    w = bdd.add_expr(r'~ x /\\ ~ y /\\ z')\n    u = bdd.find_or_add('x', v, w)\n    assert u.low == v, len(u)\n    assert u.high == w, len(u)\n    assert u.var == 'x', u.var\n    assert u.level == 0, u.level\n\n\ndef test_count():\n    zdd = _cudd_zdd.ZDD()\n    zdd.declare('x', 'y')\n    # FALSE\n    u = zdd.false\n    n = zdd.count(u, 2)\n    assert n == 0, n\n    # TRUE\n    u = zdd.true\n    n = zdd.count(u, 1)\n    assert n == 2, n\n    n = zdd.count(u, 2)\n    assert n == 4, n\n\n\ndef test_bdd_to_zdd_copy():\n    zdd = _cudd_zdd.ZDD()\n    zdd.declare('x', 'y', 'z')\n    bdd = _cudd.BDD()\n    bdd.declare('x', 'y', 'z')\n    u = bdd.add_expr('x')\n    v = bdd.copy(u, zdd)\n    x = zdd.var('x')\n    assert v == x, len(v)\n    print_size(v, 'v')\n    # copy `y`\n    u = bdd.var('y')\n    y = bdd.copy(u, zdd)\n    y_ = zdd.var('y')\n    assert y == y_, (y, y_)\n\n\ndef test_len():\n    zdd = _cudd_zdd.ZDD()\n    zdd.declare('x', 'y', 'z')\n    # x\n    x = zdd.var('x')\n    assert len(x) == 3, len(x)\n    # y\n    y = zdd.var('y')\n    assert len(y) == 3, len(y)\n    # x /\\ y /\\ ~ z\n    u = x & y & ~ zdd.var('z')\n    assert len(u) == 2, len(u)\n    # ~ x\n    u = zdd.add_expr('~ x')\n    assert len(u) == 2, len(u)\n\n\ndef test_ith_var_without_gaps():\n    zdd = _cudd_zdd.ZDD()\n    zdd.declare('x', 'y', 'z')\n    u = _cudd_zdd._ith_var('x', zdd)\n    # check ZDD for variable x\n    assert u.var == 'x', u.var\n    assert u.level == 0, u.level\n    assert u.low == zdd.false, (\n        u, u.low, zdd.false)\n    v = u.high\n    assert v.var == 'y', v.var\n    assert v.level == 1, v.level\n    assert v.low == v.high, (\n        v, v.low, v.high)\n    w = v.low\n    assert w.var == 'z'\n    assert w.level == 2, w.level\n    assert w.low == w.high, (\n        w, w.low, w.high)\n    assert w.low == zdd.true_node, (\n        w, w.low, zdd.true_node)\n    # check ZDD for variable y\n    u = _cudd_zdd._ith_var('y', zdd)\n    assert u.var == 'x', u.var\n    assert u.level == 0, u.level\n    assert u.low == u.high, (\n        u, u.low, u.high)\n    v = u.low\n    assert v.var == 'y', v.var\n    assert v.level == 1, v.level\n    assert v.low == zdd.false, (\n        v, v.low, zdd.false)\n    w = v.high\n    assert w.var == 'z', w.var\n    assert w.level == 2, w.level\n    assert w.low == w.high, (\n        w, w.low, w.high)\n    assert w.low == zdd.true_node, (\n        w, w.low, zdd.true_node)\n    # check ZDD for variable z\n    u = _cudd_zdd._ith_var('z', zdd)\n    assert u.var == 'x', u.var\n    assert u.level == 0, u.level\n    assert u.low == u.high, (\n        u, u.low, u.high)\n    v = u.low\n    assert v.var == 'y', v.var\n    assert v.level == 1, v.level\n    assert v.low == v.high, (\n        v, v.low, v.high)\n    w = v.low\n    assert w.var == 'z', w.var\n    assert w.level == 2, w.level\n    assert w.low == zdd.false, (\n        w, w.low, zdd.false)\n    assert w.high == zdd.true_node, (\n        w, w.high, zdd.true_node)\n\n\ndef test_ith_var_with_gaps():\n    zdd = _cudd_zdd.ZDD()\n    zdd.add_var('x', 1)\n    with pytest.raises(AssertionError):\n        # because 1 declared variable,\n        # but 2 CUDD variable indices\n        _cudd_zdd._ith_var('x', zdd)\n    zdd.vars.update(dict(y=0, z=3))\n    with pytest.raises(AssertionError):\n        _cudd_zdd._ith_var('x', zdd)\n\n\ndef test_disjunction():\n    zdd = _cudd_zdd.ZDD()\n    zdd.declare('w', 'x', 'y')\n    # x \\/ TRUE\n    v = zdd.add_expr('x')\n    w = zdd.true\n    u = zdd._disjoin_root(v, w)\n    assert u == w, len(u)\n    # x \\/ FALSE\n    w = zdd.false\n    u = zdd._disjoin_root(v, w)\n    assert u == v, len(u)\n    # x \\/ y\n    v = zdd.add_expr('x')\n    w = zdd.add_expr('y')\n    u = zdd._disjoin_root(v, w)\n    u_ = zdd.add_expr(r'x \\/ y')\n    assert u == u_, len(u)\n    # (~ w /\\ x) \\/ y\n    v = zdd.add_expr(r'~ w /\\ x')\n    w = zdd.add_expr('y')\n    u = zdd._disjoin_root(v, w)\n    u_ = zdd.add_expr(r'(~ w /\\ x) \\/ y')\n    assert u == u_, len(u)\n\n\ndef test_conjunction():\n    zdd = _cudd_zdd.ZDD()\n    zdd.declare('x', 'y', 'z')\n    v = zdd.var('x')\n    w = zdd.var('y')\n    u = zdd._conjoin_root(v, w)\n    u_ = zdd.add_expr(r'x /\\ y')\n    assert u == u_, len(u)\n    u = zdd._conjoin_root(v, ~ w)\n    u_ = zdd.add_expr(r'x /\\ ~ y')\n    assert u == u_, len(u)\n\n\ndef test_methods_disjoin_conjoin_gaps_opt():\n    run_python_with_optimization(\n        test_methods_disjoin_conjoin_gaps)\n\n\ndef test_methods_disjoin_conjoin_gaps():\n    import dd.cudd_zdd as _zdd\n    import pytest\n    zdd = _zdd.ZDD()\n    zdd.add_var('x', 20)\n    u = zdd.find_or_add(\n        'x', zdd.false, zdd.true_node)\n    level = 1\n    with pytest.raises(ValueError):\n        _zdd._call_method_disjoin(\n            zdd, level, u, ~ u, cache=dict())\n    with pytest.raises(ValueError):\n        _zdd._call_method_conjoin(\n            zdd, level, ~ u, u, cache=dict())\n\n\ndef test_method_disjoin():\n    zdd = _cudd_zdd.ZDD()\n    zdd.declare('x')\n    v = zdd.var('x')\n    level = 1\n    with pytest.raises(ValueError):\n        _cudd_zdd._call_method_disjoin(\n            zdd, level, v, v, dict())\n\n\ndef test_methods_disjoin_conjoin_with_opt():\n    run_python_with_optimization(\n        test_methods_disjoin_conjoin)\n\n\ndef test_methods_disjoin_conjoin():\n    import dd.cudd_zdd as _zdd\n    import pytest\n    zdd = _zdd.ZDD()\n    zdd.declare('x')\n    v = zdd.var('x')\n    true = zdd.true_node\n    level = 1\n    with pytest.raises(ValueError):\n        _zdd._call_method_disjoin(\n            zdd, level, v, true, dict())\n    with pytest.raises(ValueError):\n        _zdd._call_method_conjoin(\n            zdd, level, v, true, dict())\n    with pytest.raises(ValueError):\n        _zdd._call_method_conjoin(\n            zdd, level, v, ~ v, dict())\n\n\ndef run_python_with_optimization(\n        function):\n    \"\"\"Run `function` with `python -O`.\n\n    Start new `python` process\n    because Python's optimization level\n    cannot be changed at runtime.\n    \"\"\"\n    name = function.__name__\n    function_src = inspect.getsource(function)\n    assertion_src = inspect.getsource(_assert)\n    src = f'{function_src}\\n{assertion_src}\\n{name}()'\n    assert sys.executable, sys.executable\n    cmd = [\n        sys.executable,\n        '-O', '-c',\n        src]\n    proc = subprocess.run(\n        cmd, capture_output=True, text=True)\n    if proc.returncode == 0:\n        return\n    raise AssertionError(\n        f'The function `{name}`, when run with '\n        f'`{cmd[:-1]}`, resulted in exiting with '\n        f'return code {proc.returncode}.\\n'\n        f'The `stdout` was:\\n{proc.stdout}\\n'\n        f'The `stderr` was:\\n{proc.stderr}')\n\n\ndef _assert(test):\n    if test:\n        return\n    raise AssertionError(test)\n\n\ndef test_c_disjunction():\n    zdd = _cudd_zdd.ZDD()\n    zdd.declare('w', 'x', 'y')\n    v = zdd.add_expr(r'~ w /\\ x')\n    w = zdd.add_expr('y')\n    u = _cudd_zdd._c_disjoin(v, w)\n    u_ = zdd.add_expr(r'(~ w /\\ x) \\/ y')\n    assert u == u_, len(u)\n\n\ndef test_c_conjunction():\n    zdd = _cudd_zdd.ZDD()\n    zdd.declare('x', 'y', 'z')\n    x = zdd.var('x')\n    y = zdd.var('y')\n    u = _cudd_zdd._c_conjoin(x, y)\n    u_ = zdd.add_expr(r'x /\\ y')\n    assert u == u_, len(u)\n\n\ndef test_c_disjoin_conjoin():\n    zdd = _cudd_zdd.ZDD()\n    zdd.declare('x')\n    u = zdd.var('x')\n    true = zdd.true_node\n    level = 1\n    with pytest.raises(AssertionError):\n        _cudd_zdd._call_disjoin(level, u, true)\n    with pytest.raises(AssertionError):\n        _cudd_zdd._call_conjoin(level, u, true)\n    with pytest.raises(AssertionError):\n        _cudd_zdd._call_conjoin(level, true, u)\n\n\ndef test_c_disjoin_conjoin_leaf_check():\n    zdd = _cudd_zdd.ZDD()\n    leaf_level = zdd.false.level\n    u = zdd.true_node\n    r = _cudd_zdd._call_disjoin(\n        leaf_level, u, u)\n    assert r == u, (r.level, u.level)\n    zdd.declare('x')\n    v = zdd.var('x')\n    with pytest.raises(AssertionError):\n        _cudd_zdd._call_disjoin(\n            leaf_level, v, v)\n    r = _cudd_zdd._call_conjoin(\n        leaf_level, u, u)\n    assert r == u, (r.level, u.level)\n    with pytest.raises(AssertionError):\n        _cudd_zdd._call_conjoin(\n            leaf_level, v, v)\n\n\ndef test_c_exist():\n    zdd = _cudd_zdd.ZDD()\n    zdd.declare('x', 'y', 'z')\n    # \\E x:  (x /\\ ~ y) \\/ ~ z\n    u = zdd.add_expr(r'(x /\\ ~ y) \\/ ~ z')\n    qvars = ['x']\n    r = _cudd_zdd._c_exist(qvars, u)\n    r_ = zdd.exist(qvars, u)\n    assert r == r_, len(r)\n    # \\E x:  x\n    u = zdd.add_expr('x')\n    qvars = ['x']\n    r = _cudd_zdd._c_exist(qvars, u)\n    r_ = zdd.exist(qvars, u)\n    assert r == r_, len(r)\n\n\ndef test_dump():\n    zdd = _cudd_zdd.ZDD()\n    zdd.declare('x', 'y', 'w')\n    u = zdd.add_expr('~ w')\n    fname = 'not_w.pdf'\n    if os.path.isfile(fname):\n        os.remove(fname)\n    assert not os.path.isfile(fname)\n    zdd.dump(fname, [u])\n    assert os.path.isfile(fname)\n\n\ndef test_dict_to_zdd():\n    zdd = _cudd_zdd.ZDD()\n    zdd.declare('x', 'y', 'z')\n    qvars = {'x', 'z'}\n    u = _cudd_zdd._dict_to_zdd(qvars, zdd)\n    assert len(u) == 2, len(u)\n    assert u.var == 'x', u.var\n    assert u.low == u.high\n    v = u.low\n    assert v.var == 'z', v.var\n    assert v.low == v.high\n    assert v.low == zdd.true_node\n\n\ndef print_size(u, msg):\n    n = len(u)\n    print(f'Dag size of {msg}: {n}')\n\n\nif __name__ == '__main__':\n    Tests().test_support()\n    # test_compose()\n"
  },
  {
    "path": "tests/dddmp_test.py",
    "content": "\"\"\"Tests of the module `dd.dddmp`.\"\"\"\nimport logging\nimport os\n\nimport dd.dddmp as _dddmp\nimport pytest\n\n\nlogger = logging.getLogger(\n    'dd.dddmp.parser_logger')\nlogger.setLevel(logging.ERROR)\n\n\ndef test_lexer():\n    lexer = _dddmp.Lexer()\n    s = '.ghs?5'\n    lexer.lexer.input(s)\n    tok = lexer.lexer.token()\n    assert tok.value == '.ghs'\n    with pytest.raises(Exception):\n        lexer.lexer.token()\n\n\ndef test_parser():\n    parser = _dddmp.Parser()\n    with pytest.raises(Exception):\n        parser.parser.parse(\n            input='.mode C',\n            lexer=parser.lexer.lexer)\n\n\ndef test_sample0():\n    fname = 'sample0.dddmp'\n    parser = _dddmp.Parser()\n    bdd, n_vars, ordering, roots = parser.parse(fname)\n    assert set(bdd) == set(range(1, 6)), sorted(bdd)\n    bdd_ = {\n        1: (None, None),\n        2: (-1, 1),  # -1 is a complemented edge\n        3: (2, 1),\n        4: (-2, 1),  # -2 is a complemented edge\n        5: (4, 3)}\n    for u, (level, v, w) in bdd.items():\n        v_, w_ = bdd_[u]\n        assert v == v_, (u, v, w, v_, w_)\n        assert w == w_, (u, v, w, v_, w_)\n    # other attributes\n    assert n_vars == 50, n_vars\n    assert ordering == {\n        'a': 1, 'b': 2, 'c': 3}, ordering\n    assert roots == {-5}\n    # debugging\n    # h.dump('bdd.pdf', roots, filetype='pdf')\n\n\ndef test_sample1():\n    fname = 'sample1.dddmp'\n    parser = _dddmp.Parser()\n    parser.build(debug=True)\n    bdd, n_vars, ordering, roots = parser.parse(fname)\n    assert len(bdd) == 16, len(bdd)\n    assert n_vars == 10, n_vars\n    assert roots == {6, -13, -16}, roots\n\n\ndef test_sample2():\n    # x /\\ y\n    # where x, y have indices 0, 1\n    fname = 'sample2.dddmp'\n    bdd = _dddmp.load(fname)\n    n = len(bdd)\n    assert n == 3, n\n    n_vars = len(bdd.vars)\n    assert n_vars == 2, n_vars\n    assert bdd.roots == {3}, bdd.roots\n    root = 3\n    i, v, w = bdd.succ(root)\n    assert i == 0, i\n    assert v == -1, v\n    i, v, w = bdd.succ(w)\n    assert i == 1, i\n    assert v == -1, v\n    assert w == 1, w\n    # overwrite indices with strings\n    bdd.vars = dict(x=0, y=1)\n    u = bdd.add_expr(r'x /\\ y')\n    assert u == root, u\n\n\ndef test_sample3():\n    # x /\\ y\n    # where x, y are at levels 1, 0\n    # nodes are labeled with var names\n    fname = 'sample3.dddmp'\n    bdd = _dddmp.load(fname)\n    n = len(bdd)\n    assert n == 3, n\n    n_vars = len(bdd.vars)\n    assert n_vars == 2, n_vars\n    assert bdd.roots == {3}, bdd.roots\n    root = 3\n    u = bdd.add_expr(r'x /\\ y')\n    assert root == u, u\n\n\ndef test_load_dddmp():\n    # small sample\n    fname = 'sample0.dddmp'\n    bdd = _dddmp.load(fname)\n    n = len(bdd)\n    n_vars = len(bdd.vars)\n    assert n == 5, n\n    assert n_vars == 3, n_vars\n    assert bdd.roots == {-5}, bdd.roots\n    root = -5\n    u = bdd.add_expr(\n        r'~ ( (a /\\ (b \\/ c)) \\/ '\n        r'(~ a /\\ (b \\/ ~ c)) )')\n    assert u == root, (u, root)\n    # larger sample\n    fname = 'sample1.dddmp'\n    bdd = _dddmp.load(fname)\n    n = len(bdd)\n    n_vars = len(bdd.vars)\n    assert n == 16, n\n    assert n_vars == 10, n_vars\n    assert bdd.roots == {6, -13, -16}\n    varnames = {\n        'G0', 'G1', 'G2', 'G3', 'G5', 'G6',\n        'G7', 'TMP1', 'TMP2', 'TMP3'}\n    bddvars = set(bdd.vars)\n    assert bddvars == varnames, bddvars\n    bdd.assert_consistent()\n\n\ndef test_rewrite_tables():\n    prefix = '_dddmp_parser_state_machine'\n    for ext in ('.py', '.pyc'):\n        fname = f'{prefix}{ext}'\n        if os.path.isfile(fname):\n            os.remove(fname)\n    _dddmp._rewrite_tables()\n    assert os.path.isfile(f'{prefix}.py')\n\n\ndef to_nx(bdd, n_vars, ordering, roots):\n    \"\"\"Convert parsing result to `networkx` graph.\n\n    Convert result of the method\n    `dd.dddmp.Parser.parse()` to an instance of\n    the class `networkx.MultiDiGraph`.\n\n    The arguments `bdd`, `n_vars`, `ordering`, `roots`\n    are those values that are returned from the method\n    `dd.dddmp.Parser.parse`.\n    \"\"\"\n    import networkx as nx\n    level2var = {\n        ordering[k]: k\n        for k in ordering}\n    level2var[n_vars + 1] = 'T'\n    h = nx.MultiDiGraph()\n    h.roots = roots\n    for u in bdd:\n        i, v, w = bdd[u]\n        assert u >= 0, u\n        label = level2var[i]\n        h.add_node(u, label=label)\n        # terminal ?\n        if v is None or w is None:\n            assert v is None\n            assert w is None\n            continue\n        complemented = '-1' if v < 0 else ' '\n        h.add_edge(\n            u, abs(v),\n            label=complemented,\n            style='dashed')\n        assert w >= 0, w\n            # \"then\" edge cannot be complemented\n        h.add_edge(u, w)\n    return h\n\n\ndef test_dump_with_cudd_load_with_dddmp():\n    import dd.cudd\n    fname = 'foo.dddmp'\n    # dump\n    bdd = dd.cudd.BDD()\n    bdd.declare('y', 'x')\n    u = bdd.add_expr(r'x /\\ y')\n    bdd.dump(fname, [u])\n    # load\n    bdd = _dddmp.load(fname)\n    print(bdd.roots)\n    u, = bdd.roots\n    u_ = bdd.add_expr(r'x /\\ y')\n    assert u == u_, (u, u_)\n    expr = bdd.to_expr(u)\n    print(expr)\n\n\nif __name__ == '__main__':\n    test_load_dddmp()\n"
  },
  {
    "path": "tests/inspect_cython_signatures.py",
    "content": "\"\"\"Compare the signatures of methods in a Cython `cdef` class to ABC.\n\nA `cdef` class cannot inherit from an ABC (or a Python class that serves\nthe same purpose). `inspect.signature` works when the compiler directive\n`binding` is enabled, but returns keyword arguments as if they are positional.\n\nThis script reports any mismatch of argument names (ignoring which ones\nare keyword arguments) between methods of the classes:\n\n- `dd._abc.BDD` (the specification)\n- `dd.cudd.BDD` (the implementation)\n\nMethods present in `_abc.BDD` but absent from `cudd.BDD` are reported too.\nUse the script to ensure the API is implemented, also for other Cython\nmodules, for example `dd.sylvan`.\n\nMEMO: Remember to enable `binding` when using this script.\n\"\"\"\n# Copyright 2017 by California Institute of Technology\n# All rights reserved. Licensed under BSD-3.\n#\nimport inspect\nimport logging\nimport warnings\n\nimport dd._abc as _abc\nimport dd.cudd as _cudd\n\n\nlog = logging.getLogger(__name__)\n\n\ndef inspect_signatures(spec, imp):\n    \"\"\"Print mismatches of method names or argument names.\n\n    @param spec:\n        the specification\n    @param imp:\n        the implementation\n    \"\"\"\n    print(f'Specification class: {type(spec)}')\n    print(f'Implementation class: {type(imp)}')\n    print('Checking whether all spec methods are implemented:\\n')\n    spec_dir = dir(spec)\n    imp_dir = dir(imp)\n    for method_name in spec_dir:\n        if is_hidden(method_name):\n            continue\n        method = getattr(spec, method_name)\n        if not callable(method):\n            continue\n        log.info(f'\"{method_name}\" is callable')\n        if method_name not in imp_dir:\n            print(f'MISSING implementation for \"{method_name}\"\\n')\n            continue\n        assert method_name in spec_dir, method_name\n        assert method_name in imp_dir, method_name\n        spec_method = getattr(spec, method_name)\n        imp_method = getattr(imp, method_name)\n        spec_sig = get_signature(spec_method)\n        imp_sig = get_signature(imp_method)\n        if spec_sig is None or imp_sig is None:\n            continue\n        spec_args = spec_sig.parameters.keys()\n        imp_args = imp_sig.parameters.keys()\n        if spec_args != imp_args:\n            print(\n                f'MISMATCH: method \"{method_name}\"\\n'\n                f'    spec args: {spec_args}\\n'\n                f'    imp args: {imp_args}\\n')\n    print('\\nExtra methods:\\n')\n    for method_name in imp_dir:\n        if is_hidden(method_name):\n            continue\n        method = getattr(imp, method_name)\n        if not callable(method):\n            continue\n        if method_name not in spec_dir:\n            print(method_name)\n\n\ndef is_hidden(method_name):\n    \"\"\"Return `True` if not an interface method.\"\"\"\n    return method_name.startswith('_')\n\n\ndef get_signature(func):\n    \"\"\"Wrapper of `inspect.signature` with Cython reminder.\"\"\"\n    try:\n        sig = inspect.signature(func)\n    except ValueError:\n        warnings.warn(\n            'Compile `dd.cudd` with the compiler directive `binding`'\n            f' for the function \"{func}\"')\n        sig = None\n    return sig\n\n\ndef _main():\n    \"\"\"Check that `dd.cudd.BDD` implements `dd._abc`.\"\"\"\n    # BDD manager\n    a = _abc.BDD()\n    b = _cudd.BDD()\n    inspect_signatures(a, b)\n    # BDD nodes\n    print(30 * '-' + '\\n')\n    u = _abc.Operator\n    # cannot instantiate `dd.cudd.Function`\n    # without a `DdNode` pointer\n    b.declare('x')\n    v = b.add_expr('x')\n    inspect_signatures(u, v)\n\n\nif __name__ == '__main__':\n    _main()\n"
  },
  {
    "path": "tests/iterative_recursive_flattener.py",
    "content": "\"\"\"Mapping trees to BDDs by iteration, and by recursion.\n\nThe iterative traversal avoids exceeding:\n- CPython's call-stack bound, and\n- the underlying C call-stack bound.\n\"\"\"\n# This file is released in the public domain.\n#\nimport typing as _ty\n\n\n_QUANTIFIERS: _ty.Final = {\n    r'\\A',\n    r'\\E'}\n_LEAFS: _ty.Final = {\n    'bool',\n    'num',\n    'var'}\n_BOOLEANS: _ty.Final = {\n    'false',\n    'true'}\n\n\ndef _recurse_syntax_tree(\n        tree,\n        bdd):\n    r\"\"\"Add abstract syntax `tree` to `self`.\n\n    ```tla\n    ASSUME\n        /\\ hasattr(tree_node, 'operator')\n        /\\ hasattr(tree_node, 'operands')\n        /\\ \\/ ~ is_leaf(tree_node)\n           \\/ /\\ hasattr(tree_node, value)\n              /\\ \\/ tree_node.value \\in {\"FALSE\", \"TRUE\"}\n                    (* for leaf nodes that represent\n                    Boolean constants\n                    *)\n                 \\/ tree_node.value \\in STRING \\ {\n                        \"FALSE\", \"TRUE\"}\n                    (* for leaf nodes that represent\n                    identifiers of BDD variables, or\n                    numeric literals\n                    *)\n    ```\n    \"\"\"\n    match tree.type:\n        case 'operator':\n            if (tree.operator in _QUANTIFIERS and\n                    len(tree.operands) == 2):\n                qvars, expr = tree.operands\n                qvars = {x.value for x in qvars}\n                forall = (tree.operator == r'\\A')\n                u = _recurse_syntax_tree(expr, bdd)\n                return bdd.quantify(\n                    u, qvars,\n                    forall=forall)\n            elif tree.operator == r'\\S':\n                expr, rename = tree.operands\n                rename = {\n                    k.value: v.value\n                    for k, v in rename}\n                u = _recurse_syntax_tree(expr, bdd)\n                return bdd.rename(u, rename)\n            else:\n                operands = [\n                    _recurse_syntax_tree(x, bdd)\n                    for x in tree.operands]\n                return bdd.apply(\n                    tree.operator, *operands)\n        case 'bool':\n            value = tree.value.lower()\n            if value not in _BOOLEANS:\n                raise ValueError(tree.value)\n            return getattr(bdd, value)\n        case 'var':\n            return bdd.var(tree.value)\n        case 'num':\n            i = int(tree.value)\n            return bdd._add_int(i)\n    raise ValueError(\n        f'unknown node type:  {tree.type = }')\n\n\ndef _reduce_syntax_tree(\n        tree,\n        bdd):\n    \"\"\"Convert syntax tree to decision diagram.\n\n    This function is implemented iteratively\n    in Python, in order to avoid recursion\n    limits of Python's implementation.\n    \"\"\"\n    stack = [\n        list(),\n        [tree]]\n    while len(stack) > 1:\n        _reduce_step(stack, bdd)\n    if len(stack) == 1 and len(stack[0]):\n        res, = stack[0]\n        return res\n    raise AssertionError(stack)\n\n\ndef _reduce_step(\n        stack:\n            list,\n        bdd\n        ) -> None:\n    \"\"\"Step in iteration of tree reduction.\"\"\"\n    tree, *operands = stack[-1]\n    match tree.type:\n        case 'operator':\n            if tree.operator in _QUANTIFIERS:\n                _reduce_quantifier(\n                    tree, operands, stack, bdd)\n            elif tree.operator == r'\\S':\n                _reduce_substitution(\n                    tree, operands, stack, bdd)\n            else:\n                _reduce_operator(\n                    tree, operands, stack, bdd)\n        case 'bool':\n            stack.pop()\n            value = tree.value.lower()\n            if value not in _BOOLEANS:\n                raise ValueError(tree.value)\n            u = getattr(bdd, value)\n            stack[-1].append(u)\n        case 'var':\n            stack.pop()\n            value = bdd.var(tree.value)\n            stack[-1].append(value)\n        case 'num':\n            stack.pop()\n            number = int(tree.value)\n            value = bdd._add_int(number)\n            stack[-1].append(value)\n        case _:\n            raise ValueError(\n                f'unknown node type:  {tree.type}')\n\n\ndef _reduce_quantifier(\n        tree,\n        operands,\n        stack:\n            list,\n        bdd\n        ) -> None:\n    \"\"\"Reduce quantifier tree.\"\"\"\n    if not operands:\n        _, successor = tree.operands\n        stack.append([successor])\n        return\n    u, = operands\n    qvars, _ = tree.operands\n    qvars = {\n        name.value\n        for name in qvars}\n    forall = (tree.operator == r'\\A')\n    res = bdd.quantify(\n        u, qvars,\n        forall=forall)\n    stack.pop()\n    stack[-1].append(res)\n\n\ndef _reduce_substitution(\n        tree,\n        operands,\n        stack:\n            list,\n        bdd\n        ) -> None:\n    \"\"\"Reduce `LET`.\"\"\"\n    if not operands:\n        successor, _ = tree.operands\n        stack.append([successor])\n        return\n    u, = operands\n    _, renaming = tree.operands\n    renaming = {\n        k.value: v.value\n        for k, v in renaming}\n    res = bdd.rename(u, renaming)\n    stack.pop()\n    stack[-1].append(res)\n\n\ndef _reduce_operator(\n        tree,\n        operands,\n        stack:\n            list,\n        bdd\n        ) -> None:\n    \"\"\"Reduce operator application.\"\"\"\n    n_operands = len(operands)\n    n_successors = len(tree.operands)\n    if 0 < n_operands == n_successors:\n        res = bdd.apply(\n            tree.operator,\n            *operands)\n        stack.pop()\n        stack[-1].append(res)\n    elif 0 <= n_operands < n_successors:\n        successor = tree.operands[n_operands]\n        stack.append([successor])\n    else:\n        raise AssertionError(\n            tree, operands)\n"
  },
  {
    "path": "tests/mdd_test.py",
    "content": "\"\"\"Tests of the module `dd.mdd`.\"\"\"\nimport logging\nimport os\n\nimport dd.bdd\nimport dd.mdd\n\n\nlogger = logging.getLogger(__name__)\n\n\ndef test_ite():\n    dvars = dict(\n        x=dict(level=0, len=2),\n        y=dict(level=1, len=2))\n    mdd = dd.mdd.MDD(dvars)\n    u = mdd.find_or_add(0, -1, 1)\n    v = mdd.find_or_add(1, -1, 1)\n    g = mdd.find_or_add(0, -1, 1)\n    r_ = mdd.find_or_add(0, v, 1)\n    r = mdd.ite(g, u, v)\n    assert r == r_, (r, r_)\n\n\ndef test_find_or_add():\n    dvars = dict(x=dict(level=0, len=4),\n                 y=dict(level=1, len=2))\n    m = dd.mdd.MDD(dvars)\n    u = m.find_or_add(0, 1, -1, 1, 1)\n    # m.dump('hehe.pdf')\n    print(m.to_expr(u))\n\n\ndef test_bdd_to_mdd():\n    ordering = {'x': 0, 'y': 1}\n    bdd = dd.bdd.BDD(ordering)\n    u = bdd.add_expr(r'x /\\ ~ y')\n    bdd.incref(u)\n    # BDD -> MDD\n    dvars = dict(\n        x=dict(level=1, len=2, bitnames=['x']),\n        y=dict(level=0, len=2, bitnames=['y']))\n    mdd, umap = dd.mdd.bdd_to_mdd(bdd, dvars)\n    v = umap[abs(u)]\n    if u < 0:\n        v = -v\n    print(v)\n    bdd.decref(u)\n\n\ndef test_mdd_dump_to_pdf():\n    dvars = dict(\n        x=dict(level=0, len=2),\n        y=dict(level=1, len=2))\n    mdd = dd.mdd.MDD(dvars)\n    v = mdd.find_or_add(1, -1, 1)\n    u = mdd.find_or_add(0, -1, -v)\n        # x /\\ ~ y\n    assert u < 0, u\n    filename = 'mdd.pdf'\n    if os.path.isfile(filename):\n        os.remove(filename)\n    mdd.dump('mdd.pdf')\n    assert os.path.isfile(filename)\n\n\nif __name__ == '__main__':\n    test_bdd_to_mdd()\n"
  },
  {
    "path": "tests/parser_test.py",
    "content": "\"\"\"Tests of module `dd._parser`.\"\"\"\n# This file is released in the public domain.\n#\nimport collections.abc as _abc\nimport itertools as _itr\nimport logging\nimport math\nimport sys\nimport typing as _ty\n\nimport dd.autoref as _bdd\nimport dd._parser\nimport pytest\n\nimport iterative_recursive_flattener as _flattener\n\n\n_log = logging.getLogger(__name__)\n\n\ndef _make_parser_test_expressions(\n        ) -> _abc.Iterable[str]:\n    \"\"\"Yield test formulas.\"\"\"\n    expressions = [\n        '~ FALSE',\n        '~ TRUE',\n        '! FALSE',\n        '! TRUE',\n        '@15',\n        '@-24',\n        r'FALSE /\\ TRUE',\n        r'TRUE /\\ FALSE',\n        r'FALSE \\/ TRUE',\n        r'TRUE \\/ FALSE',\n        r'TRUE \\/ TRUE \\/ FALSE',\n        'TRUE # FALSE',\n        'TRUE && TRUE',\n        'FALSE || TRUE',\n        'TRUE & FALSE',\n        'FALSE | TRUE',\n        'TRUE ^ FALSE',\n        r'\\E x, y, z:  TRUE ^ x => y',\n        r'\\A y:  y \\/ x',\n        r'(TRUE) => (FALSE /\\ TRUE)',\n        'TRUE <=> TRUE',\n        '~ (FALSE <=> FALSE)',\n        'ite(FALSE, FALSE, TRUE)',\n        \" var_name' => x' \",\n        ]\n    def rm_blanks(expr):\n        return expr.replace('\\x20', '')\n    return _itr.chain(\n        expressions,\n        map(rm_blanks, expressions))\n\n\nBDD_TRANSLATION_TEST_EXPRESSIONS: _ty.Final = [\n    '~ a',\n    r'a /\\ b',\n    r'a \\/ b',\n    'a => b',\n    'a <=> b',\n    'a # b',\n    'a ^ b',\n    r'\\E a:  a => b',\n    r'\\A a:  \\E b:  a \\/ ~ b',\n    r'! a /\\ ~ b',\n    'a || b',\n    'a | b | c',\n    'a && b && c',\n    'a & b',\n    'b -> a',\n    'c -> a -> b',\n    'b <-> a',\n    r'(a \\/ b) & c',\n    ]\nPARSER_TEST_EXPRESSIONS: _ty.Final = list(\n    _make_parser_test_expressions())\n\n\ndef test_all_parsers_same_results():\n    parser = dd._parser.Parser()\n    bdd = _bdd.BDD()\n    bdd.declare('a', 'b', 'c')\n    for expr in BDD_TRANSLATION_TEST_EXPRESSIONS:\n        u1 = dd._parser.add_expr(expr, bdd)\n            # translator that directly\n            # creates BDDs from expression strings\n        tree = parser.parse(expr)\n            # parser creates a syntax tree\n        u2 = _flattener._recurse_syntax_tree(tree, bdd)\n            # recursive translation of\n            # syntax tree to BDD\n        u3 = _flattener._reduce_syntax_tree(tree, bdd)\n            # iterative translation of\n            # syntax tree to BDD\n        assert u1 == u2, (\n            u1, u2,\n            bdd.to_expr(u1),\n            bdd.to_expr(u2))\n        assert u2 == u3, (u2, u3)\n\n\ndef test_translator_vs_recursion_limit():\n    bdd = _bdd.BDD()\n    bdd.declare('a', 'b', 'c')\n    parser = dd._parser.Parser()\n    # expression < recursion limit\n    expr = r'a /\\ b \\/ c'\n    dd._parser.add_expr(expr, bdd)\n    # expression > recursion limit\n    expr = make_expr_gt_recursion_limit()\n    dd._parser.add_expr(expr, bdd)\n\n\ndef test_log_syntax_tree_to_bdd():\n    bdd = _bdd.BDD()\n    bdd.declare('a')\n    expr = syntax_tree_of_shape('log')\n    # parse to syntax tree\n    parser = dd._parser.Parser()\n    tree = parser.parse(expr)\n    # flatten to BDD\n    u1 = dd._parser.add_expr(expr, bdd)\n    u2 = _flattener._recurse_syntax_tree(tree, bdd)\n    u3 = _flattener._reduce_syntax_tree(tree, bdd)\n    assert u1 == u2, (u1, u2)\n    assert u2 == u3, (u2, u3)\n\n\ndef test_linear_syntax_tree_to_bdd():\n    bdd = _bdd.BDD()\n    bdd.declare('a')\n    expr = syntax_tree_of_shape('linear')\n    # parse to syntax tree\n    parser = dd._parser.Parser()\n    tree = parser.parse(expr)\n    # flatten to BDD\n    u1 = dd._parser.add_expr(expr, bdd)\n    u2 = _flattener._reduce_syntax_tree(tree, bdd)\n    assert u1 == u2, (u1, u2)\n    with pytest.raises(RecursionError):\n        _flattener._recurse_syntax_tree(tree, bdd)\n\n\ndef make_expr_gt_recursion_limit(\n        ) -> str:\n    \"\"\"Return expression with many operators.\n\n    The returned expression contains more\n    operator applications than Python's\n    current recursion limit.\n    \"\"\"\n    recursion_limit = sys.getrecursionlimit()\n    n_operators = 2 * recursion_limit\n    tail = n_operators * r' /\\ a '\n    return f' a{tail} '\n\n\ndef syntax_tree_of_shape(\n        shape:\n            _ty.Literal[\n                'log',\n                'linear']\n        ) -> str:\n    \"\"\"Return expression.\"\"\"\n    match shape:\n        case 'log':\n            delimit = True\n        case 'linear':\n            delimit = False\n        case _:\n            raise ValueError(shape)\n    recursion_limit = sys.getrecursionlimit()\n    log2 = math.log2(recursion_limit)\n    depth = round(2 + log2)\n    expr = 'a'\n    for _ in range(depth):\n        expr = _delimit(\n            rf'{expr} /\\ {expr}',\n            '(', ')',\n            delimit)\n    return expr\n\n\ndef _delimit(\n        expr:\n            str,\n        start:\n            str,\n        end:\n            str,\n        delimit:\n            bool\n        ) -> str:\n    \"\"\"Return `expr` delimited.\"\"\"\n    if not delimit:\n        return expr\n    return f'{start} {expr} {end}'\n\n\ndef test_lexing():\n    lexer = dd._parser.Lexer()\n    expr = 'variable'\n    tokens = tokenize(expr, lexer)\n    assert len(tokens) == 1, tokens\n    token, = tokens\n    assert token.type == 'NAME', token.type\n    assert token.value == 'variable', token.value\n    expr = \" primed' \"\n    tokens = tokenize(expr, lexer)\n    assert len(tokens) == 1, tokens\n    token, = tokens\n    assert token.type == 'NAME', token.type\n    assert token.value == \"primed'\", token.value\n    expr = '~ a'\n    tokens = tokenize(expr, lexer)\n    assert len(tokens) == 2, tokens\n    tilde, name = tokens\n    assert tilde.type == 'NOT', tilde.type\n    assert tilde.value == '!', tilde.value\n    assert name.type == 'NAME', name.type\n    assert name.value == 'a', name.value\n    expr = '! a'\n    tokens = tokenize(expr, lexer)\n    assert len(tokens) == 2, tokens\n    not_, name = tokens\n    assert not_.type == 'NOT', not_.type\n    assert not_.value == '!', not_.value\n    assert name.type == 'NAME', name.type\n    assert name.value == 'a', name.value\n    infixal = [\n        (r'a /\\ b', 'AND', '&'),\n        ('a && b', 'AND', '&'),\n        ('a & b', 'AND', '&'),\n        (r'a \\/ b', 'OR', '|'),\n        ('a || b', 'OR', '|'),\n        ('a | b', 'OR', '|'),\n        ('a => b', 'IMPLIES', '=>'),\n        ('a -> b', 'IMPLIES', '=>'),\n        ('a <=> b', 'EQUIV', '<->'),\n        ('a <-> b', 'EQUIV', '<->'),\n        ('a # b', 'XOR', '#'),\n        ('a ^ b', 'XOR', '^'),\n        ('a = b', 'EQUALS', '='),\n        ]\n    for expr, op_type, op_value in infixal:\n        tokens = tokenize(expr, lexer)\n        assert len(tokens) == 3, tokens\n        a, op, b = tokens\n        _assert_names_operator(\n            op, a, b, op_type, op_value)\n    expr = '@4'\n    tokens = tokenize(expr, lexer)\n    assert len(tokens) == 2, tokens\n    at, four = tokens\n    assert at.type == 'AT', at.type\n    assert at.value == '@', at.value\n    assert four.type == 'NUMBER', four.type\n    assert four.value == '4', four.value\n    expr = '@-1'\n    tokens = tokenize(expr, lexer)\n    assert len(tokens) == 3, tokens\n    at, minus, one = tokens\n    assert at.type == 'AT', at.type\n    assert at.value == '@', at.value\n    assert minus.type == 'MINUS', minus.type\n    assert minus.value == '-', minus.value\n    assert one.type == 'NUMBER', one.type\n    assert one.value == '1', one.value\n    expr = r'\\A x1, x2, x3:  (x1 => (x2 <=> x3))'\n    tokens = tokenize(expr, lexer)\n    assert len(tokens) == 16, tokens\n    (forall, x1_1, comma_1, x2_1, comma_2,\n        x3_1, colon, lparen_1, x1_2, implies,\n        lparen_2, x2_2, equiv, x3_2, rparen_1,\n        rparen_2) = tokens\n    assert forall.type == 'FORALL', forall.type\n    assert forall.value == r'\\A', forall.value\n    assert x1_1.type == 'NAME', x1_1.type\n    assert x1_1.value == 'x1', x1_1.value\n    assert x1_2.type == 'NAME', x1_2.type\n    assert x1_2.value == 'x1', x1_2.value\n    assert x2_1.type == 'NAME', x2_1.type\n    assert x2_1.value == 'x2', x2_1.value\n    assert x2_2.type == 'NAME', x2_2.type\n    assert x2_2.value == 'x2', x2_2.value\n    assert x3_1.type == 'NAME', x3_1.type\n    assert x3_1.value == 'x3', x3_1.value\n    assert x3_2.type == 'NAME', x3_2.type\n    assert x3_2.value == 'x3', x3_2.value\n    assert colon.type == 'COLON', colon.type\n    assert colon.value == ':', colon.value\n    assert lparen_1.type == 'LPAREN', lparen_1.type\n    assert lparen_1.value == '(', lparen_1.value\n    assert lparen_2.type == 'LPAREN', lparen_1.type\n    assert lparen_2.value == '(', lparen_2.value\n    assert rparen_1.type == 'RPAREN', rparen_1.type\n    assert rparen_1.value == ')', rparen_1.value\n    assert rparen_2.type == 'RPAREN', rparen_2.type\n    assert rparen_2.value == ')', rparen_2.value\n    assert implies.type == 'IMPLIES', implies.type\n    assert implies.value == '=>', implies.value\n    assert equiv.type == 'EQUIV', equiv.type\n    assert equiv.value == '<->', equiv.value\n    assert comma_1.type == 'COMMA', comma_1.type\n    assert comma_1.value == ',', comma_1.value\n    assert comma_2.type == 'COMMA', comma_2.type\n    assert comma_2.value == ',', comma_2.value\n    expr = r'\\E x, y:  x /\\ y'\n    tokens = tokenize(expr, lexer)\n    assert len(tokens) == 8, tokens\n    (exists, x_1, comma, y_1, colon,\n        x_2, and_, y_2) = tokens\n    assert exists.type == 'EXISTS', exists.type\n    assert exists.value == r'\\E', exists.value\n    assert x_1.type == 'NAME', x_1.type\n    assert x_1.value == 'x', x_1.value\n    assert x_2.type == 'NAME', x_2.type\n    assert x_2.value == 'x', x_2.value\n    assert y_1.type == 'NAME', y_1.type\n    assert y_1.value == 'y', y_1.value\n    assert y_2.type == 'NAME', y_2.type\n    assert y_2.value == 'y', y_2.value\n    assert and_.type == 'AND', and_.type\n    assert and_.value == '&', and_.value\n    assert colon.type == 'COLON', colon.type\n    assert colon.value == ':', colon.value\n    assert comma.type == 'COMMA', comma.type\n    assert comma.value == ',', comma.value\n\n\ndef _assert_names_operator(\n        op,\n        a,\n        b,\n        op_type,\n        op_value):\n    assert a.type == 'NAME', a.type\n    assert a.value == 'a', a.value\n    assert b.type == 'NAME', b.type\n    assert b.value == 'b', b.value\n    assert op.type == op_type, (\n        op.type, op_type)\n    assert op.value == op_value, (\n        op.value, op_value)\n\n\ndef tokenize(\n        string:\n            str,\n        lexer\n        ) -> list:\n    r\"\"\"Return tokens representing `string`.\n\n    ```tla\n    ASSUME\n        /\\ hasattr(lexer, 'lexer')\n        /\\ hasattr(lexer.lexer, 'input')\n        /\\ is_iterable(lexer.lexer)\n    ```\n    \"\"\"\n    lexer.lexer.input(string)\n    return list(lexer.lexer)\n\n\ndef test_parsing():\n    # nullary operators\n    parser = dd._parser.Parser()\n    expr = 'FALSE'\n    tree = parser.parse(expr)\n    _assert_false_node(tree)\n    expr = 'TRUE'\n    tree = parser.parse(expr)\n    _assert_true_node(tree)\n    expr = '@1'\n    tree = parser.parse(expr)\n    assert tree.type == 'num', tree.type\n    assert tree.value == '1', tree.value\n    expr = '@20'\n    tree = parser.parse(expr)\n    assert tree.type == 'num', tree.type\n    assert tree.value == '20', tree.value\n    expr = 'operator_name'\n    tree = parser.parse(expr)\n    assert tree.type == 'var', tree.type\n    assert (tree.value == 'operator_name'\n        ), tree.value\n    expr = \"operator_name'\"\n    tree = parser.parse(expr)\n    assert tree.type == 'var', tree.type\n    assert (tree.value == \"operator_name'\"\n        ), tree.value\n    expr = '_OPerATorN_amE'\n    tree = parser.parse(expr)\n    assert (tree.type == 'var'\n        ), tree.type\n    assert tree.value == '_OPerATorN_amE'\n    # unary operators\n    expr = '~ FALSE'\n    tree = parser.parse(expr)\n    assert tree.type == 'operator', tree.type\n    assert tree.operator == '!', tree.operator\n    assert (len(tree.operands) == 1\n        ), tree.operands\n    tree, = tree.operands\n    _assert_false_node(tree)\n    expr = '! TRUE'\n    tree = parser.parse(expr)\n    assert tree.type == 'operator', tree.type\n    assert tree.operator == '!', tree.operator\n    assert (len(tree.operands) == 1\n        ), tree.operands\n    tree, = tree.operands\n    _assert_true_node(tree)\n    expr = '@1'\n    tree = parser.parse(expr)\n    assert tree.type == 'num', tree.type\n    assert tree.value == '1', tree.value\n    expr = '@-5'\n    tree = parser.parse(expr)\n    assert tree.type == 'num', tree.type\n    assert tree.value == '-5', tree.value\n    # binary operators\n    binary_operators = {\n        '/\\\\':\n            '&',\n        r'\\/':\n            '|',\n        '=>':\n            '=>',\n        '<=>':\n            '<->',\n        '&&':\n            '&',\n        '||':\n            '|',\n        '->':\n            '=>',\n        '<->':\n            '<->',\n        '&':\n            '&',\n        '|':\n            '|',\n        '^':\n            '^',\n        '#':\n            '#',\n        '=':\n            '=',\n        }\n    pairs = binary_operators.items()\n    for operator, token_type in pairs:\n        _check_binary_operator(\n            operator, token_type,\n            parser)\n    expr = 'TRUE <=> TRUE'\n    tree = parser.parse(expr)\n    _assert_binary_operator_tree(tree, '<->')\n    true_1, true_2 = tree.operands\n    _assert_true_node(true_1)\n    _assert_true_node(true_2)\n    expr = '(TRUE)'\n    tree = parser.parse(expr)\n    _assert_true_node(tree)\n    expr = '(~ TRUE)'\n    tree = parser.parse(expr)\n    assert tree.type == 'operator', tree.type\n    assert tree.operator == '!', tree.operator\n    assert (len(tree.operands) == 1\n        ), tree.operands\n    true, = tree.operands\n    _assert_true_node(true)\n    expr = r'(TRUE /\\ FALSE)'\n    tree = parser.parse(expr)\n    _assert_binary_operator_tree(tree, '&')\n    true, false = tree.operands\n    _assert_false_true_nodes(false, true)\n    # ternary operators\n    expr = 'ite(TRUE, FALSE, TRUE)'\n    tree = parser.parse(expr)\n    assert tree.type == 'operator', tree.type\n    assert tree.operator == 'ite', tree.operator\n    assert (len(tree.operands) == 3\n        ), tree.operands\n    true_1, false, true_2 = tree.operands\n    _assert_true_node(true_1)\n    _assert_true_node(true_2)\n    _assert_false_node(false)\n    # quantification\n    expr = r'\\A x, y, z:  (x /\\ y) => z'\n    tree = parser.parse(expr)\n    assert tree.type == 'operator', tree.type\n    assert tree.operator == r'\\A', tree.operator\n    assert (len(tree.operands) == 2\n        ), tree.operands\n    names, predicate = tree.operands\n    assert len(names) == 3, names\n    x, y, z = names\n    assert x.type == 'var', x.type\n    assert x.value == 'x', x.value\n    assert y.type == 'var', y.type\n    assert y.value == 'y', y.value\n    assert z.type == 'var', z.type\n    assert z.value == 'z', z.value\n    _assert_binary_operator_tree(\n        predicate, '=>')\n    expr_1, expr_2 = predicate.operands\n    _assert_binary_operator_tree(\n        expr_1, '&')\n    x, y = expr_1.operands\n    assert x.type == 'var', x.type\n    assert x.value == 'x', x.value\n    assert y.type == 'var', y.type\n    assert y.value == 'y', y.value\n    assert expr_2.type == 'var', expr_2.type\n    assert expr_2.value == 'z', expr_2.value\n    names, predicate = tree.operands\n    expr = r'\\E u:  (u = x) \\/ (x # u)'\n    tree = parser.parse(expr)\n    assert tree.type == 'operator', tree.type\n    assert (tree.operator == r'\\E'\n        ), tree.operator\n    assert (len(tree.operands) == 2\n        ), tree.operands\n    names, predicate = tree.operands\n    assert len(names) == 1, names\n    name, = names\n    assert name.type == 'var', name.type\n    assert name.value == 'u', name.value\n    _assert_binary_operator_tree(\n        predicate, '|')\n    expr_1, expr_2 = predicate.operands\n    _assert_binary_operator_tree(expr_1, '=')\n    u, x = expr_1.operands\n    assert u.type == 'var', u.type\n    assert u.value == 'u', u.value\n    assert x.type == 'var', x.type\n    assert x.value == 'x', x.value\n    _assert_binary_operator_tree(expr_2, '#')\n    x, u = expr_2.operands\n    assert x.type == 'var', x.type\n    assert x.value == 'x', x.value\n    assert u.type == 'var', u.type\n    assert u.value == 'u', u.value\n\n\ndef _check_binary_operator(\n        operator:\n            str,\n        token_type:\n            str,\n        parser\n        ) -> None:\n    expr = f'FALSE {operator} TRUE'\n    _log.debug(expr)\n    tree = parser.parse(expr)\n    _assert_binary_operator_tree(\n        tree, token_type)\n    false, true = tree.operands\n    _assert_false_true_nodes(false, true)\n\n\ndef _assert_binary_operator_tree(\n        tree,\n        operator:\n            str\n        ) -> None:\n    assert tree.type == 'operator', tree.type\n    assert tree.operator == operator, (\n        tree.operator, operator)\n    assert (len(tree.operands) == 2\n        ), tree.operands\n\n\ndef _assert_false_true_nodes(\n        false,\n        true):\n    _assert_false_node(false)\n    _assert_true_node(true)\n\n\ndef _assert_false_node(\n        false):\n    assert false.type == 'bool', false.type\n    assert false.value == 'FALSE', false.value\n\n\ndef _assert_true_node(\n        true):\n    assert true.type == 'bool', true.type\n    assert true.value == 'TRUE', true.value\n\n\ndef test_add_expr():\n    bdd = BDD()\n    for expr in PARSER_TEST_EXPRESSIONS:\n        _log.debug(expr)\n        dd._parser.add_expr(expr, bdd)\n\n\nclass BDD:\n    \"\"\"Scaffold for testing.\"\"\"\n\n    def __init__(\n            self):\n        self.false = 1\n        self.true = 1\n\n    def _add_int(\n            self,\n            number):\n        _log.debug(f'{number = }')\n        return 1\n\n    def var(\n            self,\n            name):\n        _log.debug(f'{name =}')\n        return 1\n\n    def apply(\n            self,\n            operator,\n            *operands):\n        _log.debug(f'''\n            {operator = }\n            {operands = }\n            ''')\n        return 1\n\n    def quantify(\n            self,\n            u,\n            qvars,\n            forall=None):\n        _log.debug(f'''\n            {u = }\n            {qvars = }\n            {forall = }\n            ''')\n        return 1\n\n    def rename(\n            self,\n            u,\n            renaming):\n        _log.debug(f'''\n            {u = }\n            {renaming = }\n            ''')\n        return 1\n\n\ndef test_recursive_traversal_vs_recursion_limit():\n    bdd = BDD()\n    parser = dd._parser.Parser()\n    # expression < recursion limit\n    expr = r'a /\\ b \\/ c'\n    tree = parser.parse(expr)\n    _flattener._recurse_syntax_tree(tree, bdd)\n    # expression > recursion limit\n    expr = make_expr_gt_recursion_limit()\n    tree = parser.parse(expr)\n    with pytest.raises(RecursionError):\n        _flattener._recurse_syntax_tree(tree, bdd)\n\n\ndef test_iterative_traversal_vs_recursion_limit():\n    bdd = BDD()\n    parser = dd._parser.Parser()\n    # expression < recursion limit\n    expr = r'a \\/ ~ b /\\ c'\n    tree = parser.parse(expr)\n    _flattener._reduce_syntax_tree(tree, bdd)\n    # expression > recursion limit\n    expr = make_expr_gt_recursion_limit()\n    tree = parser.parse(expr)\n    _flattener._reduce_syntax_tree(tree, bdd)\n\n\nif __name__ == '__main__':\n    test_all_parsers_same_results()\n"
  },
  {
    "path": "tests/pytest.ini",
    "content": "# configuration file for package `pytest`\n[pytest]\nfilterwarnings = error\npython_files = *_test.py\npython_classes = *Tests\npython_functions = test_*\n"
  },
  {
    "path": "tests/regressions_test.py",
    "content": "import dd.cudd as _cudd\n\n\ndef test_reordering_setting_restore():\n    # Original report at https://github.com/tulip-control/dd/issues/40\n    b = _cudd.BDD()\n    b.configure(reordering=False)\n    b.add_var('x')\n    b.add_var('y')\n    # x /\\ y\n    s = r'~ x /\\ y'\n    u = b.add_expr(s)\n    assert not b.configure()['reordering']\n    g = b.pick_iter(u)\n    m = list(g)\n    m_ = [dict(x=False, y=True)]\n    assert m == m_, (m, m_)\n    assert not b.configure()['reordering']\n\n\nif __name__ == '__main__':\n    test_reordering_setting_restore()\n"
  },
  {
    "path": "tests/sylvan_test.py",
    "content": "\"\"\"Tests of the module `dd.sylvan`.\"\"\"\nimport logging\nimport dd.sylvan as _sylvan\n\n\nlogging.getLogger('astutils').setLevel('ERROR')\n\n\ndef test_len():\n    b = _sylvan.BDD()\n    # constant\n    assert len(b) == 0, len(b)\n    u = b.false\n    assert len(b) == 0, len(b)\n    del u\n    assert len(b) == 0, len(b)\n    # var node\n    b.add_var('x')\n    u = b.var('x')\n    assert len(b) == 1, len(b)\n    del u\n    assert len(b) == 0, len(b)\n\n\ndef test_true_false():\n    b = _sylvan.BDD()\n    false = b.false\n    true = b.true\n    assert false != true\n    assert false == ~ true\n    assert false == false & true\n    assert true == true | false\n    del true, false\n\n\ndef test_add_var():\n    bdd = _sylvan.BDD()\n    bdd.add_var('x')\n    bdd.add_var('y')\n    jx = bdd._index_of_var['x']\n    jy = bdd._index_of_var['y']\n    assert jx == 0, jx\n    assert jy == 1, jy\n    x = bdd._var_with_index[0]\n    y = bdd._var_with_index[1]\n    assert x == 'x', x\n    assert y == 'y', y\n    assert bdd.vars == {'x', 'y'}, bdd.vars\n    x = bdd.var('x')\n    y = bdd.var('y')\n    assert x != y, (x, y)\n    del x, y\n\n\ndef test_insert_var():\n    bdd = _sylvan.BDD()\n    level = 0\n    j = bdd.add_var('x', index=level)\n    assert j == 0, j  # initially indices = levels\n    x = bdd.var_at_level(level)\n    assert x == 'x', x\n    level = 101\n    bdd.add_var('y', index=level)\n    y = bdd.var_at_level(level)\n    assert y == 'y', y\n\n\ndef test_add_expr():\n    bdd = _sylvan.BDD()\n    for var in ['x', 'y']:\n        bdd.add_var(var)\n    # ((0 \\/ 1) /\\ x) \\equiv x\n    s = r'(TRUE \\/ FALSE) /\\ x'\n    u = bdd.add_expr(s)\n    x = bdd.var('x')\n    assert u == x, (u, x)\n    # ((x \\/ ~ y) /\\ x) \\equiv x\n    s = r'(x \\/ ~ y) /\\ x'\n    u = bdd.add_expr(s)\n    assert u == x, (u, x)\n    # x /\\ y /\\ z\n    bdd.add_var('z')\n    z = bdd.var('z')\n    u = bdd.add_expr(r'x /\\ y /\\ z')\n    u_ = bdd.cube(dict(x=True, y=True, z=True))\n    assert u == u_, (u, u_)\n    # x /\\ ~ y /\\ z\n    u = bdd.add_expr(r'x /\\ ~ y /\\ z')\n    u_ = bdd.cube(dict(x=True, y=False, z=True))\n    assert u == u_, (u, u_)\n    # (\\E x:  x /\\ y) \\equiv y\n    y = bdd.var('y')\n    u = bdd.add_expr(r'\\E x:  x /\\ y')\n    assert u == y, (str(u), str(y))\n    # (\\A x:  x \\/ ~ x) \\equiv TRUE\n    u = bdd.add_expr(r'\\A x:  ~ x \\/ x')\n    assert u == bdd.true, u\n    del x, y, z, u, u_\n\n\ndef test_support():\n    bdd = _sylvan.BDD()\n    bdd.add_var('x')\n    bdd.add_var('y')\n    u = bdd.var('x')\n    supp = bdd.support(u)\n    assert supp == {'x'}, supp\n    u = bdd.var('y')\n    supp = bdd.support(u)\n    assert supp == {'y'}, supp\n    u = bdd.add_expr(r'x /\\ y')\n    supp = bdd.support(u)\n    assert supp == {'x', 'y'}, supp\n    del u\n\n\ndef test_compose():\n    bdd = _sylvan.BDD()\n    bdd.add_var('x')\n    bdd.add_var('y')\n    x = bdd.var('x')\n    y = bdd.var('y')\n    var_sub = dict(x=y)\n    y_ = bdd.let(var_sub, x)\n    assert y == y_, bdd.to_expr(y_)\n    del x, y, y_, var_sub\n\n\ndef test_cofactor():\n    bdd = _sylvan.BDD()\n    bdd.add_var('x')\n    x = bdd.var('x')\n    # u = bdd.let(dict(x=True), x)\n    # assert u == bdd.true, u\n    # u = bdd.let(dict(x=False), x)\n    # u = bdd.true\n    # u_ = bdd.false\n    # assert u == ~u_\n    assert x == bdd.add_expr('x')\n    u = bdd.let(dict(x=bdd.false), x)\n    u_ = bdd.false\n    assert u == u_, (u, u_)\n    del x, u, u_\n\n\ndef test_rename():\n    bdd = _sylvan.BDD()\n    # single variable\n    bdd.add_var('x')\n    bdd.add_var('y')\n    x = bdd.var('x')\n    y = bdd.var('y')\n    rename = dict(x='y')\n    y_ = bdd.let(rename, x)\n    assert y == y_, bdd.to_expr(y_)\n    # multiple variables\n    bdd.add_var('z')\n    bdd.add_var('w')\n    s = r'(x /\\ ~ y) \\/ w'\n    u = bdd.add_expr(s)\n    rename = dict(x='w', y='z', w='y')\n    v = bdd.let(rename, u)\n    s = r'(w /\\ ~ z) \\/ y'\n    v_ = bdd.add_expr(s)\n    assert v == v_, bdd.to_expr(v)\n    del x, y, y_, u, v, v_\n\n\ndef test_count():\n    bdd = _sylvan.BDD()\n    n = bdd.count(bdd.false)\n    assert n == 0, n\n    n = bdd.count(bdd.true)\n    assert n == 1, n\n    bdd.declare('x')\n    x = bdd.var('x')\n    n = bdd.count(x)\n    assert n == 1, n\n    bdd.declare('y')\n    u = bdd.add_expr('~ y')\n    n = bdd.count(u)\n    assert n == 1, n\n    u = bdd.add_expr(r'x /\\ y')\n    n = bdd.count(u)\n    assert n == 1, n\n    u = bdd.add_expr(r'~ x \\/ y')\n    n = bdd.count(u)\n    assert n == 3, n\n    bdd.declare('z')\n    u = bdd.add_expr(r'x /\\ (~y \\/ z)')\n    n = bdd.count(u)\n    assert n == 3, n\n    u = bdd.add_expr(r'x \\/ y \\/ ~z')\n    n = bdd.count(u)\n    assert n == 7, n\n\n\n# The function `test_pick_iter` is copied\n# from `common.Tests.test_pick_iter`.\ndef test_pick_iter():\n    b = _sylvan.BDD()\n    b.add_var('x')\n    b.add_var('y')\n    # FALSE\n    u = b.false\n    m = list(b.pick_iter(u))\n    assert not m, m\n    # TRUE, no care vars\n    u = b.true\n    m = list(b.pick_iter(u))\n    assert m == [{}], m\n    # x\n    u = b.add_expr('x')\n    m = list(b.pick_iter(u))\n    m_ = [dict(x=True)]\n    assert m == m_, (m, m_)\n    # ~ x /\\ y\n    s = r'~ x /\\ y'\n    u = b.add_expr(s)\n    g = b.pick_iter(u, care_vars=set())\n    m = list(g)\n    m_ = [dict(x=False, y=True)]\n    assert m == m_, (m, m_)\n    u = b.add_expr(s)\n    g = b.pick_iter(u)\n    m = list(g)\n    assert m == m_, (m, m_)\n    # x /\\ y\n    u = b.add_expr(r'x /\\ y')\n    m = list(b.pick_iter(u))\n    m_ = [dict(x=True, y=True)]\n    assert m == m_, m\n    # x\n    s = '~ y'\n    u = b.add_expr(s)\n    # partial\n    g = b.pick_iter(u)\n    m = list(g)\n    m_ = [dict(y=False)]\n    equal_list_contents(m, m_)\n    # partial\n    g = b.pick_iter(u, care_vars=['x', 'y'])\n    m = list(g)\n    m_ = [\n        dict(x=True, y=False),\n        dict(x=False, y=False)]\n    equal_list_contents(m, m_)\n    # care bits x, y\n    b.add_var('z')\n    s = r'x \\/ y'\n    u = b.add_expr(s)\n    g = b.pick_iter(u, care_vars=['x', 'y'])\n    m = list(g)\n    m_ = [\n        dict(x=True, y=False),\n        dict(x=False, y=True),\n        dict(x=True, y=True)]\n    equal_list_contents(m, m_)\n\n\n# The function `equal_list_contents` is copied\n# from `common.Tests.equal_list_contents`.\ndef equal_list_contents(x, y):\n    for u in x:\n        assert u in y, (u, x, y)\n    for u in y:\n        assert u in x, (u, x, y)\n\n\ndef test_py_operators():\n    bdd = _sylvan.BDD()\n    bdd.declare('x', 'y')\n    x = bdd.var('x')\n    y = bdd.var('y')\n    u = ~ x\n    u_ = bdd.add_expr('~ x')\n    assert u == u_, (u, u_)\n    u = x & y\n    u_ = bdd.add_expr(r'x /\\ y')\n    assert u == u_, (u, u_)\n    u = x | y\n    u_ = bdd.add_expr(r'x \\/ y')\n    assert u == u_, (u, u_)\n    u = x ^ y\n    u_ = bdd.add_expr('x # y')\n    assert u == u_, (u, u_)\n\n\nif __name__ == '__main__':\n    test_pick_iter()\n"
  }
]