pydp-engine 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pydp_engine-0.1.0/PKG-INFO +266 -0
- pydp_engine-0.1.0/README.md +245 -0
- pydp_engine-0.1.0/pydp/__init__.py +76 -0
- pydp_engine-0.1.0/pydp/analyze.py +185 -0
- pydp_engine-0.1.0/pydp/engine.py +360 -0
- pydp_engine-0.1.0/pydp/explain.py +55 -0
- pydp_engine-0.1.0/pydp/graph.py +152 -0
- pydp_engine-0.1.0/pydp/optimize.py +176 -0
- pydp_engine-0.1.0/pydp/state.py +40 -0
- pydp_engine-0.1.0/pydp/stats.py +53 -0
- pydp_engine-0.1.0/pydp/storage.py +66 -0
- pydp_engine-0.1.0/pydp/templates.py +322 -0
- pydp_engine-0.1.0/pydp/visualize.py +103 -0
- pydp_engine-0.1.0/pydp_engine.egg-info/PKG-INFO +266 -0
- pydp_engine-0.1.0/pydp_engine.egg-info/SOURCES.txt +25 -0
- pydp_engine-0.1.0/pydp_engine.egg-info/dependency_links.txt +1 -0
- pydp_engine-0.1.0/pydp_engine.egg-info/requires.txt +7 -0
- pydp_engine-0.1.0/pydp_engine.egg-info/top_level.txt +1 -0
- pydp_engine-0.1.0/pyproject.toml +34 -0
- pydp_engine-0.1.0/setup.cfg +4 -0
- pydp_engine-0.1.0/tests/test_analysis.py +84 -0
- pydp_engine-0.1.0/tests/test_ast_analysis.py +73 -0
- pydp_engine-0.1.0/tests/test_core.py +82 -0
- pydp_engine-0.1.0/tests/test_domains.py +79 -0
- pydp_engine-0.1.0/tests/test_parallel.py +75 -0
- pydp_engine-0.1.0/tests/test_strategies.py +66 -0
- pydp_engine-0.1.0/tests/test_templates.py +56 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pydp-engine
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: An intelligent, declarative Dynamic Programming engine for Python.
|
|
5
|
+
Author: PyDP
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/pydp/pydp
|
|
8
|
+
Keywords: dynamic-programming,memoization,algorithms,dp,optimization
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Topic :: Scientific/Engineering
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Education
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
Provides-Extra: viz
|
|
17
|
+
Requires-Dist: graphviz>=0.20; extra == "viz"
|
|
18
|
+
Requires-Dist: matplotlib>=3.5; extra == "viz"
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
21
|
+
|
|
22
|
+
# PyDP — an intelligent, declarative Dynamic Programming engine
|
|
23
|
+
|
|
24
|
+
PyDP lets you write Dynamic Programming solutions by declaring **only the
|
|
25
|
+
recurrence relation**. The engine handles memoization, dependency analysis,
|
|
26
|
+
execution order, memory-optimization analysis, statistics, and visualization
|
|
27
|
+
automatically.
|
|
28
|
+
|
|
29
|
+
> Describe the recurrence. Let the engine handle everything else.
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
from pydp import dp
|
|
33
|
+
|
|
34
|
+
@dp
|
|
35
|
+
def fib(solve, n):
|
|
36
|
+
if n < 2:
|
|
37
|
+
return n
|
|
38
|
+
return solve(n - 1) + solve(n - 2)
|
|
39
|
+
|
|
40
|
+
fib(30) # 832040 — memoized automatically
|
|
41
|
+
print(fib.stats.summary())
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install -e . # from this repo
|
|
48
|
+
# optional graph/plot rendering:
|
|
49
|
+
pip install -e ".[viz]"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Requires Python 3.9+. Zero required dependencies.
|
|
53
|
+
|
|
54
|
+
## The core idea
|
|
55
|
+
|
|
56
|
+
A recurrence is a function whose **first parameter is a `solve` handle** used to
|
|
57
|
+
request sub-states. You never write a cache, allocate a table, choose an
|
|
58
|
+
iteration order, or add base-case bookkeeping — you express the math, and PyDP
|
|
59
|
+
builds the machinery.
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
@dp
|
|
63
|
+
def grid_paths(solve, r, c):
|
|
64
|
+
if r == 0 or c == 0:
|
|
65
|
+
return 1
|
|
66
|
+
return solve(r - 1, c) + solve(r, c - 1)
|
|
67
|
+
|
|
68
|
+
grid_paths(10, 10) # 184756
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Multi-argument states, string arguments, and (frozen) list arguments all work —
|
|
72
|
+
state keys are normalized to hashable form automatically.
|
|
73
|
+
|
|
74
|
+
## What you get for free after any run
|
|
75
|
+
|
|
76
|
+
Every `DPProblem` exposes artifacts from its most recent run:
|
|
77
|
+
|
|
78
|
+
| Attribute | What it holds |
|
|
79
|
+
| ---------------- | --------------------------------------------------------- |
|
|
80
|
+
| `.stats` | `PerformanceStats` — states, cache hits/misses, depth, time |
|
|
81
|
+
| `.graph` | `DependencyGraph` — every `state -> dependency` edge |
|
|
82
|
+
| `.table` | the realized memo table `{state: value}` |
|
|
83
|
+
| `.tree` | the full recursion tree (parent → children) |
|
|
84
|
+
| `.storage()` | recommended storage backing (array / sparse / dict) |
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
fib(20)
|
|
88
|
+
fib.stats.states_explored # 21
|
|
89
|
+
fib.stats.hit_rate # fraction of lookups served from cache
|
|
90
|
+
fib.graph.dependencies_of(6) # {5, 4}
|
|
91
|
+
fib.graph.topological_order() # dependencies-first evaluation order
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Execution strategies
|
|
95
|
+
|
|
96
|
+
The same recurrence runs top-down (default), bottom-up (iterative), or parallel
|
|
97
|
+
with no code change.
|
|
98
|
+
|
|
99
|
+
| Strategy | How it evaluates |
|
|
100
|
+
| -------------- | ----------------------------------------------------------- |
|
|
101
|
+
| `"top-down"` | demand-driven recursion + memoization (default) |
|
|
102
|
+
| `"bottom-up"` | discovers the DAG, topologically orders it, iterates (depth 1) |
|
|
103
|
+
| `"parallel"` | evaluates topological *layers* concurrently in a thread pool |
|
|
104
|
+
| `"auto"` | currently resolves to top-down |
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
fib(100, strategy="bottom-up") # 354224848179261915075
|
|
108
|
+
fib.stats.max_recursion_depth # 1
|
|
109
|
+
|
|
110
|
+
fib(100, strategy="parallel", workers=8) # same answer, layered concurrency
|
|
111
|
+
|
|
112
|
+
@dp(strategy="bottom-up")
|
|
113
|
+
def knap(solve, i, cap): ...
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Deep recurrences that would overflow the C stack run inside an enlarged-stack
|
|
117
|
+
worker thread, so `strategy="top-down"` also handles chains thousands deep.
|
|
118
|
+
Genuinely cyclic recurrences raise `CyclicDependencyError` instead of hanging.
|
|
119
|
+
|
|
120
|
+
### Parallel execution
|
|
121
|
+
|
|
122
|
+
`strategy="parallel"` groups states into dependency **layers** via
|
|
123
|
+
`graph.topological_layers()` — every state in a layer depends only on earlier
|
|
124
|
+
layers, so a whole layer is evaluated concurrently. Workers get a read-only view
|
|
125
|
+
of already-computed values, so results are deterministic regardless of thread
|
|
126
|
+
scheduling.
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
fib.graph.topological_layers() # [[0, 1], [2], [3], ...]
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Note: because the recurrence body is Python, the GIL bounds CPU-bound speedup;
|
|
133
|
+
the win is real for GIL-releasing work (I/O, NumPy), and the layering is exactly
|
|
134
|
+
what a process-based backend would consume.
|
|
135
|
+
|
|
136
|
+
## Static AST analysis
|
|
137
|
+
|
|
138
|
+
`analyze()` reads the recurrence's **source** (before it runs) and infers its
|
|
139
|
+
structure — solve handle, state parameters, base cases, and recursive calls:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from pydp import analyze
|
|
143
|
+
|
|
144
|
+
fib = ... # a @dp problem
|
|
145
|
+
print(analyze(fib).summary())
|
|
146
|
+
# Solve handle : solve
|
|
147
|
+
# State parameters: n (1-D)
|
|
148
|
+
# Base cases : 1 -> if n < 2: return n @line 4
|
|
149
|
+
# Recursive calls : 2 -> solve(n - 1), solve(n - 2)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
It degrades gracefully (`source_available == False`) when source isn't
|
|
153
|
+
retrievable, e.g. lambdas or REPL-defined functions.
|
|
154
|
+
|
|
155
|
+
## Analysis & optimization
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
from pydp import analyze_memory, estimate_complexity, suggestions
|
|
159
|
+
|
|
160
|
+
fib(50)
|
|
161
|
+
analyze_memory(fib) # O(n)=51 -> O(2) via rolling array
|
|
162
|
+
estimate_complexity(fib) # time~O(n*k), space~O(2)
|
|
163
|
+
suggestions(fib) # human-readable optimization hints
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`analyze_memory` inspects the realized dependency graph: if every state only
|
|
167
|
+
reaches back `k` steps, it reports that memory can be reduced to `O(k)` with a
|
|
168
|
+
rolling array (and the analogous axis reduction for N-D grids).
|
|
169
|
+
|
|
170
|
+
## Visualization
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
from pydp import recursion_tree, dependency_text, heatmap_text, to_graphviz
|
|
174
|
+
|
|
175
|
+
fib(7)
|
|
176
|
+
print(recursion_tree(fib)) # ASCII recursion tree ("cached" nodes marked)
|
|
177
|
+
print(dependency_text(fib)) # text dependency listing
|
|
178
|
+
print(heatmap_text(fib)) # visited-state heatmap (1-D and 2-D)
|
|
179
|
+
|
|
180
|
+
to_graphviz(fib, "fib_graph") # PNG via the optional 'viz' extra
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Educational mode
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
from pydp import explain
|
|
187
|
+
fib(15)
|
|
188
|
+
print(explain(fib))
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Narrates the state space, dependency structure, a valid evaluation order, why
|
|
192
|
+
memoization helped, the complexity estimate, and optimization suggestions.
|
|
193
|
+
|
|
194
|
+
## Template library
|
|
195
|
+
|
|
196
|
+
Ready-made problems, each returning a full `DPProblem` (so you still get stats,
|
|
197
|
+
graphs, and analysis):
|
|
198
|
+
|
|
199
|
+
```python
|
|
200
|
+
from pydp import templates as T
|
|
201
|
+
|
|
202
|
+
T.fibonacci()(20)
|
|
203
|
+
T.coin_change([1, 2, 5])(11) # 3
|
|
204
|
+
T.rod_cutting([1,5,8,9,10,17,17,20])(8) # 22
|
|
205
|
+
T.solve_knapsack([1,3,4,5], [1,4,5,7], 7)[0] # 9
|
|
206
|
+
T.solve_lcs("AGGTAB", "GXTXAYB")[0] # 4
|
|
207
|
+
T.solve_edit_distance("sunday", "saturday")[0] # 3
|
|
208
|
+
T.solve_lis([10,9,2,5,3,7,101,18])[0] # 4
|
|
209
|
+
T.solve_matrix_chain([40,20,30,10,30])[0] # 26000
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### DP domains
|
|
213
|
+
|
|
214
|
+
Beyond the classic 1-D/2-D problems, the library ships one representative of
|
|
215
|
+
each major DP domain, so you can see how the engine handles each shape:
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
# Tree DP — max-weight independent set on a rooted tree
|
|
219
|
+
T.solve_tree_independent_set({0:[1,2,3], 1:[4], 2:[], 3:[], 4:[]},
|
|
220
|
+
{0:10, 1:5, 2:3, 3:4, 4:8})[0] # 18
|
|
221
|
+
|
|
222
|
+
# Interval DP — minimum palindrome-partition cuts
|
|
223
|
+
T.solve_palindrome_partition("aab")[0] # 1
|
|
224
|
+
|
|
225
|
+
# Bitmask DP — Travelling Salesman shortest tour
|
|
226
|
+
T.solve_tsp([[0,10,15,20],[10,0,35,25],[15,35,0,30],[20,25,30,0]])[0] # 80
|
|
227
|
+
|
|
228
|
+
# Digit DP — count integers in [0, N] avoiding a digit
|
|
229
|
+
T.solve_count_without_digit(20, forbidden_digit=3)[0] # 19
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
## Run the tour
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
python examples/demo.py
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
## Tests
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
pip install pytest
|
|
242
|
+
python -m pytest
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
61 tests cover the engine, all three execution strategies, cycle detection, the
|
|
246
|
+
AST and runtime analysis modules, and every template (including the tree /
|
|
247
|
+
interval / bitmask / digit-DP domains, the latter validated against brute force).
|
|
248
|
+
|
|
249
|
+
## Architecture
|
|
250
|
+
|
|
251
|
+
| Layer | Module |
|
|
252
|
+
| -------------------- | ----------------- |
|
|
253
|
+
| API / decorator | `engine.dp` |
|
|
254
|
+
| Core engine | `engine.DPProblem`|
|
|
255
|
+
| State normalization | `state` |
|
|
256
|
+
| Dependency analysis | `graph` |
|
|
257
|
+
| Static AST analysis | `analyze` |
|
|
258
|
+
| Storage analysis | `storage` |
|
|
259
|
+
| Optimizer | `optimize` |
|
|
260
|
+
| Visualization | `visualize` |
|
|
261
|
+
| Educational mode | `explain` |
|
|
262
|
+
| Template library | `templates` |
|
|
263
|
+
|
|
264
|
+
## License
|
|
265
|
+
|
|
266
|
+
MIT
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# PyDP — an intelligent, declarative Dynamic Programming engine
|
|
2
|
+
|
|
3
|
+
PyDP lets you write Dynamic Programming solutions by declaring **only the
|
|
4
|
+
recurrence relation**. The engine handles memoization, dependency analysis,
|
|
5
|
+
execution order, memory-optimization analysis, statistics, and visualization
|
|
6
|
+
automatically.
|
|
7
|
+
|
|
8
|
+
> Describe the recurrence. Let the engine handle everything else.
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
from pydp import dp
|
|
12
|
+
|
|
13
|
+
@dp
|
|
14
|
+
def fib(solve, n):
|
|
15
|
+
if n < 2:
|
|
16
|
+
return n
|
|
17
|
+
return solve(n - 1) + solve(n - 2)
|
|
18
|
+
|
|
19
|
+
fib(30) # 832040 — memoized automatically
|
|
20
|
+
print(fib.stats.summary())
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install -e . # from this repo
|
|
27
|
+
# optional graph/plot rendering:
|
|
28
|
+
pip install -e ".[viz]"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Requires Python 3.9+. Zero required dependencies.
|
|
32
|
+
|
|
33
|
+
## The core idea
|
|
34
|
+
|
|
35
|
+
A recurrence is a function whose **first parameter is a `solve` handle** used to
|
|
36
|
+
request sub-states. You never write a cache, allocate a table, choose an
|
|
37
|
+
iteration order, or add base-case bookkeeping — you express the math, and PyDP
|
|
38
|
+
builds the machinery.
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
@dp
|
|
42
|
+
def grid_paths(solve, r, c):
|
|
43
|
+
if r == 0 or c == 0:
|
|
44
|
+
return 1
|
|
45
|
+
return solve(r - 1, c) + solve(r, c - 1)
|
|
46
|
+
|
|
47
|
+
grid_paths(10, 10) # 184756
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Multi-argument states, string arguments, and (frozen) list arguments all work —
|
|
51
|
+
state keys are normalized to hashable form automatically.
|
|
52
|
+
|
|
53
|
+
## What you get for free after any run
|
|
54
|
+
|
|
55
|
+
Every `DPProblem` exposes artifacts from its most recent run:
|
|
56
|
+
|
|
57
|
+
| Attribute | What it holds |
|
|
58
|
+
| ---------------- | --------------------------------------------------------- |
|
|
59
|
+
| `.stats` | `PerformanceStats` — states, cache hits/misses, depth, time |
|
|
60
|
+
| `.graph` | `DependencyGraph` — every `state -> dependency` edge |
|
|
61
|
+
| `.table` | the realized memo table `{state: value}` |
|
|
62
|
+
| `.tree` | the full recursion tree (parent → children) |
|
|
63
|
+
| `.storage()` | recommended storage backing (array / sparse / dict) |
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
fib(20)
|
|
67
|
+
fib.stats.states_explored # 21
|
|
68
|
+
fib.stats.hit_rate # fraction of lookups served from cache
|
|
69
|
+
fib.graph.dependencies_of(6) # {5, 4}
|
|
70
|
+
fib.graph.topological_order() # dependencies-first evaluation order
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Execution strategies
|
|
74
|
+
|
|
75
|
+
The same recurrence runs top-down (default), bottom-up (iterative), or parallel
|
|
76
|
+
with no code change.
|
|
77
|
+
|
|
78
|
+
| Strategy | How it evaluates |
|
|
79
|
+
| -------------- | ----------------------------------------------------------- |
|
|
80
|
+
| `"top-down"` | demand-driven recursion + memoization (default) |
|
|
81
|
+
| `"bottom-up"` | discovers the DAG, topologically orders it, iterates (depth 1) |
|
|
82
|
+
| `"parallel"` | evaluates topological *layers* concurrently in a thread pool |
|
|
83
|
+
| `"auto"` | currently resolves to top-down |
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
fib(100, strategy="bottom-up") # 354224848179261915075
|
|
87
|
+
fib.stats.max_recursion_depth # 1
|
|
88
|
+
|
|
89
|
+
fib(100, strategy="parallel", workers=8) # same answer, layered concurrency
|
|
90
|
+
|
|
91
|
+
@dp(strategy="bottom-up")
|
|
92
|
+
def knap(solve, i, cap): ...
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Deep recurrences that would overflow the C stack run inside an enlarged-stack
|
|
96
|
+
worker thread, so `strategy="top-down"` also handles chains thousands deep.
|
|
97
|
+
Genuinely cyclic recurrences raise `CyclicDependencyError` instead of hanging.
|
|
98
|
+
|
|
99
|
+
### Parallel execution
|
|
100
|
+
|
|
101
|
+
`strategy="parallel"` groups states into dependency **layers** via
|
|
102
|
+
`graph.topological_layers()` — every state in a layer depends only on earlier
|
|
103
|
+
layers, so a whole layer is evaluated concurrently. Workers get a read-only view
|
|
104
|
+
of already-computed values, so results are deterministic regardless of thread
|
|
105
|
+
scheduling.
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
fib.graph.topological_layers() # [[0, 1], [2], [3], ...]
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Note: because the recurrence body is Python, the GIL bounds CPU-bound speedup;
|
|
112
|
+
the win is real for GIL-releasing work (I/O, NumPy), and the layering is exactly
|
|
113
|
+
what a process-based backend would consume.
|
|
114
|
+
|
|
115
|
+
## Static AST analysis
|
|
116
|
+
|
|
117
|
+
`analyze()` reads the recurrence's **source** (before it runs) and infers its
|
|
118
|
+
structure — solve handle, state parameters, base cases, and recursive calls:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
from pydp import analyze
|
|
122
|
+
|
|
123
|
+
fib = ... # a @dp problem
|
|
124
|
+
print(analyze(fib).summary())
|
|
125
|
+
# Solve handle : solve
|
|
126
|
+
# State parameters: n (1-D)
|
|
127
|
+
# Base cases : 1 -> if n < 2: return n @line 4
|
|
128
|
+
# Recursive calls : 2 -> solve(n - 1), solve(n - 2)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
It degrades gracefully (`source_available == False`) when source isn't
|
|
132
|
+
retrievable, e.g. lambdas or REPL-defined functions.
|
|
133
|
+
|
|
134
|
+
## Analysis & optimization
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from pydp import analyze_memory, estimate_complexity, suggestions
|
|
138
|
+
|
|
139
|
+
fib(50)
|
|
140
|
+
analyze_memory(fib) # O(n)=51 -> O(2) via rolling array
|
|
141
|
+
estimate_complexity(fib) # time~O(n*k), space~O(2)
|
|
142
|
+
suggestions(fib) # human-readable optimization hints
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
`analyze_memory` inspects the realized dependency graph: if every state only
|
|
146
|
+
reaches back `k` steps, it reports that memory can be reduced to `O(k)` with a
|
|
147
|
+
rolling array (and the analogous axis reduction for N-D grids).
|
|
148
|
+
|
|
149
|
+
## Visualization
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from pydp import recursion_tree, dependency_text, heatmap_text, to_graphviz
|
|
153
|
+
|
|
154
|
+
fib(7)
|
|
155
|
+
print(recursion_tree(fib)) # ASCII recursion tree ("cached" nodes marked)
|
|
156
|
+
print(dependency_text(fib)) # text dependency listing
|
|
157
|
+
print(heatmap_text(fib)) # visited-state heatmap (1-D and 2-D)
|
|
158
|
+
|
|
159
|
+
to_graphviz(fib, "fib_graph") # PNG via the optional 'viz' extra
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
## Educational mode
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
from pydp import explain
|
|
166
|
+
fib(15)
|
|
167
|
+
print(explain(fib))
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Narrates the state space, dependency structure, a valid evaluation order, why
|
|
171
|
+
memoization helped, the complexity estimate, and optimization suggestions.
|
|
172
|
+
|
|
173
|
+
## Template library
|
|
174
|
+
|
|
175
|
+
Ready-made problems, each returning a full `DPProblem` (so you still get stats,
|
|
176
|
+
graphs, and analysis):
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from pydp import templates as T
|
|
180
|
+
|
|
181
|
+
T.fibonacci()(20)
|
|
182
|
+
T.coin_change([1, 2, 5])(11) # 3
|
|
183
|
+
T.rod_cutting([1,5,8,9,10,17,17,20])(8) # 22
|
|
184
|
+
T.solve_knapsack([1,3,4,5], [1,4,5,7], 7)[0] # 9
|
|
185
|
+
T.solve_lcs("AGGTAB", "GXTXAYB")[0] # 4
|
|
186
|
+
T.solve_edit_distance("sunday", "saturday")[0] # 3
|
|
187
|
+
T.solve_lis([10,9,2,5,3,7,101,18])[0] # 4
|
|
188
|
+
T.solve_matrix_chain([40,20,30,10,30])[0] # 26000
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### DP domains
|
|
192
|
+
|
|
193
|
+
Beyond the classic 1-D/2-D problems, the library ships one representative of
|
|
194
|
+
each major DP domain, so you can see how the engine handles each shape:
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
# Tree DP — max-weight independent set on a rooted tree
|
|
198
|
+
T.solve_tree_independent_set({0:[1,2,3], 1:[4], 2:[], 3:[], 4:[]},
|
|
199
|
+
{0:10, 1:5, 2:3, 3:4, 4:8})[0] # 18
|
|
200
|
+
|
|
201
|
+
# Interval DP — minimum palindrome-partition cuts
|
|
202
|
+
T.solve_palindrome_partition("aab")[0] # 1
|
|
203
|
+
|
|
204
|
+
# Bitmask DP — Travelling Salesman shortest tour
|
|
205
|
+
T.solve_tsp([[0,10,15,20],[10,0,35,25],[15,35,0,30],[20,25,30,0]])[0] # 80
|
|
206
|
+
|
|
207
|
+
# Digit DP — count integers in [0, N] avoiding a digit
|
|
208
|
+
T.solve_count_without_digit(20, forbidden_digit=3)[0] # 19
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Run the tour
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
python examples/demo.py
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Tests
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
pip install pytest
|
|
221
|
+
python -m pytest
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
61 tests cover the engine, all three execution strategies, cycle detection, the
|
|
225
|
+
AST and runtime analysis modules, and every template (including the tree /
|
|
226
|
+
interval / bitmask / digit-DP domains, the latter validated against brute force).
|
|
227
|
+
|
|
228
|
+
## Architecture
|
|
229
|
+
|
|
230
|
+
| Layer | Module |
|
|
231
|
+
| -------------------- | ----------------- |
|
|
232
|
+
| API / decorator | `engine.dp` |
|
|
233
|
+
| Core engine | `engine.DPProblem`|
|
|
234
|
+
| State normalization | `state` |
|
|
235
|
+
| Dependency analysis | `graph` |
|
|
236
|
+
| Static AST analysis | `analyze` |
|
|
237
|
+
| Storage analysis | `storage` |
|
|
238
|
+
| Optimizer | `optimize` |
|
|
239
|
+
| Visualization | `visualize` |
|
|
240
|
+
| Educational mode | `explain` |
|
|
241
|
+
| Template library | `templates` |
|
|
242
|
+
|
|
243
|
+
## License
|
|
244
|
+
|
|
245
|
+
MIT
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""PyDP -- an intelligent, declarative Dynamic Programming engine.
|
|
2
|
+
|
|
3
|
+
Describe the recurrence; the engine handles memoization, dependency analysis,
|
|
4
|
+
execution strategy, memory optimization, statistics, and visualization.
|
|
5
|
+
|
|
6
|
+
Quick start::
|
|
7
|
+
|
|
8
|
+
from pydp import dp
|
|
9
|
+
|
|
10
|
+
@dp
|
|
11
|
+
def fib(solve, n):
|
|
12
|
+
if n < 2:
|
|
13
|
+
return n
|
|
14
|
+
return solve(n - 1) + solve(n - 2)
|
|
15
|
+
|
|
16
|
+
print(fib(30)) # 832040
|
|
17
|
+
print(fib.stats.summary())
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from .analyze import (
|
|
23
|
+
BaseCase,
|
|
24
|
+
RecurrenceAnalysis,
|
|
25
|
+
RecursiveCall,
|
|
26
|
+
analyze,
|
|
27
|
+
analyze_source,
|
|
28
|
+
)
|
|
29
|
+
from .engine import CyclicDependencyError, DPProblem, dp
|
|
30
|
+
from .explain import explain
|
|
31
|
+
from .graph import DependencyGraph
|
|
32
|
+
from .optimize import (
|
|
33
|
+
ComplexityEstimate,
|
|
34
|
+
MemoryOptimization,
|
|
35
|
+
analyze_memory,
|
|
36
|
+
estimate_complexity,
|
|
37
|
+
suggestions,
|
|
38
|
+
)
|
|
39
|
+
from .stats import PerformanceStats
|
|
40
|
+
from .storage import StorageAnalysis, analyze_storage
|
|
41
|
+
from .visualize import (
|
|
42
|
+
dependency_text,
|
|
43
|
+
heatmap_text,
|
|
44
|
+
recursion_tree,
|
|
45
|
+
to_graphviz,
|
|
46
|
+
)
|
|
47
|
+
from . import templates
|
|
48
|
+
|
|
49
|
+
__version__ = "0.1.0"
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
"dp",
|
|
53
|
+
"DPProblem",
|
|
54
|
+
"CyclicDependencyError",
|
|
55
|
+
"DependencyGraph",
|
|
56
|
+
"PerformanceStats",
|
|
57
|
+
"StorageAnalysis",
|
|
58
|
+
"analyze_storage",
|
|
59
|
+
"MemoryOptimization",
|
|
60
|
+
"ComplexityEstimate",
|
|
61
|
+
"analyze_memory",
|
|
62
|
+
"estimate_complexity",
|
|
63
|
+
"suggestions",
|
|
64
|
+
"explain",
|
|
65
|
+
"analyze",
|
|
66
|
+
"analyze_source",
|
|
67
|
+
"RecurrenceAnalysis",
|
|
68
|
+
"RecursiveCall",
|
|
69
|
+
"BaseCase",
|
|
70
|
+
"recursion_tree",
|
|
71
|
+
"dependency_text",
|
|
72
|
+
"heatmap_text",
|
|
73
|
+
"to_graphviz",
|
|
74
|
+
"templates",
|
|
75
|
+
"__version__",
|
|
76
|
+
]
|