executable-engineering 0.1.29__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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Michael J Welland
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,55 @@
1
+ Metadata-Version: 2.4
2
+ Name: executable_engineering
3
+ Version: 0.1.29
4
+ Summary: Tools for ENGPHYS 3NM4
5
+ Home-page: https://github.com/themintlab/ExecutableEngineering/tree/main/executable_engineering
6
+ Author: Michael Welland
7
+ License: MIT
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.9
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: numpy
15
+ Requires-Dist: matplotlib
16
+ Requires-Dist: networkx
17
+ Requires-Dist: ipywidgets
18
+ Requires-Dist: IPython
19
+ Requires-Dist: pyppeteer
20
+ Requires-Dist: nbconvert
21
+ Requires-Dist: scipy
22
+ Requires-Dist: plotly
23
+ Requires-Dist: pandas
24
+ Dynamic: author
25
+ Dynamic: classifier
26
+ Dynamic: description
27
+ Dynamic: description-content-type
28
+ Dynamic: home-page
29
+ Dynamic: license
30
+ Dynamic: license-file
31
+ Dynamic: requires-dist
32
+ Dynamic: requires-python
33
+ Dynamic: summary
34
+
35
+ # executable_engineering
36
+
37
+ `executable_engineering` is the interactive numerical methods support package used by this book.
38
+
39
+ ## Local development
40
+
41
+ From the repository root:
42
+
43
+ ```bash
44
+ python -m pip install -r requirements-book.txt
45
+ ```
46
+
47
+ That installs the in-repo package in editable mode so notebook changes use the local source.
48
+
49
+ ## Standalone package checks
50
+
51
+ ```bash
52
+ python -m pip install -e ./executable_engineering
53
+ ( cd /tmp && python -c "from executable_engineering import RootFinderOpen, RootFinderClosed" )
54
+ python -m build ./executable_engineering
55
+ ```
@@ -0,0 +1,21 @@
1
+ # executable_engineering
2
+
3
+ `executable_engineering` is the interactive numerical methods support package used by this book.
4
+
5
+ ## Local development
6
+
7
+ From the repository root:
8
+
9
+ ```bash
10
+ python -m pip install -r requirements-book.txt
11
+ ```
12
+
13
+ That installs the in-repo package in editable mode so notebook changes use the local source.
14
+
15
+ ## Standalone package checks
16
+
17
+ ```bash
18
+ python -m pip install -e ./executable_engineering
19
+ ( cd /tmp && python -c "from executable_engineering import RootFinderOpen, RootFinderClosed" )
20
+ python -m build ./executable_engineering
21
+ ```
@@ -0,0 +1,32 @@
1
+ from .neuralnet_module import init_weights, tanh, tanh_derivative, forward, compute_loss, backward, plot_nn_diagram, init_model, forward_pass, backward_pass, step, reset_model, save_function, change_depth, change_width, draw_network, update_plots
2
+ from .openrootfinder_module import RootFinderOpen
3
+ from .closedrootfinder_module import RootFinderClosed
4
+ from .closedoptimization_module import OptimizerClosed
5
+ from .openoptimization_module import OptimizerOpen
6
+ from .gradientoptimization_module import OptimizerGrad
7
+
8
+
9
+ __all__ = [
10
+ "init_weights",
11
+ "tanh",
12
+ "tanh_derivative",
13
+ "forward",
14
+ "compute_loss",
15
+ "backward",
16
+ "plot_nn_diagram",
17
+ "init_model",
18
+ "forward_pass",
19
+ "backward_pass",
20
+ "step",
21
+ "reset_model",
22
+ "save_function",
23
+ "change_depth",
24
+ "change_width",
25
+ "draw_network",
26
+ "update_plots",
27
+ "RootFinderOpen",
28
+ "RootFinderClosed",
29
+ "OptimizerClosed",
30
+ "OptimizerOpen",
31
+ "OptimizerGrad",
32
+ ]
@@ -0,0 +1,282 @@
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+ from matplotlib.animation import FuncAnimation
4
+ from IPython.display import HTML, display
5
+ import ipywidgets as widgets
6
+
7
+ class OptimizerClosed:
8
+ def __init__(self, f, a=None, b=None, tol=1e-6, max_iter=30):
9
+ self.f = f
10
+ self.a = a
11
+ self.b = b
12
+ self.tol = tol
13
+ self.max_iter = max_iter
14
+ self.gold_steps = self._run_golden_section()
15
+ self.brent_steps = self._run_brent()
16
+
17
+ def _run_golden_section(self):
18
+ if self.a is None or self.b is None:
19
+ return []
20
+
21
+ a, b = self.a, self.b
22
+ gr = (np.sqrt(5) - 1) / 2
23
+ c = b - gr * (b - a)
24
+ d = a + gr * (b - a)
25
+ steps = []
26
+
27
+ for _ in range(self.max_iter):
28
+ steps.append((a, b, c, d))
29
+
30
+ if abs(b - a) < self.tol:
31
+ break
32
+
33
+ if self.f(c) < self.f(d):
34
+ b = d
35
+ d = c
36
+ c = b - gr * (b - a)
37
+ else:
38
+ a = c
39
+ c = d
40
+ d = a + gr * (b - a)
41
+ return steps
42
+
43
+
44
+ def _run_brent(self):
45
+
46
+ if self.a is None or self.b is None:
47
+ return []
48
+
49
+ a, b = self.a, self.b
50
+ C = (3.0 - np.sqrt(5.0)) / 2.0
51
+
52
+ x = w = v = a + C * (b - a)
53
+ fx = fw = fv = self.f(x)
54
+
55
+ d = e = b - a
56
+ steps = []
57
+
58
+ for iteration in range(self.max_iter):
59
+ m = 0.5 * (a + b)
60
+ tol1 = self.tol * abs(x) + 1e-12
61
+ tol2 = 2.0 * tol1
62
+
63
+ if abs(x - m) <= tol2 - 0.5 * (b - a):
64
+ break
65
+
66
+ parabolic_accepted = False
67
+ method = "golden"
68
+ g = e
69
+
70
+ if abs(g) > tol1:
71
+ r = (x - w) * (fx - fv)
72
+ q = (x - v) * (fx - fw)
73
+ p = (x - v) * q - (x - w) * r
74
+ q = 2.0 * (q - r)
75
+
76
+ if q != 0:
77
+ if q > 0:
78
+ p = -p
79
+ q = abs(q)
80
+
81
+ p_over_q = p / q
82
+ u_candidate = x + p_over_q
83
+
84
+ cond1 = (a + tol2) <= u_candidate <= (b - tol2)
85
+ cond2 = abs(p_over_q) < abs(0.5 * g)
86
+
87
+ if cond1 and cond2:
88
+ u = u_candidate
89
+ method = "parabolic"
90
+ parabolic_accepted = True
91
+
92
+ if not parabolic_accepted:
93
+ if x < m:
94
+ e = b - x
95
+ else:
96
+ e = a - x
97
+ d = C * e
98
+ u = x + d
99
+ method = "golden"
100
+
101
+ if abs(u - x) < tol1:
102
+ u = x + np.sign(u - x) * tol1
103
+
104
+ fu = self.f(u)
105
+
106
+ steps.append((a, b, x, w, v, u, method))
107
+
108
+ if fu <= fx:
109
+ if u >= x:
110
+ a = x
111
+ else:
112
+ b = x
113
+ v, fv = w, fw
114
+ w, fw = x, fx
115
+ x, fx = u, fu
116
+ else:
117
+ if u < x:
118
+ a = u
119
+ else:
120
+ b = u
121
+ if (fu <= fw) or (w == x):
122
+ v, fv = w, fw
123
+ w, fw = u, fu
124
+ elif (fu <= fv) or (v == x) or (v == w):
125
+ v, fv = u, fu
126
+
127
+ e = d if parabolic_accepted else e
128
+
129
+ return steps
130
+
131
+
132
+ def _animate_golden(self, interval_ms=1000):
133
+ steps = self.gold_steps
134
+ if not steps:
135
+ return None
136
+
137
+ x_vals = np.linspace(self.a, self.b, 400)
138
+ y_vals = self.f(x_vals)
139
+
140
+ fig, ax = plt.subplots(figsize=(10, 6))
141
+ ax.plot(x_vals, y_vals, label='f(x)', color='blue')
142
+ ax.set_title("Golden Section Search", fontsize=14)
143
+ ax.set_xlabel("x")
144
+ ax.set_ylabel("f(x)")
145
+ ax.grid(True, linestyle=':')
146
+ ax.legend()
147
+
148
+ a_line = ax.axvline(0, color='red', linestyle=':', lw=1.5, label='x1')
149
+ b_line = ax.axvline(0, color='green', linestyle=':', lw=1.5, label='x2')
150
+
151
+ a_point, = ax.plot([], [], 'o', color='gold')
152
+ b_point, = ax.plot([], [], 'o', color='gold')
153
+ c_point, = ax.plot([], [], 'o', color='gold', label='x3')
154
+ d_point, = ax.plot([], [], 'o', color='orange', label='x4')
155
+ conn, = ax.plot([], [], '--', color='purple', alpha=0.5)
156
+
157
+ ax.legend()
158
+
159
+ def update(i):
160
+ a, b, c, d = steps[i]
161
+ y_a, y_b, y_c, y_d = self.f(a), self.f(b), self.f(c), self.f(d)
162
+
163
+ a_line.set_xdata([a])
164
+ b_line.set_xdata([b])
165
+
166
+ a_point.set_data([a], [y_a])
167
+ b_point.set_data([b], [y_b])
168
+ c_point.set_data([c], [y_c])
169
+ d_point.set_data([d], [y_d])
170
+ conn.set_data([a, c, d, b], [y_a, y_c, y_d, y_b])
171
+
172
+ ax.set_title(f"Golden Section Search - Step {i+1}\n"
173
+ f"Interval: [{a:.4f}, {b:.4f}]")
174
+ return a_line, b_line, a_point, b_point, c_point, d_point, conn
175
+
176
+ anim = FuncAnimation(fig, update, frames=len(steps),
177
+ interval=interval_ms, blit=True, repeat=False)
178
+ plt.close(fig)
179
+ return HTML(anim.to_jshtml())
180
+
181
+
182
+ def _animate_brent(self, interval_ms=1200):
183
+ steps = self.brent_steps
184
+ if not steps:
185
+ return None
186
+
187
+ x_vals = np.linspace(self.a, self.b, 800)
188
+ y_vals = self.f(x_vals)
189
+
190
+ fig, ax = plt.subplots(figsize=(10, 6))
191
+ ax.plot(x_vals, y_vals, label='f(x)', color='blue')
192
+ ax.set_title("Brent's Method (parabolic interpolation + golden fallback)", fontsize=13)
193
+ ax.set_xlabel("x")
194
+ ax.set_ylabel("f(x)")
195
+ ax.grid(True, linestyle=':')
196
+
197
+ a_line = ax.axvline(0, color='red', linestyle=':', lw=1.5, label='a')
198
+ b_line = ax.axvline(0, color='green', linestyle=':', lw=1.5, label='b')
199
+ x_dot, = ax.plot([], [], 'ro', label='x (best)')
200
+ w_dot, = ax.plot([], [], 'go', label='w')
201
+ v_dot, = ax.plot([], [], 'bo', label='v')
202
+ u_dot, = ax.plot([], [], marker='*', markersize=10, color='gold', label='u (candidate)')
203
+ parabola_line, = ax.plot([], [], '--', color='purple', alpha=0.8, label='parabola (if stable)')
204
+
205
+ annotation_texts = []
206
+ ax.legend()
207
+
208
+ def good_for_parabola(xs):
209
+ xs = np.asarray(xs, dtype=float)
210
+ min_spacing = np.min(np.abs(np.diff(np.sort(xs))))
211
+ domain_width = abs(self.b - self.a) if self.b != self.a else 1.0
212
+ return (min_spacing > 1e-10 * domain_width)
213
+
214
+ def update(i):
215
+ for t in annotation_texts:
216
+ t.remove()
217
+ annotation_texts.clear()
218
+
219
+ a, b, x, w, v, u, method = steps[i]
220
+ fa, fb, fx, fw, fv, fu = self.f(a), self.f(b), self.f(x), self.f(w), self.f(v), self.f(u)
221
+
222
+ a_line.set_xdata([a])
223
+ b_line.set_xdata([b])
224
+
225
+ x_dot.set_data([x], [fx])
226
+ w_dot.set_data([w], [fw])
227
+ v_dot.set_data([v], [fv])
228
+ u_dot.set_data([u], [fu])
229
+
230
+ xs_to_fit = np.array([x, w, v])
231
+ min_spacing = np.min(np.abs(np.diff(np.sort(xs_to_fit))))
232
+ domain_width = abs(self.b - self.a) if self.b != self.a else 1.0
233
+
234
+ xs_parabola = np.linspace(min(x, w, v) - 0.05*domain_width, max(x, w, v) + 0.05*domain_width, 300)
235
+ draw_parabola = False
236
+ if min_spacing > 1e-10 * domain_width:
237
+ try:
238
+ coeffs = np.polyfit([x, w, v], [fx, fw, fv], 2)
239
+ parabola = np.polyval(coeffs, xs_parabola)
240
+ if np.all(np.isfinite(parabola)):
241
+ parabola_line.set_data(xs_parabola, parabola)
242
+ draw_parabola = True
243
+ except Exception:
244
+ draw_parabola = False
245
+
246
+ if not draw_parabola:
247
+ parabola_line.set_data([], [])
248
+
249
+ labels = [
250
+ (a, fa, 'a'),
251
+ (b, fb, 'b'),
252
+ (x, fx, 'x'),
253
+ (w, fw, 'w'),
254
+ (v, fv, 'v'),
255
+ (u, fu, f'u ({method[:3]})')
256
+ ]
257
+
258
+ ax.set_title(f"Brent's Method - Step {i+1} (method: {method})\nInterval: [{a:.6f}, {b:.6f}]")
259
+ return (a_line, b_line, x_dot, w_dot, v_dot, u_dot, parabola_line, *annotation_texts)
260
+
261
+ anim = FuncAnimation(fig, update, frames=len(steps),
262
+ interval=interval_ms, blit=False, repeat=False)
263
+ plt.close(fig)
264
+ return HTML(anim.to_jshtml())
265
+
266
+ def show_toggle_closed(self, interval_ms=1000):
267
+ gold_anim_html = self._animate_golden(interval_ms)
268
+ brent_anim_html = self._animate_brent(interval_ms)
269
+
270
+ if gold_anim_html is None or brent_anim_html is None:
271
+ print("Cannot create animations. Check initial interval [a, b].")
272
+ return
273
+
274
+ gold_content = widgets.HTML(value=gold_anim_html.data)
275
+ brent_content = widgets.HTML(value=brent_anim_html.data)
276
+
277
+ tab_container = widgets.Tab()
278
+ tab_container.children = [gold_content, brent_content]
279
+ tab_container.set_title(0, 'Golden Section Search')
280
+ tab_container.set_title(1, "Brent's Method")
281
+
282
+ display(tab_container)
@@ -0,0 +1,155 @@
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+ from matplotlib.animation import FuncAnimation
4
+ from IPython.display import HTML, display
5
+ import ipywidgets as widgets
6
+
7
+ class RootFinderClosed:
8
+ def __init__(self, f, a=None, b=None, tol=1e-6, max_iter=20):
9
+ self.f = f
10
+ self.a = a
11
+ self.b = b
12
+ self.tol = tol
13
+ self.max_iter = max_iter
14
+ self.bisect_intervals, self.bisect_guesses = self._run_bisection()
15
+ self.falsepos_intervals, self.falsepos_guesses = self._run_false_position()
16
+
17
+ def _run_bisection(self):
18
+ if self.a is None or self.b is None:
19
+ return [], []
20
+ a, b = self.a, self.b
21
+ if self.f(a) * self.f(b) > 0:
22
+ return [], []
23
+
24
+ intervals = []
25
+ guesses = []
26
+ for _ in range(self.max_iter):
27
+ c = (a + b) / 2.0
28
+ intervals.append((a, b, c))
29
+ guesses.append(c)
30
+ if abs(self.f(c)) < self.tol or abs(b - a) < self.tol:
31
+ break
32
+ if self.f(a) * self.f(c) < 0:
33
+ b = c
34
+ else:
35
+ a = c
36
+ return intervals, guesses
37
+
38
+ def _run_false_position(self):
39
+ if self.a is None or self.b is None:
40
+ return [], []
41
+ a, b = self.a, self.b
42
+ if self.f(a) * self.f(b) > 0:
43
+ return [], []
44
+
45
+ intervals = []
46
+ guesses = []
47
+ for _ in range(self.max_iter):
48
+ fa, fb = self.f(a), self.f(b)
49
+ if abs(fb - fa) < 1e-12:
50
+ break
51
+ c = b - fb * (b - a) / (fb - fa)
52
+ intervals.append((a, b, c))
53
+ guesses.append(c)
54
+ if abs(self.f(c)) < self.tol:
55
+ break
56
+ if fa * self.f(c) < 0:
57
+ b = c
58
+ fb = self.f(b)
59
+ else:
60
+ a = c
61
+ fa = self.f(a)
62
+ return intervals, guesses
63
+
64
+ def _make_animation_closed(self, method='Bisection', interval_ms=750):
65
+ if method == 'Bisection':
66
+ intervals, guesses = self.bisect_intervals, self.bisect_guesses
67
+ title_method = "Bisection Method"
68
+ guess_label = "Midpoint"
69
+ else:
70
+ intervals, guesses = self.falsepos_intervals, self.falsepos_guesses
71
+ title_method = "False Position Method"
72
+ guess_label = "Point c"
73
+
74
+ if not guesses:
75
+ return None
76
+
77
+ total_steps = len(guesses)
78
+ all_points = [g for g in guesses] + [p for ab in intervals for p in ab[:2]]
79
+ min_g, max_g = min(all_points), max(all_points)
80
+
81
+ x_span = max_g - min_g if max_g > min_g else 1.0
82
+ x_pad = max(0.2 * x_span, 1.0)
83
+ x_min, x_max = min_g - x_pad, max_g + x_pad
84
+
85
+ x_vals = np.linspace(x_min, x_max, 400)
86
+ y_vals = self.f(x_vals)
87
+
88
+ fig, ax = plt.subplots(figsize=(10, 6))
89
+ ax.plot(x_vals, y_vals, label='f(x)', color='blue')
90
+ ax.axhline(0, color='gray', linestyle='--', alpha=0.7)
91
+
92
+ line, = ax.plot([], [], marker='o', linestyle='-', color='red', markersize=6, alpha=0.6, label='Previous guesses')
93
+ a_line = ax.axvline(0, color='red', linestyle=':', lw=1.5, alpha=0.9, label='a (left endpoint)')
94
+ b_line = ax.axvline(0, color='green', linestyle=':', lw=1.5, alpha=0.9, label='b (right endpoint)')
95
+ current_point, = ax.plot([], [], marker='o', color='gold', markersize=10, markeredgecolor='black', label=guess_label)
96
+ vertical_line, = ax.plot([], [], color='purple', linestyle=':', linewidth=2, label='Connector')
97
+
98
+ ax.set_title(f"{title_method} Animation", fontsize=14)
99
+ ax.set_xlabel("x", fontsize=12)
100
+ ax.set_ylabel("f(x)", fontsize=12)
101
+ ax.grid(True, linestyle=':', alpha=0.6)
102
+ ax.legend()
103
+ ax.set_xlim(x_min, x_max)
104
+
105
+ f_g = [self.f(g) for g in guesses]
106
+ y_min, y_max = min(f_g + [0]), max(f_g + [0])
107
+ y_span = y_max - y_min if y_max > y_min else 1.0
108
+ y_pad = max(0.2 * y_span, 1.0)
109
+ ax.set_ylim(y_min - y_pad, y_max + y_pad)
110
+
111
+ def update_closed(frame):
112
+ step = frame
113
+ shown_intervals = intervals[:step+1]
114
+ shown_guesses = guesses[:step+1]
115
+ f_guesses = [self.f(g) for g in shown_guesses]
116
+
117
+ a, b, c = shown_intervals[-1]
118
+ a_line.set_xdata([a])
119
+ b_line.set_xdata([b])
120
+
121
+ line.set_data(shown_guesses[:-1], f_guesses[:-1])
122
+ current_point.set_data([c], [self.f(c)])
123
+ vertical_line.set_data([c, c], [0, self.f(c)])
124
+
125
+ ax.set_title(f"{title_method} - Step {step}/{total_steps-1}", fontsize=14)
126
+ return line, current_point, vertical_line, a_line, b_line
127
+
128
+ anim = FuncAnimation(
129
+ fig, update_closed,
130
+ frames=total_steps,
131
+ interval=interval_ms,
132
+ blit=True,
133
+ repeat=False
134
+ )
135
+ plt.close(fig)
136
+ return HTML(anim.to_jshtml())
137
+
138
+ def show_toggle_closed(self, interval_ms=750):
139
+ bisect_anim_html = self._make_animation_closed('Bisection', interval_ms)
140
+ falsepos_anim_html = self._make_animation_closed('False Position', interval_ms)
141
+
142
+ if bisect_anim_html is None or falsepos_anim_html is None:
143
+ print("Cannot create animations. Check initial interval [a, b].")
144
+ return
145
+
146
+ bisect_content = widgets.HTML(value=bisect_anim_html.data)
147
+ falsepos_content = widgets.HTML(value=falsepos_anim_html.data)
148
+
149
+ tab_container = widgets.Tab()
150
+ tab_container.children = [bisect_content, falsepos_content]
151
+
152
+ tab_container.set_title(0, 'Bisection')
153
+ tab_container.set_title(1, 'False Position')
154
+
155
+ display(tab_container)