executable-engineering 0.1.42__tar.gz → 0.1.44__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 (24) hide show
  1. {executable_engineering-0.1.42 → executable_engineering-0.1.44}/PKG-INFO +1 -1
  2. {executable_engineering-0.1.42 → executable_engineering-0.1.44}/executable_engineering/__init__.py +16 -4
  3. {executable_engineering-0.1.42 → executable_engineering-0.1.44}/executable_engineering/closedoptimization_module.py +136 -0
  4. executable_engineering-0.1.44/executable_engineering/constrainedoptimization_module.py +160 -0
  5. executable_engineering-0.1.44/executable_engineering/functions_module.py +292 -0
  6. {executable_engineering-0.1.42 → executable_engineering-0.1.44}/executable_engineering/neuralnet_module.py +154 -31
  7. {executable_engineering-0.1.42 → executable_engineering-0.1.44}/executable_engineering/openoptimization_module.py +135 -0
  8. {executable_engineering-0.1.42 → executable_engineering-0.1.44}/executable_engineering/optimization_module.py +6 -15
  9. {executable_engineering-0.1.42 → executable_engineering-0.1.44}/executable_engineering.egg-info/PKG-INFO +1 -1
  10. {executable_engineering-0.1.42 → executable_engineering-0.1.44}/executable_engineering.egg-info/SOURCES.txt +1 -0
  11. {executable_engineering-0.1.42 → executable_engineering-0.1.44}/setup.py +1 -1
  12. executable_engineering-0.1.42/executable_engineering/functions_module.py +0 -151
  13. {executable_engineering-0.1.42 → executable_engineering-0.1.44}/LICENSE +0 -0
  14. {executable_engineering-0.1.42 → executable_engineering-0.1.44}/README.md +0 -0
  15. {executable_engineering-0.1.42 → executable_engineering-0.1.44}/executable_engineering/closedrootfinder_module.py +0 -0
  16. {executable_engineering-0.1.42 → executable_engineering-0.1.44}/executable_engineering/gradientoptimization_module.py +0 -0
  17. {executable_engineering-0.1.42 → executable_engineering-0.1.44}/executable_engineering/linearsystems_module.py +0 -0
  18. {executable_engineering-0.1.42 → executable_engineering-0.1.44}/executable_engineering/numerical_error_module.py +0 -0
  19. {executable_engineering-0.1.42 → executable_engineering-0.1.44}/executable_engineering/openrootfinder_module.py +0 -0
  20. {executable_engineering-0.1.42 → executable_engineering-0.1.44}/executable_engineering.egg-info/dependency_links.txt +0 -0
  21. {executable_engineering-0.1.42 → executable_engineering-0.1.44}/executable_engineering.egg-info/requires.txt +0 -0
  22. {executable_engineering-0.1.42 → executable_engineering-0.1.44}/executable_engineering.egg-info/top_level.txt +0 -0
  23. {executable_engineering-0.1.42 → executable_engineering-0.1.44}/pyproject.toml +0 -0
  24. {executable_engineering-0.1.42 → executable_engineering-0.1.44}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: executable_engineering
3
- Version: 0.1.42
3
+ Version: 0.1.44
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,19 +1,31 @@
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
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",
16
24
  "plot_polynomial_roots_companion",
25
+ "plot_secant",
26
+ "plot_newton_raphson",
27
+ "plot_basins_of_attraction",
28
+ "newton_raphson",
17
29
  "plot_and_find_roots",
18
30
  "complex_plot_with_shadow",
19
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()
@@ -0,0 +1,292 @@
1
+ import numpy as np
2
+
3
+ def weierstrass(x, a=0.5, b=13, iterations=100):
4
+ """
5
+ Computes the Weierstrass function.
6
+
7
+ Parameters:
8
+ x (array-like): The points at which to evaluate the function.
9
+ a (float): Parameter 0 < a < 1.
10
+ b (float): Parameter such that ab > 1 + 3pi/2.
11
+ iterations (int): Number of terms in the sum.
12
+
13
+ Returns:
14
+ y (ndarray): The function values.
15
+ """
16
+ x = np.asarray(x)
17
+ y = np.zeros_like(x, dtype=float)
18
+ for n in range(iterations):
19
+ y += (a**n) * np.cos((b**n) * np.pi * x)
20
+ return y
21
+
22
+ import plotly.graph_objects as go
23
+ from scipy.optimize import root
24
+
25
+ def plot_and_find_roots(func):
26
+ x = np.linspace(-10, 10, 400)
27
+ y = func(x)
28
+
29
+ # Find roots
30
+ x0s = np.arange(-10, 10, 1)
31
+ roots = []
32
+ for x0 in x0s:
33
+ r = root(func, x0=x0)
34
+ if r.success:
35
+ roots.append((r.x[0], r.fun[0]))
36
+
37
+ # Remove duplicate roots (within tolerance)
38
+ unique_roots = []
39
+ tol = 1e-6
40
+ for rx, ry in roots:
41
+ if not any(abs(rx - ux) < tol for ux, _ in unique_roots):
42
+ unique_roots.append((rx, ry))
43
+
44
+ # Plot with Plotly
45
+ fig = go.Figure()
46
+ fig.add_trace(go.Scatter(x=x, y=y, mode='lines', name='f(x)'))
47
+ fig.add_hline(y=0, line_dash='dash', line_color='black', annotation_text='x-axis')
48
+ if unique_roots:
49
+ rx_vals = [rx for rx, _ in unique_roots]
50
+ fig.add_trace(go.Scatter(x=rx_vals, y=[0]*len(unique_roots), mode='markers', marker=dict(color='red', size=10), name='Roots'))
51
+ fig.update_layout(title='Plot of f(x) and its Roots', xaxis_title='x', yaxis_title='f(x)', xaxis_range=[-10, 10], yaxis_range=[-10, 10])
52
+ fig.show()
53
+
54
+ def complex_plot_with_shadow(func, title='Complex Plot of f(z)', roots=None, plot_range=(-3, 3)):
55
+ real_range = np.linspace(plot_range[0], plot_range[1], 100)
56
+ imag_range = np.linspace(plot_range[0], plot_range[1], 100)
57
+ abs_val = np.empty((len(real_range), len(imag_range)))
58
+
59
+ for i, real in enumerate(real_range):
60
+ for j, imag in enumerate(imag_range):
61
+ z = complex(real, imag)
62
+ result = func(z)
63
+ abs_val[i, j] = abs(result)
64
+
65
+ surface = go.Surface(
66
+ z=abs_val.T,
67
+ x=real_range,
68
+ y=imag_range,
69
+ colorscale='Viridis',
70
+ contours=dict(z=dict(show=True, usecolormap=True, highlightcolor="limegreen", project_z=True))
71
+ )
72
+
73
+ data = [surface]
74
+
75
+ if roots is not None:
76
+ root_x = [r.real for r in roots]
77
+ root_y = [r.imag for r in roots]
78
+ # Calculate z for roots (which is abs(func(r)) = 0)
79
+ root_z = [0 for r in roots]
80
+
81
+ data.append(go.Scatter3d(
82
+ x=root_x, y=root_y, z=root_z,
83
+ mode='markers',
84
+ marker=dict(color='red', size=5, symbol='circle'),
85
+ name='Roots'
86
+ ))
87
+
88
+ fig = go.Figure(data=data)
89
+ fig.update_layout(
90
+ title=title,
91
+ scene=dict(xaxis_title='Real', yaxis_title='Imaginary', zaxis_title='|f(z)|')
92
+ )
93
+ fig.show()
94
+
95
+
96
+ from scipy.linalg import companion
97
+
98
+ def plot_polynomial_roots_companion(n):
99
+ """
100
+ Creates a random polynomial of order n, solves for its roots
101
+ using the companion matrix method, and plots the polynomial
102
+ along with the roots as a complex surface with a shadow.
103
+ """
104
+ # Create random coefficients from -10 to 10
105
+ # A polynomial of order n has n+1 coefficients. Let's make the leading coefficient non-zero.
106
+ coeffs = np.random.uniform(-10, 10, n + 1)
107
+
108
+ # Make sure leading coefficient is not too small
109
+ if abs(coeffs[0]) < 1:
110
+ coeffs[0] = 1.0 * np.sign(coeffs[0]) if coeffs[0] != 0 else 1.0
111
+
112
+ poly_func = np.poly1d(coeffs)
113
+
114
+ print("Polynomial:")
115
+ print(poly_func)
116
+
117
+ # Solve for roots using companion matrix
118
+ comp_matrix = companion(coeffs)
119
+
120
+ print("\nCompanion Matrix:")
121
+ print(np.round(comp_matrix, 2))
122
+
123
+ roots = np.linalg.eigvals(comp_matrix)
124
+
125
+ print("\nRoots from eigenvalues of companion matrix:")
126
+ print(np.round(roots, 2))
127
+
128
+ # Determine plot range based on roots
129
+ max_abs_root = max(abs(roots)) if len(roots) > 0 else 1.0
130
+ plot_limit = max_abs_root * 1.5
131
+
132
+ # --- Real-space plot ---
133
+ x_real = np.linspace(-plot_limit, plot_limit, 400)
134
+ y_real = poly_func(x_real)
135
+
136
+ fig_real = go.Figure()
137
+ fig_real.add_trace(go.Scatter(x=x_real, y=y_real, mode='lines', name='f(x)'))
138
+ fig_real.add_hline(y=0, line_dash='dash', line_color='black', annotation_text='x-axis')
139
+
140
+ # Find real roots (imaginary part near 0)
141
+ real_roots = [r.real for r in roots if abs(r.imag) < 1e-6]
142
+ if real_roots:
143
+ fig_real.add_trace(go.Scatter(x=real_roots, y=[0]*len(real_roots), mode='markers', marker=dict(color='red', size=10), name='Real Roots'))
144
+
145
+ fig_real.update_layout(title=f"Real Space Plot: Roots of Order {n} Polynomial", xaxis_title='x', yaxis_title='f(x)')
146
+ fig_real.show()
147
+
148
+ # Plot using complex_plot_with_shadow
149
+ title = f"Complex Space Plot: Roots of Order {n} Polynomial"
150
+ complex_plot_with_shadow(poly_func, title=title, roots=roots, plot_range=(-plot_limit, plot_limit))
151
+
152
+
153
+ def plot_secant(f, x0, x1, tolerance=1e-6, max_iterations=100, title='Secant Method'):
154
+ import plotly.graph_objects as go
155
+ import plotly.express as px
156
+ import numpy as np
157
+
158
+ x_values = [x0, x1]
159
+ for i in range(max_iterations):
160
+ denom = f(x1) - f(x0)
161
+ if denom == 0:
162
+ break
163
+ x_new = x1 - f(x1) * (x1 - x0) / denom
164
+ x_values.append(x_new)
165
+ if abs(f(x_new)) < tolerance:
166
+ break
167
+ x0 = x1
168
+ x1 = x_new
169
+
170
+ root = x_values[-1] if abs(f(x_values[-1])) < tolerance else None
171
+
172
+ # Determine plot bounds
173
+ min_x = min(x_values)
174
+ max_x = max(x_values)
175
+ padding = (max_x - min_x) * 0.2 if max_x > min_x else 0.5
176
+ x = np.linspace(min_x - padding, max_x + padding, 100)
177
+
178
+ fig = go.Figure()
179
+ fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines', name='f(x)'))
180
+ fig.add_trace(go.Scatter(x=[x_values[0], x_values[1]], y=[f(x_values[0]), f(x_values[1])], mode='markers', name='Initial guesses'))
181
+
182
+ colors = px.colors.qualitative.Plotly
183
+
184
+ for i in range(1, len(x_values) - 1):
185
+ # Secant line intersecting x-axis
186
+ fig.add_trace(go.Scatter(x=[x_values[i-1], x_values[i], x_values[i+1]], y=[f(x_values[i-1]), f(x_values[i]), 0], mode='lines', line=dict(dash='dash', color='gray'), showlegend=False))
187
+ # Vertical dotted line to the curve
188
+ fig.add_trace(go.Scatter(x=[x_values[i+1], x_values[i+1]], y=[0, f(x_values[i+1])], mode='lines', line=dict(dash='dot', color='gray'), showlegend=False))
189
+
190
+ color = colors[(i-1) % len(colors)]
191
+ fig.add_trace(go.Scatter(x=[x_values[i+1]], y=[0], mode='markers', marker=dict(size=10, color=color), name=f'x_{i+1}'))
192
+
193
+ if root:
194
+ fig.add_trace(go.Scatter(x=[root], y=[0], mode='markers', marker=dict(color='green', size=10), name='Approximate root'))
195
+
196
+ fig.update_layout(title=title, xaxis_title='x', yaxis_title='f(x)')
197
+ fig.show()
198
+ return root, x_values
199
+
200
+ def plot_newton_raphson(f, df, x0, tolerance=1e-6, max_iterations=100, title='Newton-Raphson Method', xrange=None, yrange=None):
201
+ import plotly.graph_objects as go
202
+ import plotly.express as px
203
+ import numpy as np
204
+
205
+ x_values = [x0]
206
+ for i in range(max_iterations):
207
+ d = df(x0)
208
+ if d == 0:
209
+ break
210
+ x_new = x0 - f(x0) / d
211
+ x_values.append(x_new)
212
+ if abs(f(x_new)) < tolerance or abs(x_new - x0) < tolerance:
213
+ break
214
+ x0 = x_new
215
+
216
+ root = x_values[-1] if abs(f(x_values[-1])) < tolerance or abs(x_values[-1] - x_values[-2]) < tolerance else None
217
+
218
+ # Determine plot bounds
219
+ if xrange is None:
220
+ min_x = min(x_values)
221
+ max_x = max(x_values)
222
+ padding = (max_x - min_x) * 0.2 if max_x > min_x else 0.5
223
+ x = np.linspace(min_x - padding, max_x + padding, 100)
224
+ else:
225
+ x = np.linspace(xrange[0], xrange[1], 100)
226
+
227
+ fig = go.Figure()
228
+ fig.add_trace(go.Scatter(x=x, y=f(x), mode='lines', name='f(x)'))
229
+ fig.add_trace(go.Scatter(x=[x_values[0]], y=[f(x_values[0])], mode='markers', name='Initial guess'))
230
+
231
+ colors = px.colors.qualitative.Plotly
232
+
233
+ for i in range(len(x_values) - 1):
234
+ # Tangent line intersecting x-axis
235
+ fig.add_trace(go.Scatter(x=[x_values[i], x_values[i+1]], y=[f(x_values[i]), 0], mode='lines', line=dict(dash='dash', color='gray'), showlegend=False))
236
+ # Vertical dotted line to the curve
237
+ fig.add_trace(go.Scatter(x=[x_values[i+1], x_values[i+1]], y=[0, f(x_values[i+1])], mode='lines', line=dict(dash='dot', color='gray'), showlegend=False))
238
+
239
+ color = colors[i % len(colors)]
240
+ fig.add_trace(go.Scatter(x=[x_values[i+1]], y=[0], mode='markers', marker=dict(size=10, color=color), name=f'x_{i+1}'))
241
+
242
+ if root:
243
+ fig.add_trace(go.Scatter(x=[root], y=[0], mode='markers', marker=dict(color='green', size=10), name='Approximate root'))
244
+
245
+ fig.update_layout(title=title, xaxis_title='x', yaxis_title='f(x)')
246
+ if xrange:
247
+ fig.update_layout(xaxis_range=xrange)
248
+ if yrange:
249
+ fig.update_layout(yaxis_range=yrange)
250
+
251
+ fig.show()
252
+ return root, x_values
253
+
254
+ def plot_basins_of_attraction(f, df, known_roots, title, real_range=(-2, 2), imag_range=(-2, 2), grid_size=500, th=1e-3):
255
+ import numpy as np
256
+ import plotly.graph_objects as go
257
+ from scipy.optimize import newton
258
+
259
+ real_values = np.linspace(real_range[0], real_range[1], grid_size)
260
+ imag_values = np.linspace(imag_range[0], imag_range[1], grid_size)
261
+ z_grid = np.array([[complex(r, i) for r in real_values] for i in imag_values])
262
+
263
+ roots = newton(f, z_grid, fprime=df)
264
+
265
+ colors = np.zeros((grid_size, grid_size))
266
+ for i in range(grid_size):
267
+ for j in range(grid_size):
268
+ if roots[i, j] is None:
269
+ colors[i, j] = 0
270
+ else:
271
+ for idx, r in enumerate(known_roots):
272
+ if abs(roots[i, j] - r) < th:
273
+ colors[i, j] = idx + 1
274
+ break
275
+
276
+ fig = go.Figure(data=go.Heatmap(z=colors, x=real_values, y=imag_values, colorscale='Viridis', showscale=False))
277
+ fig.update_layout(title=title, xaxis_title='Real', yaxis_title='Imaginary', width=600, height=600)
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
@@ -139,32 +144,40 @@ def backward_pass(zs, activations, y_true, lr=0.01):
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,52 @@ 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=0.005, max_steps=10000, tol=1e-5, patience=200):
212
+ """Trains the neural network iteratively until a 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 = 50
222
+ step_count = 0
223
+ best_loss = float('inf')
224
+ stagnant_count = 0
225
+ stop_reason = ""
226
+ current_loss = float('inf')
227
+
228
+ while step_count < max_steps:
229
+ for _ in range(batch_size):
230
+ zs, activations = forward_pass(X)
231
+ current_loss = backward_pass(zs, activations, y_true)
232
+ losses.append(current_loss)
233
+ step_count += batch_size
234
+
235
+ if current_loss <= target_loss:
236
+ stop_reason = f"Target loss met ({current_loss:.4e} <= {target_loss}) in {step_count} steps. Total: {len(losses)}"
237
+ break
238
+
239
+ if abs(best_loss - current_loss) < tol:
240
+ stagnant_count += batch_size
241
+ if stagnant_count >= patience:
242
+ stop_reason = f"Converged (loss change < {tol:.0e} over {patience} steps) at loss {current_loss:.4e} in {step_count} steps. Total: {len(losses)}"
243
+ break
244
+ else:
245
+ stagnant_count = 0
246
+ if current_loss < best_loss:
247
+ best_loss = current_loss
248
+
249
+ if not stop_reason:
250
+ stop_reason = f"Reached maximum of {max_steps} steps. Current loss: {current_loss:.4e}. Total: {len(losses)}"
251
+
252
+ status_label.value = stop_reason
253
+ update_plots(output_plot, metrics_plot, network_plot)
201
254
 
255
+
256
+ def draw_network(activations):
202
257
  G = nx.DiGraph()
203
258
  labels = {}
204
259
  pos = {}
@@ -242,44 +297,112 @@ def draw_network(activations):
242
297
 
243
298
 
244
299
  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)
300
+ plt.ioff()
248
301
  with output_plot:
302
+ output_plot.clear_output(wait=True)
249
303
  if true_function:
250
- plt.figure(figsize=(6, 3))
304
+ fig_out = plt.figure(figsize=(6, 3))
251
305
  y_true = true_function(X)
252
306
  _, activations = forward_pass(X2)
253
307
  y_pred = activations[-1]
254
- plt.plot(X, y_true, label='True')
255
- plt.scatter(X2, y_pred, label='NN')
308
+ plt.plot(X, y_true, label='True', color='black', linewidth=1.5)
309
+ plt.scatter(X2, y_pred, label='NN Output', color='tab:blue', s=20, alpha=0.8, zorder=3)
256
310
  plt.legend()
257
311
  plt.title("Function vs NN Output")
258
- plt.grid(True)
259
- plt.show()
312
+ plt.grid(True, linestyle='--', alpha=0.6)
313
+ plt.tight_layout()
314
+ display(fig_out)
315
+ plt.close(fig_out)
260
316
 
261
317
  with metrics_plot:
262
318
  if losses:
263
- plt.figure(figsize=(6, 3))
264
- plt.plot(losses, label="Loss", color='red')
319
+ metrics_plot.clear_output(wait=True)
320
+ fig_met = plt.figure(figsize=(6, 3))
321
+ stride = max(1, len(losses) // 500)
322
+ indices = list(range(0, len(losses), stride))
323
+ loss_sub = [losses[idx] for idx in indices]
324
+ plt.plot(indices, loss_sub, label="Loss", color='red', linewidth=1.5, zorder=4)
265
325
  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)
326
+ sub_w = [history[idx] for idx in indices]
327
+ plt.plot(indices, sub_w, label=f"W{i}_0", alpha=0.5)
268
328
  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)
329
+ sub_b = [history[idx] for idx in indices]
330
+ plt.plot(indices, sub_b, label=f"b{i}_0", linestyle='dotted', alpha=0.5)
271
331
  plt.title("Loss and Parameter Changes")
332
+ plt.grid(True, linestyle='--', alpha=0.6)
333
+ plt.legend(loc='upper right', fontsize=8)
334
+ plt.tight_layout()
335
+ display(fig_met)
336
+ plt.close(fig_met)
337
+ else:
338
+ metrics_plot.clear_output(wait=False)
272
339
 
273
- plt.grid(True)
274
- plt.legend()
275
- plt.show()
276
340
  with network_plot:
341
+ network_plot.clear_output(wait=True)
277
342
  if true_function:
278
343
  _, activations = forward_pass(X)
344
+ fig_net = plt.figure(figsize=(7, 3.5))
279
345
  G, pos, labels, edge_labels, edge_colors = draw_network(activations)
280
346
  edge_color_vals = [edge_colors.get(edge, '#888888') for edge in G.edges()]
281
347
  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)
348
+ edge_color=edge_color_vals, arrows=True, font_size=9)
283
349
  nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels, font_size=8)
284
350
  plt.title("Neural Network Diagram")
285
- plt.show()
351
+ display(fig_net)
352
+ plt.close(fig_net)
353
+
354
+
355
+ def interactive_neural_network():
356
+ """Constructs and displays the interactive neural network training widget."""
357
+ import ipywidgets as widgets
358
+ from ipywidgets import VBox, HBox, Button, Text, Label, Output
359
+
360
+ plt.ioff()
361
+ output_plot = Output()
362
+ network_plot = Output(layout={"height": "500px", "overflow": "auto"})
363
+ metrics_plot = Output()
364
+ status_label = Label(value="Press 'Step' to begin training.")
365
+
366
+ function_input = Text(value='exp(-x/2)*sin(2*pi*x)', description='f(x):')
367
+ save_button = Button(description='Save Function', button_style='success')
368
+ reset_button = Button(description='Reset Model', button_style='warning')
369
+
370
+ step_label = Label(value="Step:")
371
+ step_1 = Button(description='1')
372
+ step_10 = Button(description='10')
373
+ step_100 = Button(description='100')
374
+ step_1000 = Button(description='1000')
375
+ step_10000 = Button(description='10000')
376
+ train_converge_btn = Button(description='Train to Convergence', button_style='primary', tooltip='Trains iteratively until target loss (<=0.005) or plateau is reached')
377
+
378
+ depth_label = Label(value="Change Depth (layers):")
379
+ depth_add = Button(description='+1')
380
+ depth_sub = Button(description='-1')
381
+ width_label = Label(value="Change Width (neurons):")
382
+ width_add = Button(description='+1')
383
+ width_sub = Button(description='-1')
384
+
385
+ save_button.on_click(lambda b: save_function(function_input, output_plot, metrics_plot, network_plot, status_label))
386
+ reset_button.on_click(lambda b: reset_model(output_plot, metrics_plot, network_plot, status_label))
387
+ step_1.on_click(lambda b: step(1, output_plot, metrics_plot, network_plot, status_label))
388
+ step_10.on_click(lambda b: step(10, output_plot, metrics_plot, network_plot, status_label))
389
+ step_100.on_click(lambda b: step(100, output_plot, metrics_plot, network_plot, status_label))
390
+ step_1000.on_click(lambda b: step(1000, output_plot, metrics_plot, network_plot, status_label))
391
+ step_10000.on_click(lambda b: step(10000, output_plot, metrics_plot, network_plot, status_label))
392
+ train_converge_btn.on_click(lambda b: train_until_convergence(output_plot, metrics_plot, network_plot, status_label))
393
+ depth_add.on_click(lambda b: change_depth(1, output_plot, metrics_plot, network_plot, status_label))
394
+ depth_sub.on_click(lambda b: change_depth(-1, output_plot, metrics_plot, network_plot, status_label))
395
+ width_add.on_click(lambda b: change_width(1, output_plot, metrics_plot, network_plot, status_label))
396
+ width_sub.on_click(lambda b: change_width(-1, output_plot, metrics_plot, network_plot, status_label))
397
+
398
+ top_controls = HBox([function_input, save_button, reset_button])
399
+ step_controls = HBox([step_label, step_1, step_10, step_100, step_1000, step_10000, train_converge_btn])
400
+ depth_controls = HBox([depth_label, depth_sub, depth_add])
401
+ width_controls = HBox([width_label, width_sub, width_add])
402
+ graph_row = HBox([output_plot, metrics_plot])
403
+ full_ui = VBox([top_controls, status_label, step_controls, depth_controls, width_controls, network_plot, graph_row])
404
+ display(full_ui)
405
+
406
+ init_model()
407
+ save_function(function_input, output_plot, metrics_plot, network_plot, status_label)
408
+
@@ -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.42
3
+ Version: 0.1.44
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.42",
9
+ version="0.1.44",
10
10
  description="Demonstrative tools for the Executable Engineering textbook",
11
11
  long_description=long_description,
12
12
  long_description_content_type="text/markdown",
@@ -1,151 +0,0 @@
1
- import numpy as np
2
-
3
- def weierstrass(x, a=0.5, b=13, iterations=100):
4
- """
5
- Computes the Weierstrass function.
6
-
7
- Parameters:
8
- x (array-like): The points at which to evaluate the function.
9
- a (float): Parameter 0 < a < 1.
10
- b (float): Parameter such that ab > 1 + 3pi/2.
11
- iterations (int): Number of terms in the sum.
12
-
13
- Returns:
14
- y (ndarray): The function values.
15
- """
16
- x = np.asarray(x)
17
- y = np.zeros_like(x, dtype=float)
18
- for n in range(iterations):
19
- y += (a**n) * np.cos((b**n) * np.pi * x)
20
- return y
21
-
22
- import plotly.graph_objects as go
23
- from scipy.optimize import root
24
-
25
- def plot_and_find_roots(func):
26
- x = np.linspace(-10, 10, 400)
27
- y = func(x)
28
-
29
- # Find roots
30
- x0s = np.arange(-10, 10, 1)
31
- roots = []
32
- for x0 in x0s:
33
- r = root(func, x0=x0)
34
- if r.success:
35
- roots.append((r.x[0], r.fun[0]))
36
-
37
- # Remove duplicate roots (within tolerance)
38
- unique_roots = []
39
- tol = 1e-6
40
- for rx, ry in roots:
41
- if not any(abs(rx - ux) < tol for ux, _ in unique_roots):
42
- unique_roots.append((rx, ry))
43
-
44
- # Plot with Plotly
45
- fig = go.Figure()
46
- fig.add_trace(go.Scatter(x=x, y=y, mode='lines', name='f(x)'))
47
- fig.add_hline(y=0, line_dash='dash', line_color='black', annotation_text='x-axis')
48
- if unique_roots:
49
- rx_vals = [rx for rx, _ in unique_roots]
50
- fig.add_trace(go.Scatter(x=rx_vals, y=[0]*len(unique_roots), mode='markers', marker=dict(color='red', size=10), name='Roots'))
51
- fig.update_layout(title='Plot of f(x) and its Roots', xaxis_title='x', yaxis_title='f(x)', xaxis_range=[-10, 10], yaxis_range=[-10, 10])
52
- fig.show()
53
-
54
- def complex_plot_with_shadow(func, title='Complex Plot of f(z)', roots=None, plot_range=(-3, 3)):
55
- real_range = np.linspace(plot_range[0], plot_range[1], 100)
56
- imag_range = np.linspace(plot_range[0], plot_range[1], 100)
57
- abs_val = np.empty((len(real_range), len(imag_range)))
58
-
59
- for i, real in enumerate(real_range):
60
- for j, imag in enumerate(imag_range):
61
- z = complex(real, imag)
62
- result = func(z)
63
- abs_val[i, j] = abs(result)
64
-
65
- surface = go.Surface(
66
- z=abs_val.T,
67
- x=real_range,
68
- y=imag_range,
69
- colorscale='Viridis',
70
- contours=dict(z=dict(show=True, usecolormap=True, highlightcolor="limegreen", project_z=True))
71
- )
72
-
73
- data = [surface]
74
-
75
- if roots is not None:
76
- root_x = [r.real for r in roots]
77
- root_y = [r.imag for r in roots]
78
- # Calculate z for roots (which is abs(func(r)) = 0)
79
- root_z = [0 for r in roots]
80
-
81
- data.append(go.Scatter3d(
82
- x=root_x, y=root_y, z=root_z,
83
- mode='markers',
84
- marker=dict(color='red', size=5, symbol='circle'),
85
- name='Roots'
86
- ))
87
-
88
- fig = go.Figure(data=data)
89
- fig.update_layout(
90
- title=title,
91
- scene=dict(xaxis_title='Real', yaxis_title='Imaginary', zaxis_title='|f(z)|')
92
- )
93
- fig.show()
94
-
95
-
96
- from scipy.linalg import companion
97
-
98
- def plot_polynomial_roots_companion(n):
99
- """
100
- Creates a random polynomial of order n, solves for its roots
101
- using the companion matrix method, and plots the polynomial
102
- along with the roots as a complex surface with a shadow.
103
- """
104
- # Create random coefficients from -10 to 10
105
- # A polynomial of order n has n+1 coefficients. Let's make the leading coefficient non-zero.
106
- coeffs = np.random.uniform(-10, 10, n + 1)
107
-
108
- # Make sure leading coefficient is not too small
109
- if abs(coeffs[0]) < 1:
110
- coeffs[0] = 1.0 * np.sign(coeffs[0]) if coeffs[0] != 0 else 1.0
111
-
112
- poly_func = np.poly1d(coeffs)
113
-
114
- print("Polynomial:")
115
- print(poly_func)
116
-
117
- # Solve for roots using companion matrix
118
- comp_matrix = companion(coeffs)
119
-
120
- print("\nCompanion Matrix:")
121
- print(np.round(comp_matrix, 2))
122
-
123
- roots = np.linalg.eigvals(comp_matrix)
124
-
125
- print("\nRoots from eigenvalues of companion matrix:")
126
- print(np.round(roots, 2))
127
-
128
- # Determine plot range based on roots
129
- max_abs_root = max(abs(roots)) if len(roots) > 0 else 1.0
130
- plot_limit = max_abs_root * 1.5
131
-
132
- # --- Real-space plot ---
133
- x_real = np.linspace(-plot_limit, plot_limit, 400)
134
- y_real = poly_func(x_real)
135
-
136
- fig_real = go.Figure()
137
- fig_real.add_trace(go.Scatter(x=x_real, y=y_real, mode='lines', name='f(x)'))
138
- fig_real.add_hline(y=0, line_dash='dash', line_color='black', annotation_text='x-axis')
139
-
140
- # Find real roots (imaginary part near 0)
141
- real_roots = [r.real for r in roots if abs(r.imag) < 1e-6]
142
- if real_roots:
143
- fig_real.add_trace(go.Scatter(x=real_roots, y=[0]*len(real_roots), mode='markers', marker=dict(color='red', size=10), name='Real Roots'))
144
-
145
- fig_real.update_layout(title=f"Real Space Plot: Roots of Order {n} Polynomial", xaxis_title='x', yaxis_title='f(x)')
146
- fig_real.show()
147
-
148
- # Plot using complex_plot_with_shadow
149
- title = f"Complex Space Plot: Roots of Order {n} Polynomial"
150
- complex_plot_with_shadow(poly_func, title=title, roots=roots, plot_range=(-plot_limit, plot_limit))
151
-