executable-engineering 0.1.43__tar.gz → 0.1.46__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.
Files changed (23) hide show
  1. {executable_engineering-0.1.43 → executable_engineering-0.1.46}/PKG-INFO +1 -1
  2. {executable_engineering-0.1.43 → executable_engineering-0.1.46}/executable_engineering/__init__.py +13 -4
  3. {executable_engineering-0.1.43 → executable_engineering-0.1.46}/executable_engineering/closedoptimization_module.py +136 -0
  4. executable_engineering-0.1.46/executable_engineering/constrainedoptimization_module.py +160 -0
  5. {executable_engineering-0.1.43 → executable_engineering-0.1.46}/executable_engineering/functions_module.py +14 -0
  6. {executable_engineering-0.1.43 → executable_engineering-0.1.46}/executable_engineering/neuralnet_module.py +151 -32
  7. {executable_engineering-0.1.43 → executable_engineering-0.1.46}/executable_engineering/openoptimization_module.py +135 -0
  8. {executable_engineering-0.1.43 → executable_engineering-0.1.46}/executable_engineering/optimization_module.py +6 -15
  9. {executable_engineering-0.1.43 → executable_engineering-0.1.46}/executable_engineering.egg-info/PKG-INFO +1 -1
  10. {executable_engineering-0.1.43 → executable_engineering-0.1.46}/executable_engineering.egg-info/SOURCES.txt +1 -0
  11. {executable_engineering-0.1.43 → executable_engineering-0.1.46}/setup.py +1 -1
  12. {executable_engineering-0.1.43 → executable_engineering-0.1.46}/LICENSE +0 -0
  13. {executable_engineering-0.1.43 → executable_engineering-0.1.46}/README.md +0 -0
  14. {executable_engineering-0.1.43 → executable_engineering-0.1.46}/executable_engineering/closedrootfinder_module.py +0 -0
  15. {executable_engineering-0.1.43 → executable_engineering-0.1.46}/executable_engineering/gradientoptimization_module.py +0 -0
  16. {executable_engineering-0.1.43 → executable_engineering-0.1.46}/executable_engineering/linearsystems_module.py +0 -0
  17. {executable_engineering-0.1.43 → executable_engineering-0.1.46}/executable_engineering/numerical_error_module.py +0 -0
  18. {executable_engineering-0.1.43 → executable_engineering-0.1.46}/executable_engineering/openrootfinder_module.py +0 -0
  19. {executable_engineering-0.1.43 → executable_engineering-0.1.46}/executable_engineering.egg-info/dependency_links.txt +0 -0
  20. {executable_engineering-0.1.43 → executable_engineering-0.1.46}/executable_engineering.egg-info/requires.txt +0 -0
  21. {executable_engineering-0.1.43 → executable_engineering-0.1.46}/executable_engineering.egg-info/top_level.txt +0 -0
  22. {executable_engineering-0.1.43 → executable_engineering-0.1.46}/pyproject.toml +0 -0
  23. {executable_engineering-0.1.43 → executable_engineering-0.1.46}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: executable_engineering
3
- Version: 0.1.43
3
+ Version: 0.1.46
4
4
  Summary: Demonstrative tools for the Executable Engineering textbook
5
5
  Home-page: https://github.com/themintlab/ExecutableEngineering/tree/main/executable_engineering
6
6
  Author: Michael Welland
@@ -1,15 +1,23 @@
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
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, interactive_neural_network, train_until_convergence
2
2
  from .openrootfinder_module import RootFinderOpen
3
3
  from .closedrootfinder_module import RootFinderClosed
4
- from .closedoptimization_module import OptimizerClosed
5
- from .openoptimization_module import OptimizerOpen
4
+ from .closedoptimization_module import OptimizerClosed, ScalarTracker, plot_bracketed_progression
5
+ from .openoptimization_module import OptimizerOpen, plot_optimization_path_2d, plot_optimization_path_3d
6
6
  from .gradientoptimization_module import OptimizerGrad
7
7
  from .numerical_error_module import python_internal_binary, decimal_to_binary
8
8
  from .linearsystems_module import visual_solve_2d, visualize_conditioning, visualize_matrix_norms, jacobi_step, gauss_seidel_step, sor_step, visualize_convergence_2d, create_diagonally_dominant_matrix, iter_solve, steepest_descent, conjugate_gradient, IterationTracker, discretise_poisson, spai
9
- from .functions_module import weierstrass, plot_and_find_roots, complex_plot_with_shadow, plot_polynomial_roots_companion, plot_secant, plot_newton_raphson, plot_basins_of_attraction
9
+ from .functions_module import weierstrass, plot_and_find_roots, complex_plot_with_shadow, plot_polynomial_roots_companion, plot_secant, plot_newton_raphson, plot_basins_of_attraction, newton_raphson
10
10
  from .optimization_module import visual_golden_section, visual_powell_2d, newton_method
11
+ from .constrainedoptimization_module import plot_constrained_optimum_3d
11
12
 
12
13
  __all__ = [
14
+ "interactive_neural_network",
15
+ "train_until_convergence",
16
+ "plot_constrained_optimum_3d",
17
+ "plot_optimization_path_3d",
18
+ "plot_optimization_path_2d",
19
+ "ScalarTracker",
20
+ "plot_bracketed_progression",
13
21
  "newton_method",
14
22
  "visual_powell_2d",
15
23
  "visual_golden_section",
@@ -17,6 +25,7 @@ __all__ = [
17
25
  "plot_secant",
18
26
  "plot_newton_raphson",
19
27
  "plot_basins_of_attraction",
28
+ "newton_raphson",
20
29
  "plot_and_find_roots",
21
30
  "complex_plot_with_shadow",
22
31
  "weierstrass",
@@ -280,3 +280,139 @@ class OptimizerClosed:
280
280
  tab_container.set_title(1, "Brent's Method")
281
281
 
282
282
  display(tab_container)
283
+
284
+
285
+ class ScalarTracker:
286
+ """Wraps a 1D objective function to record scalar evaluations during optimization."""
287
+ def __init__(self, f):
288
+ self.f = f
289
+ self.x_history = []
290
+ self.f_history = []
291
+
292
+ def __call__(self, x):
293
+ val = self.f(x)
294
+ if np.ndim(x) == 0 or (isinstance(x, np.ndarray) and x.size == 1):
295
+ x_scalar = float(np.squeeze(x))
296
+ f_scalar = float(np.squeeze(val))
297
+ self.x_history.append(x_scalar)
298
+ self.f_history.append(f_scalar)
299
+ return val
300
+
301
+
302
+ def plot_bracketed_progression(f, tracker, result=None, x_range=(-5, 5), x_true=None, title=None):
303
+ """Plots a 2-panel Plotly figure showing function evaluations and convergence progression."""
304
+ import plotly.graph_objects as go
305
+ from plotly.subplots import make_subplots
306
+
307
+ x_vals = np.linspace(x_range[0], x_range[1], 400)
308
+ y_vals = f(x_vals)
309
+
310
+ xs = np.array(tracker.x_history, dtype=float)
311
+ ys = np.array(tracker.f_history, dtype=float)
312
+ steps = np.arange(1, len(xs) + 1)
313
+
314
+ if result is not None:
315
+ x_min, y_min = float(result.x), float(result.fun)
316
+ elif len(xs) > 0:
317
+ idx = int(np.argmin(ys))
318
+ x_min, y_min = xs[idx], ys[idx]
319
+ else:
320
+ x_min, y_min = 0.0, float(f(0.0))
321
+
322
+ x_ref = float(x_true) if x_true is not None else x_min
323
+
324
+ fig = make_subplots(
325
+ rows=1, cols=2,
326
+ subplot_titles=("Objective Function & Evaluations", "Solver Progression (|xₖ - x*| & xₖ)"),
327
+ horizontal_spacing=0.12,
328
+ specs=[[{"secondary_y": False}, {"secondary_y": True}]]
329
+ )
330
+
331
+ # Left panel: f(x) curve
332
+ fig.add_trace(
333
+ go.Scatter(x=x_vals, y=y_vals, mode='lines', name='f(x)', line=dict(color='royalblue', width=2)),
334
+ row=1, col=1
335
+ )
336
+
337
+ # Left panel: evaluated points colored by iteration
338
+ if len(xs) > 0:
339
+ hover_texts = [f"Eval {k}<br>x = {xk:.6g}<br>f(x) = {yk:.6g}" for k, xk, yk in zip(steps, xs, ys)]
340
+ fig.add_trace(
341
+ go.Scatter(
342
+ x=xs, y=ys,
343
+ mode='markers+lines',
344
+ name='Evaluations',
345
+ line=dict(color='rgba(150, 150, 150, 0.35)', dash='dot', width=1),
346
+ marker=dict(
347
+ size=8,
348
+ color=steps,
349
+ colorscale='Viridis',
350
+ showscale=True,
351
+ colorbar=dict(title="Eval #", len=0.85, x=-0.12),
352
+ line=dict(width=1, color='black')
353
+ ),
354
+ text=hover_texts,
355
+ hoverinfo='text'
356
+ ),
357
+ row=1, col=1
358
+ )
359
+
360
+ # Left panel: Final minimum star
361
+ fig.add_trace(
362
+ go.Scatter(
363
+ x=[x_min], y=[y_min],
364
+ mode='markers',
365
+ name=f'Minimum (x*={x_min:.4g})',
366
+ marker=dict(color='crimson', size=13, symbol='star', line=dict(width=1, color='black'))
367
+ ),
368
+ row=1, col=1
369
+ )
370
+
371
+ # Right panel: x_k progression (primary y-axis) and |x_k - x*| (secondary log y-axis)
372
+ if len(xs) > 0:
373
+ fig.add_trace(
374
+ go.Scatter(
375
+ x=steps, y=xs,
376
+ mode='lines+markers',
377
+ name='Guess xₖ',
378
+ line=dict(color='teal', width=2),
379
+ marker=dict(size=6)
380
+ ),
381
+ row=1, col=2, secondary_y=False
382
+ )
383
+ fig.add_hline(y=x_ref, line_dash='dash', line_color='crimson', opacity=0.6, row=1, col=2, secondary_y=False)
384
+
385
+ raw_err = np.abs(xs - x_ref)
386
+ if x_true is None and np.any(raw_err == 0):
387
+ nonzero = raw_err[raw_err > 0]
388
+ floor_val = 0.5 * np.min(nonzero) if len(nonzero) > 0 else 1e-12
389
+ err = np.where(raw_err == 0, floor_val, raw_err)
390
+ else:
391
+ err = np.maximum(raw_err, 1e-16)
392
+
393
+ fig.add_trace(
394
+ go.Scatter(
395
+ x=steps, y=err,
396
+ mode='lines+markers',
397
+ name='|xₖ - x*| (log)',
398
+ line=dict(color='darkorange', width=2, dash='dot'),
399
+ marker=dict(size=6, symbol='diamond')
400
+ ),
401
+ row=1, col=2, secondary_y=True
402
+ )
403
+
404
+ fig.update_xaxes(title_text="x", row=1, col=1)
405
+ fig.update_yaxes(title_text="f(x)", row=1, col=1)
406
+ fig.update_xaxes(title_text="Evaluation Number (k)", row=1, col=2)
407
+ fig.update_yaxes(title_text="Guess xₖ", row=1, col=2, secondary_y=False)
408
+ fig.update_yaxes(title_text="Error |xₖ - x*|", type="log", row=1, col=2, secondary_y=True)
409
+
410
+ fig.update_layout(
411
+ title=title or f"Solver Progression ({len(xs)} Function Evaluations)",
412
+ width=950,
413
+ height=430,
414
+ margin=dict(l=90, r=60, t=60, b=50),
415
+ legend=dict(orientation="h", yanchor="bottom", y=-0.28, xanchor="center", x=0.5)
416
+ )
417
+ fig.show()
418
+
@@ -0,0 +1,160 @@
1
+ import numpy as np
2
+ import plotly.graph_objects as go
3
+
4
+
5
+ def plot_constrained_optimum_3d(
6
+ func,
7
+ optimum,
8
+ constraint_curve=None,
9
+ x_range=(-1.5, 1.5),
10
+ y_range=(-1.5, 1.5),
11
+ x0=None,
12
+ path=None,
13
+ displacement_factor=0.8,
14
+ title="Constrained Optimization Surface",
15
+ ):
16
+ """
17
+ Plots an interactive 3D objective surface, constraint boundary, iterates, and the constrained optimum.
18
+
19
+ Args:
20
+ func: Callable taking a 2-element sequence [x, y] and returning a scalar f(x, y).
21
+ optimum: Sequence [x*, y*] representing the constrained optimum.
22
+ constraint_curve: Optional array-like of shape (M, 2) defining (x, y) coordinates of the constraint boundary.
23
+ x_range: Tuple of (x_min, x_max).
24
+ y_range: Tuple of (y_min, y_max).
25
+ x0: Optional sequence [x0, y0] representing the initial starting guess.
26
+ path: Optional array-like of shape (N, 2) representing intermediate iterates.
27
+ displacement_factor: Fraction of z-span to displace the floor plane below the surface minimum.
28
+ title: Plot title.
29
+ """
30
+ x_vals = np.linspace(x_range[0], x_range[1], 100)
31
+ y_vals = np.linspace(y_range[0], y_range[1], 100)
32
+ X, Y = np.meshgrid(x_vals, y_vals)
33
+
34
+ Z = np.zeros_like(X)
35
+ for i in range(X.shape[0]):
36
+ for j in range(X.shape[1]):
37
+ Z[i, j] = func([X[i, j], Y[i, j]])
38
+
39
+ z_min_surf = float(np.min(Z))
40
+ z_max_surf = float(np.max(Z))
41
+ z_span = z_max_surf - z_min_surf if z_max_surf > z_min_surf else 1.0
42
+ z_floor = z_min_surf - displacement_factor * z_span
43
+
44
+ fig = go.Figure()
45
+
46
+ # 3D surface with bottom-projected contours (shadow)
47
+ fig.add_trace(
48
+ go.Surface(
49
+ x=x_vals,
50
+ y=y_vals,
51
+ z=Z,
52
+ colorscale="Viridis",
53
+ opacity=0.8,
54
+ contours=dict(
55
+ z=dict(
56
+ show=True,
57
+ usecolormap=True,
58
+ highlightcolor="limegreen",
59
+ project_z=True,
60
+ )
61
+ ),
62
+ colorbar=dict(title="f(x, y)", len=0.75, x=1.05),
63
+ name="Surface",
64
+ )
65
+ )
66
+
67
+ # Constraint curve (both along surface and on floor plane)
68
+ if constraint_curve is not None:
69
+ c_pts = np.asarray(constraint_curve, dtype=float)
70
+ cz_surf = np.array([float(func(pt)) for pt in c_pts])
71
+
72
+ # Constraint along surface
73
+ fig.add_trace(
74
+ go.Scatter3d(
75
+ x=c_pts[:, 0],
76
+ y=c_pts[:, 1],
77
+ z=cz_surf,
78
+ mode="lines",
79
+ line=dict(color="orange", width=6),
80
+ name="Constraint on Surface",
81
+ )
82
+ )
83
+
84
+ # Constraint projected on floor
85
+ fig.add_trace(
86
+ go.Scatter3d(
87
+ x=c_pts[:, 0],
88
+ y=c_pts[:, 1],
89
+ z=[z_floor] * len(c_pts),
90
+ mode="lines",
91
+ line=dict(color="rgba(255, 140, 0, 0.7)", width=4, dash="dash"),
92
+ name="Constraint on Floor",
93
+ )
94
+ )
95
+
96
+ # Optional optimization path
97
+ if path is not None:
98
+ pts = np.asarray(path, dtype=float)
99
+ if pts.ndim == 2 and len(pts) > 0:
100
+ pz = np.array([float(func(pt)) for pt in pts])
101
+ fig.add_trace(
102
+ go.Scatter3d(
103
+ x=pts[:, 0],
104
+ y=pts[:, 1],
105
+ z=pz,
106
+ mode="lines+markers",
107
+ line=dict(color="red", width=4),
108
+ marker=dict(size=4, color="crimson"),
109
+ name="Solver Path",
110
+ )
111
+ )
112
+
113
+ # Initial guess
114
+ if x0 is not None:
115
+ pt0 = np.asarray(x0, dtype=float)
116
+ z0 = float(func(pt0))
117
+ fig.add_trace(
118
+ go.Scatter3d(
119
+ x=[pt0[0]],
120
+ y=[pt0[1]],
121
+ z=[z0],
122
+ mode="markers",
123
+ marker=dict(size=8, color="cyan", symbol="circle"),
124
+ name=f"Initial Guess ({pt0[0]:.2f}, {pt0[1]:.2f})",
125
+ )
126
+ )
127
+
128
+ # Optimum point
129
+ opt = np.asarray(optimum, dtype=float)
130
+ z_opt = float(func(opt))
131
+ fig.add_trace(
132
+ go.Scatter3d(
133
+ x=[opt[0]],
134
+ y=[opt[1]],
135
+ z=[z_opt],
136
+ mode="markers",
137
+ marker=dict(size=10, color="crimson", symbol="diamond"),
138
+ name=f"Optimum ({opt[0]:.3f}, {opt[1]:.3f})",
139
+ )
140
+ )
141
+
142
+ fig.update_layout(
143
+ title=title,
144
+ scene=dict(
145
+ xaxis_title="x",
146
+ yaxis_title="y",
147
+ zaxis_title="f(x, y)",
148
+ zaxis=dict(range=[z_floor, z_max_surf + 0.05 * z_span]),
149
+ aspectmode="manual",
150
+ aspectratio=dict(x=1, y=1, z=0.9),
151
+ camera=dict(eye=dict(x=1.6, y=1.6, z=1.3)),
152
+ ),
153
+ width=750,
154
+ height=550,
155
+ margin=dict(l=20, r=20, t=50, b=20),
156
+ legend=dict(
157
+ orientation="h", yanchor="bottom", y=-0.15, xanchor="center", x=0.5
158
+ ),
159
+ )
160
+ fig.show()
@@ -276,3 +276,17 @@ def plot_basins_of_attraction(f, df, known_roots, title, real_range=(-2, 2), ima
276
276
  fig = go.Figure(data=go.Heatmap(z=colors, x=real_values, y=imag_values, colorscale='Viridis', showscale=False))
277
277
  fig.update_layout(title=title, xaxis_title='Real', yaxis_title='Imaginary', width=600, height=600)
278
278
  fig.show()
279
+
280
+ def newton_raphson(f, df, x0, max_iter=8, tolerance=1e-6):
281
+ x = x0
282
+ guesses = [x]
283
+ for i in range(max_iter):
284
+ d = df(x)
285
+ if d == 0:
286
+ break
287
+ x_new = x - f(x) / d
288
+ guesses.append(x_new)
289
+ if abs(x_new - x) < tolerance:
290
+ return x_new, guesses
291
+ x = x_new
292
+ return None, guesses
@@ -1,9 +1,14 @@
1
1
  # Neural network model UI and plotting
2
2
  import ipywidgets as widgets
3
3
  import matplotlib.pyplot as plt
4
+ import matplotlib.cm as cm
5
+ import matplotlib.colors as mcolors
4
6
  import networkx as nx
5
7
  import numpy as np
6
8
  import plotly.graph_objs as go
9
+ from IPython.display import display
10
+
11
+ plt.ioff()
7
12
 
8
13
  def init_weights():
9
14
  global W1, b1, W2, b2
@@ -133,38 +138,46 @@ def forward_pass(x):
133
138
  activations.append(z) # no activation on final output
134
139
  return zs, activations
135
140
 
136
- def backward_pass(zs, activations, y_true, lr=0.01):
141
+ def backward_pass(zs, activations, y_true, lr=0.02):
137
142
  global weights, biases, weight_history, bias_history
138
143
  grads_w = [None] * len(weights)
139
144
  grads_b = [None] * len(biases)
140
145
 
141
146
  delta = (activations[-1] - y_true)
142
- grads_w[-1] = activations[-2].T @ delta / len(X)
147
+ inv_m = 1.0 / len(y_true)
148
+ grads_w[-1] = (activations[-2].T @ delta) * inv_m
143
149
  grads_b[-1] = np.mean(delta, axis=0, keepdims=True)
144
150
 
145
151
  for l in range(2, len(weights)+1):
146
152
  z = zs[-l]
147
153
  sp = activation_derivative(z)
148
154
  delta = (delta @ weights[-l+1].T) * sp
149
- grads_w[-l] = activations[-l-1].T @ delta / len(X)
155
+ grads_w[-l] = (activations[-l-1].T @ delta) * inv_m
150
156
  grads_b[-l] = np.mean(delta, axis=0, keepdims=True)
151
157
 
152
158
  for i in range(len(weights)):
153
159
  weights[i] -= lr * grads_w[i]
154
160
  biases[i] -= lr * grads_b[i]
155
- weight_history[i].append(weights[i].copy())
156
- bias_history[i].append(biases[i].copy())
161
+ # Efficient scalar tracking to eliminate memory overhead
162
+ weight_history[i].append(float(weights[i].ravel()[0]))
163
+ bias_history[i].append(float(biases[i].ravel()[0]))
157
164
 
158
- return np.mean((activations[-1] - y_true)**2)
165
+ return float(np.mean((activations[-1] - y_true)**2))
159
166
 
160
- def step(n,output_plot, metrics_plot, network_plot):
167
+ def step(n, output_plot, metrics_plot, network_plot, status_label=None):
161
168
  global losses
162
- if true_function is None: return
169
+ if true_function is None:
170
+ if status_label:
171
+ status_label.value = "Please save a function first."
172
+ return
163
173
  y_true = true_function(X)
174
+ last_loss = 0.0
164
175
  for _ in range(n):
165
176
  zs, activations = forward_pass(X)
166
- loss = backward_pass(zs, activations, y_true)
167
- losses.append(loss)
177
+ last_loss = backward_pass(zs, activations, y_true)
178
+ losses.append(last_loss)
179
+ if status_label:
180
+ status_label.value = f"Trained {n} steps. Total steps: {len(losses)} | Loss: {last_loss:.4e}"
168
181
  update_plots(output_plot, metrics_plot, network_plot)
169
182
 
170
183
  def reset_model(output_plot, metrics_plot, network_plot, status_label):
@@ -195,10 +208,48 @@ def change_width(d, output_plot, metrics_plot, network_plot, status_label):
195
208
  reset_model(output_plot, metrics_plot, network_plot, status_label)
196
209
 
197
210
 
198
- def draw_network(activations):
199
- import matplotlib.cm as cm
200
- import matplotlib.colors as mcolors
211
+ def train_until_convergence(output_plot, metrics_plot, network_plot, status_label, target_loss=1e-4, max_steps=50000, window=5000, min_rel_improvement=0.005):
212
+ """Trains the neural network iteratively until a high-precision target loss or convergence plateau is reached."""
213
+ global losses
214
+ if true_function is None:
215
+ status_label.value = "Please save a function first."
216
+ return
217
+
218
+ y_true = true_function(X)
219
+ status_label.value = "Training to convergence..."
220
+
221
+ batch_size = 500
222
+ step_count = 0
223
+ stop_reason = ""
224
+ window_start_loss = float('inf')
225
+ current_loss = float('inf')
226
+
227
+ while step_count < max_steps:
228
+ for _ in range(batch_size):
229
+ zs, activations = forward_pass(X)
230
+ current_loss = backward_pass(zs, activations, y_true, lr=0.02)
231
+ losses.append(current_loss)
232
+ step_count += batch_size
233
+
234
+ if current_loss <= target_loss:
235
+ stop_reason = f"Target loss reached ({current_loss:.4e} <= {target_loss}) in {step_count} steps. Total: {len(losses)}"
236
+ break
237
+
238
+ if step_count % window == 0:
239
+ rel_improvement = (window_start_loss - current_loss) / max(window_start_loss, 1e-8)
240
+ if step_count > window and rel_improvement < min_rel_improvement:
241
+ stop_reason = f"Converged to plateau (rel. change < {min_rel_improvement:.1%} over {window} steps) at loss {current_loss:.4e}. Total steps: {len(losses)}"
242
+ break
243
+ window_start_loss = current_loss
244
+
245
+ if not stop_reason:
246
+ stop_reason = f"Completed {step_count} steps. Total steps: {len(losses)} | Loss: {current_loss:.4e} (Click again to train further)"
247
+
248
+ status_label.value = stop_reason
249
+ update_plots(output_plot, metrics_plot, network_plot)
201
250
 
251
+
252
+ def draw_network(activations):
202
253
  G = nx.DiGraph()
203
254
  labels = {}
204
255
  pos = {}
@@ -242,44 +293,112 @@ def draw_network(activations):
242
293
 
243
294
 
244
295
  def update_plots(output_plot, metrics_plot, network_plot):
245
- output_plot.clear_output(wait=True)
246
- metrics_plot.clear_output(wait=True)
247
- network_plot.clear_output(wait=True)
296
+ plt.ioff()
248
297
  with output_plot:
298
+ output_plot.clear_output(wait=True)
249
299
  if true_function:
250
- plt.figure(figsize=(6, 3))
300
+ fig_out = plt.figure(figsize=(6, 3))
251
301
  y_true = true_function(X)
252
302
  _, activations = forward_pass(X2)
253
303
  y_pred = activations[-1]
254
- plt.plot(X, y_true, label='True')
255
- plt.scatter(X2, y_pred, label='NN')
304
+ plt.plot(X, y_true, label='True', color='black', linewidth=1.5)
305
+ plt.scatter(X2, y_pred, label='NN Output', color='tab:blue', s=20, alpha=0.8, zorder=3)
256
306
  plt.legend()
257
307
  plt.title("Function vs NN Output")
258
- plt.grid(True)
259
- plt.show()
308
+ plt.grid(True, linestyle='--', alpha=0.6)
309
+ plt.tight_layout()
310
+ display(fig_out)
311
+ plt.close(fig_out)
260
312
 
261
313
  with metrics_plot:
262
314
  if losses:
263
- plt.figure(figsize=(6, 3))
264
- plt.plot(losses, label="Loss", color='red')
315
+ metrics_plot.clear_output(wait=True)
316
+ fig_met = plt.figure(figsize=(6, 3))
317
+ stride = max(1, len(losses) // 500)
318
+ indices = list(range(0, len(losses), stride))
319
+ loss_sub = [losses[idx] for idx in indices]
320
+ plt.plot(indices, loss_sub, label="Loss", color='red', linewidth=1.5, zorder=4)
265
321
  for i, history in enumerate(weight_history):
266
- flat_vals = [w.flatten()[0] for w in history]
267
- plt.plot(flat_vals, label=f"W{i}_0", alpha=0.5)
322
+ sub_w = [history[idx] for idx in indices]
323
+ plt.plot(indices, sub_w, label=f"W{i}_0", alpha=0.5)
268
324
  for i, history in enumerate(bias_history):
269
- flat_vals = [b.flatten()[0] for b in history]
270
- plt.plot(flat_vals, label=f"b{i}_0", linestyle='dotted', alpha=0.5)
325
+ sub_b = [history[idx] for idx in indices]
326
+ plt.plot(indices, sub_b, label=f"b{i}_0", linestyle='dotted', alpha=0.5)
271
327
  plt.title("Loss and Parameter Changes")
328
+ plt.grid(True, linestyle='--', alpha=0.6)
329
+ plt.legend(loc='upper right', fontsize=8)
330
+ plt.tight_layout()
331
+ display(fig_met)
332
+ plt.close(fig_met)
333
+ else:
334
+ metrics_plot.clear_output(wait=False)
272
335
 
273
- plt.grid(True)
274
- plt.legend()
275
- plt.show()
276
336
  with network_plot:
337
+ network_plot.clear_output(wait=True)
277
338
  if true_function:
278
339
  _, activations = forward_pass(X)
340
+ fig_net = plt.figure(figsize=(7, 3.5))
279
341
  G, pos, labels, edge_labels, edge_colors = draw_network(activations)
280
342
  edge_color_vals = [edge_colors.get(edge, '#888888') for edge in G.edges()]
281
343
  nx.draw(G, pos, labels=labels, node_color='lightblue', node_size=600,
282
- edge_color=edge_color_vals, edge_cmap=plt.cm.bwr, arrows=True)
344
+ edge_color=edge_color_vals, arrows=True, font_size=9)
283
345
  nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels, font_size=8)
284
346
  plt.title("Neural Network Diagram")
285
- plt.show()
347
+ display(fig_net)
348
+ plt.close(fig_net)
349
+
350
+
351
+ def interactive_neural_network():
352
+ """Constructs and displays the interactive neural network training widget."""
353
+ import ipywidgets as widgets
354
+ from ipywidgets import VBox, HBox, Button, Text, Label, Output
355
+
356
+ plt.ioff()
357
+ output_plot = Output()
358
+ network_plot = Output(layout={"height": "500px", "overflow": "auto"})
359
+ metrics_plot = Output()
360
+ status_label = Label(value="Press 'Step' to begin training.")
361
+
362
+ function_input = Text(value='exp(-x/2)*sin(2*pi*x)', description='f(x):')
363
+ save_button = Button(description='Save Function', button_style='success')
364
+ reset_button = Button(description='Reset Model', button_style='warning')
365
+
366
+ step_label = Label(value="Step:")
367
+ step_1 = Button(description='1')
368
+ step_10 = Button(description='10')
369
+ step_100 = Button(description='100')
370
+ step_1000 = Button(description='1000')
371
+ step_10000 = Button(description='10000')
372
+ train_converge_btn = Button(description='Train to Convergence', button_style='primary', tooltip='Trains iteratively until a plateau or high-precision convergence (<1e-4) is reached')
373
+
374
+ depth_label = Label(value="Change Depth (layers):")
375
+ depth_add = Button(description='+1')
376
+ depth_sub = Button(description='-1')
377
+ width_label = Label(value="Change Width (neurons):")
378
+ width_add = Button(description='+1')
379
+ width_sub = Button(description='-1')
380
+
381
+ save_button.on_click(lambda b: save_function(function_input, output_plot, metrics_plot, network_plot, status_label))
382
+ reset_button.on_click(lambda b: reset_model(output_plot, metrics_plot, network_plot, status_label))
383
+ step_1.on_click(lambda b: step(1, output_plot, metrics_plot, network_plot, status_label))
384
+ step_10.on_click(lambda b: step(10, output_plot, metrics_plot, network_plot, status_label))
385
+ step_100.on_click(lambda b: step(100, output_plot, metrics_plot, network_plot, status_label))
386
+ step_1000.on_click(lambda b: step(1000, output_plot, metrics_plot, network_plot, status_label))
387
+ step_10000.on_click(lambda b: step(10000, output_plot, metrics_plot, network_plot, status_label))
388
+ train_converge_btn.on_click(lambda b: train_until_convergence(output_plot, metrics_plot, network_plot, status_label))
389
+ depth_add.on_click(lambda b: change_depth(1, output_plot, metrics_plot, network_plot, status_label))
390
+ depth_sub.on_click(lambda b: change_depth(-1, output_plot, metrics_plot, network_plot, status_label))
391
+ width_add.on_click(lambda b: change_width(1, output_plot, metrics_plot, network_plot, status_label))
392
+ width_sub.on_click(lambda b: change_width(-1, output_plot, metrics_plot, network_plot, status_label))
393
+
394
+ top_controls = HBox([function_input, save_button, reset_button])
395
+ step_controls = HBox([step_label, step_1, step_10, step_100, step_1000, step_10000, train_converge_btn])
396
+ depth_controls = HBox([depth_label, depth_sub, depth_add])
397
+ width_controls = HBox([width_label, width_sub, width_add])
398
+ graph_row = HBox([output_plot, metrics_plot])
399
+ full_ui = VBox([top_controls, status_label, step_controls, depth_controls, width_controls, network_plot, graph_row])
400
+ display(full_ui)
401
+
402
+ init_model()
403
+ save_function(function_input, output_plot, metrics_plot, network_plot, status_label)
404
+
@@ -229,3 +229,138 @@ class OptimizerOpen:
229
229
  tab.set_title(0, "Powell's Method")
230
230
  tab.set_title(1, "Nelder–Mead Method")
231
231
  display(tab)
232
+
233
+
234
+ def plot_optimization_path_3d(func, path, x_range=None, y_range=None, displacement_factor=0.8, title="3D Optimization Surface & Path"):
235
+ """
236
+ Plots an interactive 3D surface with floor contour shadow and the optimization path.
237
+
238
+ Args:
239
+ func: Callable taking a 2-element sequence [x1, x2] and returning a scalar.
240
+ path: Array-like of shape (N, 2) containing the sequence of iterate points.
241
+ x_range: Tuple of (x_min, x_max). If None, inferred with margin from path.
242
+ y_range: Tuple of (y_min, y_max). If None, inferred with margin from path.
243
+ displacement_factor: Fraction of z-span to displace the floor below the surface minimum.
244
+ title: Plot title.
245
+ """
246
+ import numpy as np
247
+ import plotly.graph_objects as go
248
+
249
+ pts = np.asarray(path, dtype=float)
250
+ if pts.ndim != 2 or pts.shape[1] != 2:
251
+ raise ValueError("path must be of shape (N, 2)")
252
+
253
+ if x_range is None:
254
+ dx = max(np.ptp(pts[:, 0]) * 0.35, 1.0)
255
+ x_range = (np.min(pts[:, 0]) - dx, np.max(pts[:, 0]) + dx)
256
+ if y_range is None:
257
+ dy = max(np.ptp(pts[:, 1]) * 0.35, 1.0)
258
+ y_range = (np.min(pts[:, 1]) - dy, np.max(pts[:, 1]) + dy)
259
+
260
+ x_vals = np.linspace(x_range[0], x_range[1], 100)
261
+ y_vals = np.linspace(y_range[0], y_range[1], 100)
262
+ X, Y = np.meshgrid(x_vals, y_vals)
263
+
264
+ Z = np.zeros_like(X)
265
+ for i in range(X.shape[0]):
266
+ for j in range(X.shape[1]):
267
+ Z[i, j] = func([X[i, j], Y[i, j]])
268
+
269
+ z_pts = np.array([float(func(pt)) for pt in pts])
270
+ z_min_surf = float(np.min(Z))
271
+ z_max_surf = float(np.max(Z))
272
+ z_span = z_max_surf - z_min_surf if z_max_surf > z_min_surf else 1.0
273
+ z_gap = displacement_factor * z_span
274
+ z_floor = z_min_surf - z_gap
275
+ steps = np.arange(len(pts))
276
+
277
+ fig = go.Figure()
278
+
279
+ # 3D surface with bottom-projected contours (shadow)
280
+ fig.add_trace(go.Surface(
281
+ x=x_vals, y=y_vals, z=Z,
282
+ colorscale="Viridis",
283
+ opacity=0.85,
284
+ contours=dict(
285
+ z=dict(show=True, usecolormap=True, highlightcolor="limegreen", project_z=True)
286
+ ),
287
+ colorbar=dict(title="f(x1, x2)", len=0.75, x=1.05),
288
+ name="Surface"
289
+ ))
290
+
291
+ # 3D path line along the surface
292
+ fig.add_trace(go.Scatter3d(
293
+ x=pts[:, 0], y=pts[:, 1], z=z_pts,
294
+ mode="lines",
295
+ line=dict(color="red", width=5),
296
+ name="Path (3D)"
297
+ ))
298
+
299
+ # 3D iterate markers colored by step number
300
+ hover_texts = [
301
+ f"Step {k}<br>x1 = {pt[0]:.4f}<br>x2 = {pt[1]:.4f}<br>f = {zk:.4e}"
302
+ for k, (pt, zk) in enumerate(zip(pts, z_pts))
303
+ ]
304
+
305
+ fig.add_trace(go.Scatter3d(
306
+ x=pts[:, 0], y=pts[:, 1], z=z_pts,
307
+ mode="markers",
308
+ marker=dict(
309
+ size=5,
310
+ color=steps,
311
+ colorscale="Plasma",
312
+ showscale=True,
313
+ colorbar=dict(title="Iteration", len=0.75, x=-0.08)
314
+ ),
315
+ name="Iterates",
316
+ text=hover_texts,
317
+ hoverinfo="text"
318
+ ))
319
+
320
+ # Projected 2D shadow path on the floor plane z = z_floor
321
+ fig.add_trace(go.Scatter3d(
322
+ x=pts[:, 0], y=pts[:, 1], z=[z_floor] * len(pts),
323
+ mode="lines+markers",
324
+ line=dict(color="rgba(40, 40, 40, 0.6)", width=3, dash="dot"),
325
+ marker=dict(size=3, color="black"),
326
+ name="Floor Projection"
327
+ ))
328
+
329
+ # Start and End points
330
+ fig.add_trace(go.Scatter3d(
331
+ x=[pts[0, 0]], y=[pts[0, 1]], z=[z_pts[0]],
332
+ mode="markers",
333
+ marker=dict(size=8, color="cyan", symbol="circle"),
334
+ name=f"Start ({pts[0, 0]:.2f}, {pts[0, 1]:.2f})"
335
+ ))
336
+
337
+ fig.add_trace(go.Scatter3d(
338
+ x=[pts[-1, 0]], y=[pts[-1, 1]], z=[z_pts[-1]],
339
+ mode="markers",
340
+ marker=dict(size=10, color="crimson", symbol="diamond"),
341
+ name=f"End ({pts[-1, 0]:.4f}, {pts[-1, 1]:.4f})"
342
+ ))
343
+
344
+ fig.update_layout(
345
+ title=title,
346
+ scene=dict(
347
+ xaxis_title="x1",
348
+ yaxis_title="x2",
349
+ zaxis_title="f(x1, x2)",
350
+ zaxis=dict(range=[z_floor, z_max_surf + 0.05 * z_span]),
351
+ aspectmode="manual",
352
+ aspectratio=dict(x=1, y=1, z=0.9),
353
+ camera=dict(eye=dict(x=1.6, y=1.6, z=1.3))
354
+ ),
355
+ width=800,
356
+ height=600,
357
+ margin=dict(l=20, r=20, t=50, b=20),
358
+ legend=dict(orientation="h", yanchor="bottom", y=-0.15, xanchor="center", x=0.5)
359
+ )
360
+ fig.show()
361
+
362
+
363
+ # Backward-compatible alias
364
+ plot_optimization_path_2d = plot_optimization_path_3d
365
+
366
+
@@ -290,8 +290,8 @@ def visual_golden_section(f, x1, x3, tol=1e-5, max_iter=None):
290
290
  ),
291
291
  xaxis_title="x",
292
292
  yaxis_title="f(x)",
293
- width=800,
294
- height=620,
293
+ width=700,
294
+ height=540,
295
295
  margin=dict(
296
296
  l=25,
297
297
  r=25,
@@ -479,23 +479,13 @@ def visual_powell_2d(
479
479
  )
480
480
  )
481
481
 
482
- if converged:
483
- title = (
484
- f"Powell's Method: Converged "
485
- f"({iteration + 1} iterations)"
486
- )
487
- else:
488
- title = (
489
- f"Powell's Method: Maximum Iterations Reached "
490
- f"({max_iter} iterations)"
491
- )
492
-
482
+ title = (f"Powell's Method")
493
483
  fig.update_layout(
494
484
  title=title,
495
485
  xaxis_title="x",
496
486
  yaxis_title="y",
497
- width=800,
498
- height=600,
487
+ width=700,
488
+ height=525,
499
489
  margin=dict(
500
490
  l=25,
501
491
  r=25,
@@ -550,6 +540,7 @@ def newton_method(f, grad_f, hessian_f, x0, tol=1e-6, max_iter=100):
550
540
 
551
541
  fig.update_layout(
552
542
  title='Newton\'s Method Optimization',
543
+ height = 600,
553
544
  scene=dict(
554
545
  xaxis_title='x',
555
546
  yaxis_title='y',
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: executable_engineering
3
- Version: 0.1.43
3
+ Version: 0.1.46
4
4
  Summary: Demonstrative tools for the Executable Engineering textbook
5
5
  Home-page: https://github.com/themintlab/ExecutableEngineering/tree/main/executable_engineering
6
6
  Author: Michael Welland
@@ -5,6 +5,7 @@ setup.py
5
5
  executable_engineering/__init__.py
6
6
  executable_engineering/closedoptimization_module.py
7
7
  executable_engineering/closedrootfinder_module.py
8
+ executable_engineering/constrainedoptimization_module.py
8
9
  executable_engineering/functions_module.py
9
10
  executable_engineering/gradientoptimization_module.py
10
11
  executable_engineering/linearsystems_module.py
@@ -6,7 +6,7 @@ long_description = (this_directory / "README.md").read_text(encoding="utf-8")
6
6
 
7
7
  setup(
8
8
  name="executable_engineering",
9
- version="0.1.43",
9
+ version="0.1.46",
10
10
  description="Demonstrative tools for the Executable Engineering textbook",
11
11
  long_description=long_description,
12
12
  long_description_content_type="text/markdown",