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.
- {executable_engineering-0.1.42 → executable_engineering-0.1.44}/PKG-INFO +1 -1
- {executable_engineering-0.1.42 → executable_engineering-0.1.44}/executable_engineering/__init__.py +16 -4
- {executable_engineering-0.1.42 → executable_engineering-0.1.44}/executable_engineering/closedoptimization_module.py +136 -0
- executable_engineering-0.1.44/executable_engineering/constrainedoptimization_module.py +160 -0
- executable_engineering-0.1.44/executable_engineering/functions_module.py +292 -0
- {executable_engineering-0.1.42 → executable_engineering-0.1.44}/executable_engineering/neuralnet_module.py +154 -31
- {executable_engineering-0.1.42 → executable_engineering-0.1.44}/executable_engineering/openoptimization_module.py +135 -0
- {executable_engineering-0.1.42 → executable_engineering-0.1.44}/executable_engineering/optimization_module.py +6 -15
- {executable_engineering-0.1.42 → executable_engineering-0.1.44}/executable_engineering.egg-info/PKG-INFO +1 -1
- {executable_engineering-0.1.42 → executable_engineering-0.1.44}/executable_engineering.egg-info/SOURCES.txt +1 -0
- {executable_engineering-0.1.42 → executable_engineering-0.1.44}/setup.py +1 -1
- executable_engineering-0.1.42/executable_engineering/functions_module.py +0 -151
- {executable_engineering-0.1.42 → executable_engineering-0.1.44}/LICENSE +0 -0
- {executable_engineering-0.1.42 → executable_engineering-0.1.44}/README.md +0 -0
- {executable_engineering-0.1.42 → executable_engineering-0.1.44}/executable_engineering/closedrootfinder_module.py +0 -0
- {executable_engineering-0.1.42 → executable_engineering-0.1.44}/executable_engineering/gradientoptimization_module.py +0 -0
- {executable_engineering-0.1.42 → executable_engineering-0.1.44}/executable_engineering/linearsystems_module.py +0 -0
- {executable_engineering-0.1.42 → executable_engineering-0.1.44}/executable_engineering/numerical_error_module.py +0 -0
- {executable_engineering-0.1.42 → executable_engineering-0.1.44}/executable_engineering/openrootfinder_module.py +0 -0
- {executable_engineering-0.1.42 → executable_engineering-0.1.44}/executable_engineering.egg-info/dependency_links.txt +0 -0
- {executable_engineering-0.1.42 → executable_engineering-0.1.44}/executable_engineering.egg-info/requires.txt +0 -0
- {executable_engineering-0.1.42 → executable_engineering-0.1.44}/executable_engineering.egg-info/top_level.txt +0 -0
- {executable_engineering-0.1.42 → executable_engineering-0.1.44}/pyproject.toml +0 -0
- {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.
|
|
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
|
{executable_engineering-0.1.42 → executable_engineering-0.1.44}/executable_engineering/__init__.py
RENAMED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
156
|
-
|
|
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:
|
|
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
|
-
|
|
167
|
-
losses.append(
|
|
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
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
264
|
-
plt.
|
|
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
|
-
|
|
267
|
-
plt.plot(
|
|
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
|
-
|
|
270
|
-
plt.plot(
|
|
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,
|
|
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
|
-
|
|
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=
|
|
294
|
-
height=
|
|
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
|
-
|
|
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=
|
|
498
|
-
height=
|
|
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.
|
|
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.
|
|
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
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|