Skip to content

Commit

Permalink
ENH: support roots as dict in JSON interface
Browse files Browse the repository at this point in the history
Dump roots of BDD to JSON from a `dict`,
in `dd._copy.dump_json()`.
Load roots of BDD from JSON into a `dict`,
in `dd._copy.load_json()`.

This change is motivated by readability,
and that reference by name is more accessible
to humans than reference by index.

The previous interface used lists of root
nodes of BDDs. Therefore, each root was
identified by its index in a `list`.
This way of referring to nodes is equivalent
to having a dictionary that maps integers to
root nodes. This view enables comparing to
the new interface (implemented by this commit).

String names as keys are more readable by
humans, and memorable, than indices.
This is especially so for data persistence,
because the intention for storing a BDD to
a (JSON) file may be archiving.

So the BDD may be accessed again years later,
and possibly by others. Having a string as
name for each root of the BDD, instead of
an integer, can communicate more information
about what the BDD and its roots mean.

Using strings, instead of positions (i.e.,
indices) also could help catch bugs when
reloading the BDDs. Those bugs would be
effectively implicit when lists are used,
and pass without getting noticed.
Explicit is better than implicit.
Errors should never pass silently.

In any case, the previous interface of
`roots` as `list` is still supported,
for any cases when a `list` would suffice,
and for backward compatibility.

- REF: move typing information from
  the docstring of the function
  `dd._copy._load_json()` to its signature,
  as type hint.
  • Loading branch information
johnyf committed Nov 29, 2023
1 parent 8da18f7 commit 0c3ac89
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 9 deletions.
4 changes: 4 additions & 0 deletions dd/_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
# Copyright 2017 by California Institute of Technology
# All rights reserved. Licensed under BSD-3.
#
import typing as _ty


Yes: _ty.TypeAlias = bool


class BDD:
Expand Down
77 changes: 68 additions & 9 deletions dd/_copy.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,34 @@
import os
import shelve
import shutil
import typing as _ty

import dd._abc
import dd._utils as _utils


SHELVE_DIR = '__shelve__'
_Yes: _ty.TypeAlias = dd._abc.Yes


class _Ref(_ty.Protocol):
var: str
level: int
low: '_Ref'
high: '_Ref'
bdd: '_BDD'
negated: _Yes
ref: int

def __int__(
self
) -> int:
...

def __invert__(
self
) -> '_Ref':
...


def copy_vars(source, target):
Expand Down Expand Up @@ -140,6 +165,12 @@ def dump_json(nodes, file_name):
Also dumps the variable names and the
variable order, to the same JSON file.
@param nodes: `dict` that maps names to
roots of the BDDs that will be written
to the JSON file
@type nodes: `dict` with `str` keys and
`Function` values
"""
tmp_fname = os.path.join(
SHELVE_DIR, 'temporary_shelf')
Expand All @@ -162,19 +193,24 @@ def _dump_json(nodes, fd, cache):
"""
fd.write('{')
_dump_bdd_info(nodes, fd)
for u in nodes:
for u in _utils._values_of(nodes):
_dump_bdd(u, fd, cache)
fd.write('\n}\n')


def _dump_bdd_info(nodes, fd):
"""Dump variable levels and roots."""
u = next(iter(nodes))
"""Dump variable levels and roots.
@param nodes: `dict` that maps
names to roots of BDDs
@type nodes: `dict` with `str` keys
"""
roots = _utils._map_container(_node_to_int, nodes)
u = next(iter(_utils._values_of(nodes)))
bdd = u.bdd
var_level = {
var: bdd.level_of_var(var)
for var in bdd.vars}
roots = [_node_to_int(u) for u in nodes]
info = (
'\n"level_of_var": {level}'
',\n"roots": {roots}').format(
Expand Down Expand Up @@ -208,12 +244,22 @@ def _dump_bdd(u, fd, cache):
return -k if u.negated else k


def load_json(file_name, bdd, load_order=False):
def load_json(
file_name,
bdd,
load_order=False
) -> (
dict[str, _Ref] |
list[_Ref]):
"""Add BDDs from JSON `file_name` to `bdd`.
@param load_order: if `True`,
@param load_order:
if `True`,
then load variable order
from `file_name`.
@return:
- keys (or indices) are names
- values are BDD roots
"""
tmp_fname = os.path.join(
SHELVE_DIR, 'temporary_shelf')
Expand All @@ -228,7 +274,13 @@ def load_json(file_name, bdd, load_order=False):
return nodes


def _load_json(fd, bdd, load_order, cache):
def _load_json(
fd,
bdd,
load_order,
cache) -> (
dict[str, _Ref] |
list[_Ref]):
"""Load BDDs from JSON file `fd` to manager `bdd`."""
context = dict(load_order=load_order)
# if the variable order is going to be loaded,
Expand All @@ -243,8 +295,15 @@ def _load_json(fd, bdd, load_order, cache):
for line in fd:
d = _parse_line(line)
_store_line(d, bdd, context, cache)
roots = [_node_from_int(k, bdd, cache)
for k in context['roots']]
roots = context['roots']
if hasattr(roots, 'items'):
roots = {
name: _node_from_int(k, bdd, cache)
for name, k in roots.items()}
else:
roots = [
_node_from_int(k, bdd, cache)
for k in roots]
# rm refs to cached nodes
for uid in cache:
u = _node_from_int(int(uid), bdd, cache)
Expand Down
68 changes: 68 additions & 0 deletions dd/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# Copyright 2017-2018 by California Institute of Technology
# All rights reserved. Licensed under 3-clause BSD.
#
import collections.abc as _abc
import textwrap as _tw
import typing as _ty

Expand Down Expand Up @@ -110,3 +111,70 @@ def _raise_runtimerror_about_ref_count(
f'value of attribute `_ref` is:\n{ref_count_lb}\n'
'For more information read the docstring of '
f'the class {class_name}.')



@_ty.overload
def _map_container(
mapper:
_abc.Callable,
container:
_abc.Mapping
) -> dict:
...


@_ty.overload
def _map_container(
mapper:
_abc.Callable,
container:
_abc.Iterable
) -> list:
...


def _map_container(
mapper,
container):
"""Map `container`, using `mapper()`.
If `container` is a sequence,
then map each item.
If `container` is a mapping of
keys to values, then map each value.
"""
if isinstance(container, _abc.Mapping):
return _map_values(mapper, container)
return list(map(mapper, container))


def _map_values(
mapper:
_abc.Callable,
kv:
_abc.Mapping
) -> dict:
"""Map each value of `kv` using `mapper()`.
The keys of `kv` remain unchanged.
"""
return {k: mapper(v) for k, v in kv.items()}


def _values_of(
container:
_abc.Mapping |
_abc.Collection
) -> _abc.Iterable:
"""Return container values.
@return:
- `container.values()` if
`container` is a mapping
- `container` otherwise
"""
if isinstance(container, _abc.Mapping):
return container.values()
return container

0 comments on commit 0c3ac89

Please sign in to comment.