Skip to content

Commit 2d163df

Browse files
authored
Merge pull request #17 from QuMuLab/memoization
Memoization and other optimizations and fixes
2 parents b102a3c + c190322 commit 2d163df

File tree

16 files changed

+545
-353
lines changed

16 files changed

+545
-353
lines changed

.travis.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ python:
88
- "3.6"
99
- "3.7"
1010
- "3.8"
11+
- "3.9-dev"
1112
- "pypy3.5"
13+
- "pypy3"
1214

1315
install:
1416
- pip install tox-travis

CONTRIBUTING.md

Lines changed: 78 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,82 @@
11
# Contributing to python-nnf
22

3-
More information coming soon.
3+
## Running the tests and linters
44

5-
## Notes for now
5+
[`tox`](https://tox.readthedocs.io/en/latest/) is used to run the tests and linters. After installing it, run:
66

7-
- `tox -e py3`
8-
- Set notation for Internal nodes (rather than lists)
9-
- Use of `@memoize`
10-
- Use of Typing / mypy
7+
```
8+
tox
9+
```
10+
11+
This will install and run all the tooling.
12+
13+
`tox` aborts early if one of the steps fails. To run just the tests (for example if you can't get `mypy` to work), install and run [`pytest`](https://docs.pytest.org/en/latest/getting-started.html). To run just one particular test, run `pytest -k <name of test>`.
14+
15+
## Mypy
16+
17+
[Mypy](https://mypy.readthedocs.io/en/stable/) is used for static typing. This is also managed by `tox`.
18+
19+
You can look at the existing code for cues. If you can't figure it out, just leave it be and we'll look at it during code review.
20+
21+
## Hypothesis
22+
23+
[Hypothesis](https://hypothesis.readthedocs.io/en/latest/) is used for property-based testing: it generates random sentences for tests to use. See the existing tests for examples.
24+
25+
It's ideal for a feature to have both tests that do use Hypothesis and tests that don't.
26+
27+
## Memoization
28+
29+
[Memoization](https://en.wikipedia.org/wiki/Memoization) is used in various places to avoid computing the same thing multiple times. A memoized function remembers past calls so it can return the same return value again in the future.
30+
31+
A downside of memoization is that it increases memory usage.
32+
33+
It's used in two patterns throughout the codebase:
34+
35+
### Temporary internal memoization with `@memoize`
36+
37+
This is used for functions that run on individual nodes of sentences, within a method. For example:
38+
39+
```python
40+
def height(self) -> int:
41+
"""The number of edges between here and the furthest leaf."""
42+
@memoize
43+
def height(node: NNF) -> int:
44+
if isinstance(node, Internal) and node.children:
45+
return 1 + max(height(child) for child in node.children)
46+
return 0
47+
48+
return height(self)
49+
```
50+
51+
Because the function is defined inside the method, it's garbage collected along with its cache when the method returns. This makes sure we don't keep all the individual node heights in memory indefinitely.
52+
53+
### Memoizing sentence properties with `@weakref_memoize`
54+
55+
This is used for commonly used methods that run on whole sentences. For example:
56+
57+
```python
58+
@weakref_memoize
59+
def vars(self) -> t.FrozenSet[Name]:
60+
"""The names of all variables that appear in the sentence."""
61+
return frozenset(node.name
62+
for node in self.walk()
63+
if isinstance(node, Var))
64+
```
65+
66+
This lets us call `.vars()` often without worrying about performance.
67+
68+
Unlike the other decorator, this one uses `weakref`, so it doesn't interfere with garbage collection. It's slightly less efficient though, so the temporary functions from the previous section are better off with `@memoize`.
69+
70+
## Documentation
71+
72+
Methods are documented with reStructuredText inside docstrings. This looks a little like markdown, but it's different, so take care and look at other docstrings for examples.
73+
74+
Documentation is automatically generated and ends up at [Read the Docs](https://python-nnf.readthedocs.io/en/latest/).
75+
76+
To build the documentation locally, run `make html` inside the `docs/` directory. This generates a manual in `docs/_build/html/`.
77+
78+
New modules have to be added to `docs/nnf.rst` to be documented.
79+
80+
## Style/miscellaneous
81+
82+
- Prefer sets over lists where it make sense. For example, `Or({~c for c in children} | {aux})` instead of `Or([~c for c in children] + [aux])`.

docs/caveats.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,11 @@ Decomposability and determinism
4444

4545
A lot of methods are much faster to perform on sentences that are decomposable or deterministic, such as model enumeration.
4646

47-
Decomposability is automatically detected. However, you can skip the check if you already know whether the sentence is decomposable or not, by passing ``decomposable=True`` or ``decomposable=False`` as a keyword argument.
47+
Decomposability is automatically detected.
4848

49-
Determinism is too expensive to automatically detect, but it can give a huge speedup. If you know a sentence to be deterministic, pass ``deterministic=True`` as a keyword argument to take advantage.
49+
Determinism is too expensive to automatically detect, but it can give a huge speedup. If you know a sentence to be deterministic, run ``.mark_deterministic()`` to enable the relevant optimizations.
5050

51-
A compiler like `DSHARP <https://github.com/QuMuLab/dsharp>`_ may be able to convert some sentences into equivalent deterministic decomposable sentences. The output of DSHARP can be loaded using the :mod:`nnf.dsharp` module.
51+
A compiler like `DSHARP <https://github.com/QuMuLab/dsharp>`_ may be able to convert some sentences into equivalent deterministic decomposable sentences. The output of DSHARP can be loaded using the :mod:`nnf.dsharp` module. Sentences returned by :func:`nnf.dsharp.compile` are automatically marked as deterministic.
5252

5353
Other duplication inefficiencies
5454
--------------------------------

examples/socialchoice.py

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@
44
import functools
55
import itertools
66

7-
import nnf
7+
from nnf import amc, And, Or, Var, true, false
88

9-
from nnf import amc, And, Or, Var
10-
11-
memoize = functools.lru_cache(None) # huge speedup
9+
# Cache generated nodes
10+
memoize = functools.lru_cache(None)
1211

1312

1413
def powerset(iterable):
@@ -21,8 +20,6 @@ def powerset(iterable):
2120

2221
def lin(candidates):
2322
# candidates must be hashable, have total ordering
24-
builder = nnf.Builder()
25-
2623
candidates = frozenset(candidates)
2724
n = len(candidates)
2825
T = frozenset(powerset(candidates))
@@ -32,25 +29,24 @@ def lin(candidates):
3229
def defeats(i, j):
3330
assert i != j
3431
if i < j:
35-
return builder.Var((i, j))
32+
return Var((i, j))
3633
else:
37-
return builder.Var((j, i), False)
34+
return Var((j, i), False)
3835

3936
@memoize
4037
def C(S):
4138
if S == candidates:
42-
return builder.true
39+
return true
4340

44-
return builder.Or(C_child(i, S)
45-
for i in candidates - S)
41+
return Or(C_child(i, S) for i in candidates - S)
4642

4743
@memoize
4844
def C_child(i, S):
4945
children = {C(S | {i})}
5046
children.update(defeats(i, j)
5147
for j in candidates - S
5248
if i != j)
53-
return builder.And(children)
49+
return And(children)
5450

5551
return C(frozenset())
5652

@@ -81,17 +77,17 @@ def order_to_model(order):
8177
def kemeny(votes):
8278
labels = collections.Counter()
8379
for vote in votes:
84-
for name, true in order_to_model(vote).items():
85-
labels[nnf.Var(name, true)] += 1
80+
for name, truthiness in order_to_model(vote).items():
81+
labels[Var(name, truthiness)] += 1
8682
return {model_to_order(model)
8783
for model in amc.maxplus_reduce(lin(votes[0]), labels).models()}
8884

8985

9086
def slater(votes):
9187
totals = collections.Counter()
9288
for vote in votes:
93-
for name, true in order_to_model(vote).items():
94-
totals[nnf.Var(name, true)] += 1
89+
for name, truthiness in order_to_model(vote).items():
90+
totals[Var(name, truthiness)] += 1
9591
labels = {}
9692
for var in totals:
9793
labels[var] = 1 if totals[var] > totals[~var] else 0
@@ -143,7 +139,7 @@ def test():
143139
s_3 = s.condition({(0, 1): True, (1, 2): True, (0, 2): False})
144140
assert len(list(s_3.models())) == 0
145141
assert not s_3.satisfiable()
146-
assert s_3.simplify() == nnf.false
142+
assert s_3.simplify() == false
147143

148144
# strings as candidates
149145
named = lin({"Alice", "Bob", "Carol"})

mypy.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# -*- conf -*-
2+
[mypy]
3+
strict = True

0 commit comments

Comments
 (0)