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.
@@ -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
+ ]