executable-engineering 0.1.29__py3-none-any.whl
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/__init__.py +32 -0
- executable_engineering/closedoptimization_module.py +282 -0
- executable_engineering/closedrootfinder_module.py +155 -0
- executable_engineering/gradientoptimization_module.py +169 -0
- executable_engineering/neuralnet_module.py +285 -0
- executable_engineering/openoptimization_module.py +231 -0
- executable_engineering/openrootfinder_module.py +157 -0
- executable_engineering-0.1.29.dist-info/METADATA +55 -0
- executable_engineering-0.1.29.dist-info/RECORD +12 -0
- executable_engineering-0.1.29.dist-info/WHEEL +5 -0
- executable_engineering-0.1.29.dist-info/licenses/LICENSE +21 -0
- executable_engineering-0.1.29.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# Neural network model UI and plotting
|
|
2
|
+
import ipywidgets as widgets
|
|
3
|
+
import matplotlib.pyplot as plt
|
|
4
|
+
import networkx as nx
|
|
5
|
+
import numpy as np
|
|
6
|
+
import plotly.graph_objs as go
|
|
7
|
+
|
|
8
|
+
def init_weights():
|
|
9
|
+
global W1, b1, W2, b2
|
|
10
|
+
W1 = np.random.randn(4,1) * 0.5
|
|
11
|
+
b1 = np.random.randn(4,1) * 0.5
|
|
12
|
+
W2 = np.random.randn(1,4) * 0.5
|
|
13
|
+
b2 = np.random.randn(1,1) * 0.5
|
|
14
|
+
|
|
15
|
+
def tanh(x): return np.tanh(x)
|
|
16
|
+
def tanh_derivative(x): return 1 - np.tanh(x)**2
|
|
17
|
+
|
|
18
|
+
np.random.seed(42)
|
|
19
|
+
|
|
20
|
+
init_weights()
|
|
21
|
+
loss_history = []
|
|
22
|
+
|
|
23
|
+
depth = 1
|
|
24
|
+
width = 3
|
|
25
|
+
activation = np.tanh
|
|
26
|
+
activation_derivative = lambda x: 1 - np.tanh(x) ** 2
|
|
27
|
+
|
|
28
|
+
X = np.linspace(0, 3, 50).reshape(-1, 1)
|
|
29
|
+
X2 = np.linspace(0, 3.5, 50).reshape(-1, 1)
|
|
30
|
+
true_function = None
|
|
31
|
+
losses = []
|
|
32
|
+
weights, biases = [], []
|
|
33
|
+
weight_history, bias_history = [], []
|
|
34
|
+
|
|
35
|
+
def forward(X):
|
|
36
|
+
Z1 = W1 @ X + b1
|
|
37
|
+
A1 = tanh(Z1)
|
|
38
|
+
Z2 = W2 @ A1 + b2
|
|
39
|
+
A2 = Z2
|
|
40
|
+
return Z1, A1, Z2, A2
|
|
41
|
+
|
|
42
|
+
def compute_loss(A2, y): return np.mean((A2 - y)**2)
|
|
43
|
+
|
|
44
|
+
def backward(X, y, Z1, A1, A2, lr=0.1):
|
|
45
|
+
global W1, b1, W2, b2
|
|
46
|
+
m = X.shape[1]
|
|
47
|
+
dZ2 = (A2 - y) / m
|
|
48
|
+
dW2 = dZ2 @ A1.T
|
|
49
|
+
db2 = np.sum(dZ2, axis=1, keepdims=True)
|
|
50
|
+
dA1 = W2.T @ dZ2
|
|
51
|
+
dZ1 = dA1 * tanh_derivative(Z1)
|
|
52
|
+
dW1 = dZ1 @ X.T
|
|
53
|
+
db1 = np.sum(dZ1, axis=1, keepdims=True)
|
|
54
|
+
W2 -= lr * dW2
|
|
55
|
+
b2 -= lr * db2
|
|
56
|
+
W1 -= lr * dW1
|
|
57
|
+
b1 -= lr * db1
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def plot_nn_diagram():
|
|
61
|
+
layer_x = [0, 2, 4]
|
|
62
|
+
y_positions = [[0], [-3, -1, 1, 3], [0]]
|
|
63
|
+
annotations = []
|
|
64
|
+
shapes = []
|
|
65
|
+
layer_titles = ['x', 'Hidden Layer', 'f(x)']
|
|
66
|
+
|
|
67
|
+
for l, ys in enumerate(y_positions):
|
|
68
|
+
for i, y in enumerate(ys):
|
|
69
|
+
shapes.append(dict(type="circle", xref="x", yref="y",
|
|
70
|
+
x0=layer_x[l]-0.2, x1=layer_x[l]+0.2,
|
|
71
|
+
y0=y-0.2, y1=y+0.2, line_color="black"))
|
|
72
|
+
label = 'x' if l==0 else ('y' if l==2 else f'h{i+1}')
|
|
73
|
+
annotations.append(dict(x=layer_x[l], y=y, text=label,
|
|
74
|
+
showarrow=False, font=dict(size=12)))
|
|
75
|
+
if l == 1:
|
|
76
|
+
bias = b1[i, 0]
|
|
77
|
+
annotations.append(dict(x=layer_x[l]+0.3, y=y, text=f"b={bias:.2f}",
|
|
78
|
+
showarrow=False, font=dict(size=10, color='gray')))
|
|
79
|
+
elif l == 2:
|
|
80
|
+
annotations.append(dict(x=layer_x[l]+0.3, y=y, text=f"b={b2[0,0]:.2f}",
|
|
81
|
+
showarrow=False, font=dict(size=10, color='gray')))
|
|
82
|
+
|
|
83
|
+
for i, y_in in enumerate(y_positions[0]):
|
|
84
|
+
for j, y_hid in enumerate(y_positions[1]):
|
|
85
|
+
weight = W1[j, i]
|
|
86
|
+
shapes.append(dict(type='line', xref='x', yref='y',
|
|
87
|
+
x0=layer_x[0]+0.2, y0=y_in, x1=layer_x[1]-0.2, y1=y_hid,
|
|
88
|
+
line=dict(color='blue')))
|
|
89
|
+
annotations.append(dict(x=1, y=(y_in + y_hid)/2, text=f"{weight:.2f}",
|
|
90
|
+
showarrow=False, font=dict(size=9, color='blue')))
|
|
91
|
+
|
|
92
|
+
for j, y_hid in enumerate(y_positions[1]):
|
|
93
|
+
for k, y_out in enumerate(y_positions[2]):
|
|
94
|
+
weight = W2[k, j]
|
|
95
|
+
shapes.append(dict(type='line', xref='x', yref='y',
|
|
96
|
+
x0=layer_x[1]+0.2, y0=y_hid, x1=layer_x[2]-0.2, y1=y_out,
|
|
97
|
+
line=dict(color='red')))
|
|
98
|
+
annotations.append(dict(x=3, y=(y_hid + y_out)/2, text=f"{weight:.2f}",
|
|
99
|
+
showarrow=False, font=dict(size=9, color='red')))
|
|
100
|
+
|
|
101
|
+
for i, x in enumerate(layer_x):
|
|
102
|
+
annotations.append(dict(x=x, y=max(y_positions[1])+1.5,
|
|
103
|
+
text=layer_titles[i], showarrow=False, font=dict(size=14)))
|
|
104
|
+
|
|
105
|
+
fig = go.Figure()
|
|
106
|
+
fig.update_layout(shapes=shapes, annotations=annotations,
|
|
107
|
+
xaxis=dict(visible=False), yaxis=dict(visible=False),
|
|
108
|
+
title='Neural Network Structure', height=500, width=700,
|
|
109
|
+
margin=dict(l=20, r=20, t=40, b=20))
|
|
110
|
+
fig.show()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def init_model():
|
|
115
|
+
global weights, biases, weight_history, bias_history
|
|
116
|
+
layers = [1] + [width]*depth + [1]
|
|
117
|
+
weights = [np.random.randn(layers[i], layers[i+1]) * np.sqrt(2 / layers[i]) for i in range(len(layers)-1)]
|
|
118
|
+
biases = [np.zeros((1, layers[i+1])) for i in range(len(layers)-1)]
|
|
119
|
+
weight_history = [[] for _ in weights]
|
|
120
|
+
bias_history = [[] for _ in biases]
|
|
121
|
+
|
|
122
|
+
def forward_pass(x):
|
|
123
|
+
activations = [x]
|
|
124
|
+
zs = []
|
|
125
|
+
a = x
|
|
126
|
+
for w, b in zip(weights[:-1], biases[:-1]):
|
|
127
|
+
z = a @ w + b
|
|
128
|
+
zs.append(z)
|
|
129
|
+
a = activation(z)
|
|
130
|
+
activations.append(a)
|
|
131
|
+
z = a @ weights[-1] + biases[-1]
|
|
132
|
+
zs.append(z)
|
|
133
|
+
activations.append(z) # no activation on final output
|
|
134
|
+
return zs, activations
|
|
135
|
+
|
|
136
|
+
def backward_pass(zs, activations, y_true, lr=0.01):
|
|
137
|
+
global weights, biases, weight_history, bias_history
|
|
138
|
+
grads_w = [None] * len(weights)
|
|
139
|
+
grads_b = [None] * len(biases)
|
|
140
|
+
|
|
141
|
+
delta = (activations[-1] - y_true)
|
|
142
|
+
grads_w[-1] = activations[-2].T @ delta / len(X)
|
|
143
|
+
grads_b[-1] = np.mean(delta, axis=0, keepdims=True)
|
|
144
|
+
|
|
145
|
+
for l in range(2, len(weights)+1):
|
|
146
|
+
z = zs[-l]
|
|
147
|
+
sp = activation_derivative(z)
|
|
148
|
+
delta = (delta @ weights[-l+1].T) * sp
|
|
149
|
+
grads_w[-l] = activations[-l-1].T @ delta / len(X)
|
|
150
|
+
grads_b[-l] = np.mean(delta, axis=0, keepdims=True)
|
|
151
|
+
|
|
152
|
+
for i in range(len(weights)):
|
|
153
|
+
weights[i] -= lr * grads_w[i]
|
|
154
|
+
biases[i] -= lr * grads_b[i]
|
|
155
|
+
weight_history[i].append(weights[i].copy())
|
|
156
|
+
bias_history[i].append(biases[i].copy())
|
|
157
|
+
|
|
158
|
+
return np.mean((activations[-1] - y_true)**2)
|
|
159
|
+
|
|
160
|
+
def step(n,output_plot, metrics_plot, network_plot):
|
|
161
|
+
global losses
|
|
162
|
+
if true_function is None: return
|
|
163
|
+
y_true = true_function(X)
|
|
164
|
+
for _ in range(n):
|
|
165
|
+
zs, activations = forward_pass(X)
|
|
166
|
+
loss = backward_pass(zs, activations, y_true)
|
|
167
|
+
losses.append(loss)
|
|
168
|
+
update_plots(output_plot, metrics_plot, network_plot)
|
|
169
|
+
|
|
170
|
+
def reset_model(output_plot, metrics_plot, network_plot, status_label):
|
|
171
|
+
init_model()
|
|
172
|
+
losses.clear()
|
|
173
|
+
status_label.value = "Model reset."
|
|
174
|
+
update_plots(output_plot, metrics_plot, network_plot)
|
|
175
|
+
|
|
176
|
+
def save_function(function_input, output_plot, metrics_plot, network_plot, status_label):
|
|
177
|
+
global true_function, losses
|
|
178
|
+
try:
|
|
179
|
+
code = function_input.value
|
|
180
|
+
true_function = lambda x: eval(code, {"x": x, "np": np, "sin": np.sin, "cos": np.cos, "exp": np.exp, "pi": np.pi})
|
|
181
|
+
losses.clear()
|
|
182
|
+
status_label.value = "Function saved."
|
|
183
|
+
update_plots(output_plot, metrics_plot, network_plot)
|
|
184
|
+
except Exception as e:
|
|
185
|
+
status_label.value = f"Error: {e}"
|
|
186
|
+
|
|
187
|
+
def change_depth(d, output_plot, metrics_plot, network_plot, status_label):
|
|
188
|
+
global depth
|
|
189
|
+
depth = max(0, depth + d)
|
|
190
|
+
reset_model(output_plot, metrics_plot, network_plot, status_label)
|
|
191
|
+
|
|
192
|
+
def change_width(d, output_plot, metrics_plot, network_plot, status_label):
|
|
193
|
+
global width
|
|
194
|
+
width = max(1, width + d)
|
|
195
|
+
reset_model(output_plot, metrics_plot, network_plot, status_label)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def draw_network(activations):
|
|
199
|
+
import matplotlib.cm as cm
|
|
200
|
+
import matplotlib.colors as mcolors
|
|
201
|
+
|
|
202
|
+
G = nx.DiGraph()
|
|
203
|
+
labels = {}
|
|
204
|
+
pos = {}
|
|
205
|
+
edge_labels = {}
|
|
206
|
+
edge_colors = {}
|
|
207
|
+
layer_sizes = [1] + [width] * depth + [1]
|
|
208
|
+
|
|
209
|
+
max_layer_size = max(layer_sizes)
|
|
210
|
+
|
|
211
|
+
for l, size in enumerate(layer_sizes):
|
|
212
|
+
layer_offset = (max_layer_size - size) / 2
|
|
213
|
+
for n in range(size):
|
|
214
|
+
node = f"L{l}N{n}"
|
|
215
|
+
pos[node] = (l, -(n + layer_offset))
|
|
216
|
+
G.add_node(node)
|
|
217
|
+
|
|
218
|
+
# Label nodes
|
|
219
|
+
if l == 0:
|
|
220
|
+
labels[node] = "x"
|
|
221
|
+
elif l == len(layer_sizes) - 1:
|
|
222
|
+
labels[node] = "f(x)"
|
|
223
|
+
else:
|
|
224
|
+
try:
|
|
225
|
+
act_val = activations[l][0, n]
|
|
226
|
+
bias_val = biases[l - 1][0, n]
|
|
227
|
+
labels[node] = f"{act_val:.2f}\nb={bias_val:.2f}"
|
|
228
|
+
except Exception:
|
|
229
|
+
labels[node] = f"H{l-1}N{n}"
|
|
230
|
+
|
|
231
|
+
# Add edges
|
|
232
|
+
if l > 0:
|
|
233
|
+
for p in range(layer_sizes[l - 1]):
|
|
234
|
+
prev_node = f"L{l-1}N{p}"
|
|
235
|
+
G.add_edge(prev_node, node)
|
|
236
|
+
w_val = weights[l - 1][p, n]
|
|
237
|
+
edge_labels[(prev_node, node)] = f"{w_val:.2f}"
|
|
238
|
+
norm_val = np.tanh(w_val) # normalized for color
|
|
239
|
+
edge_colors[(prev_node, node)] = plt.cm.bwr((norm_val + 1) / 2)
|
|
240
|
+
|
|
241
|
+
return G, pos, labels, edge_labels, edge_colors
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
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)
|
|
248
|
+
with output_plot:
|
|
249
|
+
if true_function:
|
|
250
|
+
plt.figure(figsize=(6, 3))
|
|
251
|
+
y_true = true_function(X)
|
|
252
|
+
_, activations = forward_pass(X2)
|
|
253
|
+
y_pred = activations[-1]
|
|
254
|
+
plt.plot(X, y_true, label='True')
|
|
255
|
+
plt.scatter(X2, y_pred, label='NN')
|
|
256
|
+
plt.legend()
|
|
257
|
+
plt.title("Function vs NN Output")
|
|
258
|
+
plt.grid(True)
|
|
259
|
+
plt.show()
|
|
260
|
+
|
|
261
|
+
with metrics_plot:
|
|
262
|
+
if losses:
|
|
263
|
+
plt.figure(figsize=(6, 3))
|
|
264
|
+
plt.plot(losses, label="Loss", color='red')
|
|
265
|
+
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)
|
|
268
|
+
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)
|
|
271
|
+
plt.title("Loss and Parameter Changes")
|
|
272
|
+
|
|
273
|
+
plt.grid(True)
|
|
274
|
+
plt.legend()
|
|
275
|
+
plt.show()
|
|
276
|
+
with network_plot:
|
|
277
|
+
if true_function:
|
|
278
|
+
_, activations = forward_pass(X)
|
|
279
|
+
G, pos, labels, edge_labels, edge_colors = draw_network(activations)
|
|
280
|
+
edge_color_vals = [edge_colors.get(edge, '#888888') for edge in G.edges()]
|
|
281
|
+
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)
|
|
283
|
+
nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels, font_size=8)
|
|
284
|
+
plt.title("Neural Network Diagram")
|
|
285
|
+
plt.show()
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import matplotlib.pyplot as plt
|
|
3
|
+
from matplotlib.animation import FuncAnimation
|
|
4
|
+
from IPython.display import HTML, display
|
|
5
|
+
import ipywidgets as widgets
|
|
6
|
+
from scipy.optimize import minimize
|
|
7
|
+
|
|
8
|
+
class OptimizerOpen:
|
|
9
|
+
|
|
10
|
+
def __init__(self, f, x0, tol=1e-6, max_iter=300):
|
|
11
|
+
self.f = f
|
|
12
|
+
self.x0 = np.asarray(x0, dtype=float)
|
|
13
|
+
self.tol = tol
|
|
14
|
+
self.max_iter = max_iter
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
self.powell_steps = self._run_optimization('Powell')
|
|
18
|
+
self.nm_data = self._run_nelder_mead()
|
|
19
|
+
(self.nelder_mead_steps,
|
|
20
|
+
self.nelder_mead_labels,
|
|
21
|
+
self.nelder_mead_simplices,
|
|
22
|
+
self.rejected_points) = self.nm_data
|
|
23
|
+
|
|
24
|
+
self._define_plot_domain()
|
|
25
|
+
|
|
26
|
+
def _run_optimization(self, method):
|
|
27
|
+
steps = []
|
|
28
|
+
def callback(xk):
|
|
29
|
+
steps.append(xk.copy())
|
|
30
|
+
res = minimize(self.f, self.x0, method=method, callback=callback,
|
|
31
|
+
options={'maxiter': self.max_iter})
|
|
32
|
+
if not steps or not np.allclose(steps[0], self.x0):
|
|
33
|
+
steps.insert(0, self.x0.copy())
|
|
34
|
+
return [tuple(s) for s in steps]
|
|
35
|
+
|
|
36
|
+
def _run_nelder_mead(self):
|
|
37
|
+
α, γ, ρ, σ = 1.0, 2.0, 0.5, 0.5
|
|
38
|
+
x0 = self.x0
|
|
39
|
+
n = len(x0)
|
|
40
|
+
simplex = [x0]
|
|
41
|
+
for i in range(n):
|
|
42
|
+
e = np.zeros(n)
|
|
43
|
+
e[i] = 0.05 if x0[i] == 0 else 0.05 * x0[i]
|
|
44
|
+
simplex.append(x0 + e)
|
|
45
|
+
simplex = np.array(simplex)
|
|
46
|
+
fvals = np.array([self.f(v) for v in simplex])
|
|
47
|
+
|
|
48
|
+
steps = [x0.copy()]
|
|
49
|
+
labels = ["Initial simplex"]
|
|
50
|
+
simplices = [simplex.copy()]
|
|
51
|
+
rejected_points = [None]
|
|
52
|
+
|
|
53
|
+
for _ in range(self.max_iter):
|
|
54
|
+
order = np.argsort(fvals)
|
|
55
|
+
simplex, fvals = simplex[order], fvals[order]
|
|
56
|
+
centroid = np.mean(simplex[:-1], axis=0)
|
|
57
|
+
|
|
58
|
+
xr = centroid + α * (centroid - simplex[-1])
|
|
59
|
+
fr = self.f(xr)
|
|
60
|
+
|
|
61
|
+
label = None
|
|
62
|
+
rejected_point = None
|
|
63
|
+
|
|
64
|
+
if fvals[0] <= fr < fvals[-2]:
|
|
65
|
+
simplex[-1], fvals[-1] = xr, fr
|
|
66
|
+
label = "Reflection accepted"
|
|
67
|
+
elif fr < fvals[0]:
|
|
68
|
+
xe = centroid + γ * (xr - centroid)
|
|
69
|
+
fe = self.f(xe)
|
|
70
|
+
if fe < fr:
|
|
71
|
+
simplex[-1], fvals[-1] = xe, fe
|
|
72
|
+
label = "Reflection accepted → Expansion accepted"
|
|
73
|
+
rejected_point = xr
|
|
74
|
+
else:
|
|
75
|
+
simplex[-1], fvals[-1] = xr, fr
|
|
76
|
+
label = "Expansion rejected → Reflection accepted"
|
|
77
|
+
rejected_point = xe
|
|
78
|
+
else:
|
|
79
|
+
xc = centroid + ρ * (simplex[-1] - centroid)
|
|
80
|
+
fc = self.f(xc)
|
|
81
|
+
if fc < fvals[-1]:
|
|
82
|
+
simplex[-1], fvals[-1] = xc, fc
|
|
83
|
+
label = "Reflection rejected → Contraction accepted"
|
|
84
|
+
rejected_point = xr
|
|
85
|
+
else:
|
|
86
|
+
rejected_point = xc
|
|
87
|
+
best = simplex[0]
|
|
88
|
+
for j in range(1, len(simplex)):
|
|
89
|
+
simplex[j] = best + σ * (simplex[j] - best)
|
|
90
|
+
fvals[j] = self.f(simplex[j])
|
|
91
|
+
label = "Contraction rejected → Shrinkage accepted"
|
|
92
|
+
|
|
93
|
+
steps.append(centroid.copy())
|
|
94
|
+
labels.append(label)
|
|
95
|
+
simplices.append(simplex.copy())
|
|
96
|
+
rejected_points.append(rejected_point.copy() if rejected_point is not None else None)
|
|
97
|
+
|
|
98
|
+
if np.std(fvals) < self.tol:
|
|
99
|
+
break
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
[tuple(s) for s in steps],
|
|
103
|
+
labels,
|
|
104
|
+
simplices,
|
|
105
|
+
rejected_points
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def _define_plot_domain(self):
|
|
109
|
+
all_points = np.array(self.powell_steps + self.nelder_mead_steps)
|
|
110
|
+
x_min, y_min = np.min(all_points, axis=0)
|
|
111
|
+
x_max, y_max = np.max(all_points, axis=0)
|
|
112
|
+
dx, dy = (x_max - x_min) * 0.3, (y_max - y_min) * 0.3
|
|
113
|
+
x_min, x_max = x_min - dx, x_max + dx
|
|
114
|
+
y_min, y_max = y_min - dy, y_max + dy
|
|
115
|
+
self.x = np.linspace(x_min, x_max, 150)
|
|
116
|
+
self.y = np.linspace(y_min, y_max, 150)
|
|
117
|
+
self.X, self.Y = np.meshgrid(self.x, self.y)
|
|
118
|
+
self.Z = np.array([[self.f([xi, yi]) for xi in self.x] for yi in self.y])
|
|
119
|
+
|
|
120
|
+
def _get_color_for_rejection(self, label):
|
|
121
|
+
if "Reflection rejected" in label:
|
|
122
|
+
return "blue"
|
|
123
|
+
elif "Expansion rejected" in label:
|
|
124
|
+
return "green"
|
|
125
|
+
elif "Contraction rejected" in label:
|
|
126
|
+
return "orange"
|
|
127
|
+
elif "Shrinkage accepted" in label:
|
|
128
|
+
return "purple"
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
def _animate_optimization(self, steps, method_name, interval_ms=500,
|
|
132
|
+
labels=None, simplices=None, rejected_points=None, show_vectors=False):
|
|
133
|
+
if not steps:
|
|
134
|
+
return None
|
|
135
|
+
fig, ax = plt.subplots(figsize=(8,8))
|
|
136
|
+
contour = ax.contourf(self.X, self.Y, self.Z, levels=50, cmap='viridis')
|
|
137
|
+
ax.contour(self.X, self.Y, self.Z, levels=15, colors='k', alpha=0.3, linewidths=0.5)
|
|
138
|
+
cbar = fig.colorbar(contour, ax=ax)
|
|
139
|
+
cbar.set_label("f(x, y)", rotation=270, labelpad=15)
|
|
140
|
+
|
|
141
|
+
ax.plot(self.x0[0], self.x0[1], 'r*', markersize=10, label='Initial Guess')
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
path_line, = ax.plot([], [], 'r-', lw=1.5, alpha=0.7)
|
|
145
|
+
current_dot, = ax.plot([], [], 'ro', markersize=6, label='Current Point')
|
|
146
|
+
|
|
147
|
+
ax.set_xlim(self.x[0], self.x[-1])
|
|
148
|
+
ax.set_ylim(self.y[0], self.y[-1])
|
|
149
|
+
|
|
150
|
+
title_text = ax.text(0.5, 1.03, '', transform=ax.transAxes,
|
|
151
|
+
|
|
152
|
+
ha='center', fontsize=12)
|
|
153
|
+
|
|
154
|
+
indicator_box = ax.annotate(
|
|
155
|
+
'',
|
|
156
|
+
xy=(0.02, 0.98),
|
|
157
|
+
xycoords='axes fraction',
|
|
158
|
+
textcoords='axes fraction',
|
|
159
|
+
ha='left',
|
|
160
|
+
va='top',
|
|
161
|
+
fontsize=10,
|
|
162
|
+
bbox=dict(boxstyle="round,pad=0.5", fc="white", alpha=0.8, ec="black")
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
ax.legend(loc='lower left')
|
|
166
|
+
simplex_lines, rejected_markers = [], []
|
|
167
|
+
quivers = []
|
|
168
|
+
|
|
169
|
+
def update(i):
|
|
170
|
+
for ln in simplex_lines:
|
|
171
|
+
ln.remove()
|
|
172
|
+
for m in rejected_markers:
|
|
173
|
+
m.remove()
|
|
174
|
+
simplex_lines.clear()
|
|
175
|
+
rejected_markers.clear()
|
|
176
|
+
|
|
177
|
+
px, py = [s[0] for s in steps[:i+1]], [s[1] for s in steps[:i+1]]
|
|
178
|
+
path_line.set_data(px, py)
|
|
179
|
+
cx, cy = steps[i]
|
|
180
|
+
current_dot.set_data([cx], [cy])
|
|
181
|
+
|
|
182
|
+
if simplices is not None and i < len(simplices):
|
|
183
|
+
tri = simplices[i]
|
|
184
|
+
tri_closed = np.vstack([tri, tri[0]])
|
|
185
|
+
ln, = ax.plot(tri_closed[:,0], tri_closed[:,1], 'w-', lw=2, alpha=0.9)
|
|
186
|
+
simplex_lines.append(ln)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
for q in quivers:
|
|
190
|
+
q.remove()
|
|
191
|
+
quivers.clear()
|
|
192
|
+
|
|
193
|
+
if show_vectors and i > 0:
|
|
194
|
+
x_prev, y_prev = steps[i-1]
|
|
195
|
+
dx, dy = current_x - x_prev, current_y - y_prev
|
|
196
|
+
q = ax.quiver(x_prev, y_prev, dx, dy, angles='xy', scale_units='xy',
|
|
197
|
+
scale=1, color='red', width=0.005)
|
|
198
|
+
quivers.append(q)
|
|
199
|
+
|
|
200
|
+
label_text = labels[i] if labels and i < len(labels) else ""
|
|
201
|
+
title_text.set_text(
|
|
202
|
+
f"{method_name} Step {i+1}/{len(steps)}\n"
|
|
203
|
+
f"({cx:.4f}, {cy:.4f}) f={self.f([cx, cy]):.4f}"
|
|
204
|
+
)
|
|
205
|
+
if method_name == "Nelder–Mead":
|
|
206
|
+
indicator_box.set_text(
|
|
207
|
+
f"Action: {label_text}\n"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
return path_line, current_dot, title_text, indicator_box, *simplex_lines, *rejected_markers
|
|
211
|
+
|
|
212
|
+
anim = FuncAnimation(fig, update, frames=len(steps),
|
|
213
|
+
interval=interval_ms, blit=True, repeat=False)
|
|
214
|
+
plt.close(fig)
|
|
215
|
+
return HTML(anim.to_jshtml())
|
|
216
|
+
|
|
217
|
+
def show_toggle_open(self, interval_ms=500):
|
|
218
|
+
powell_anim = self._animate_optimization(
|
|
219
|
+
self.powell_steps, "Powell's Method", interval_ms, show_vectors=True)
|
|
220
|
+
nm_anim = self._animate_optimization(
|
|
221
|
+
self.nelder_mead_steps, "Nelder–Mead", interval_ms,
|
|
222
|
+
labels=self.nelder_mead_labels,
|
|
223
|
+
simplices=self.nelder_mead_simplices,
|
|
224
|
+
rejected_points=self.rejected_points)
|
|
225
|
+
powell_tab = widgets.HTML(value=powell_anim.data)
|
|
226
|
+
nm_tab = widgets.HTML(value=nm_anim.data)
|
|
227
|
+
tab = widgets.Tab()
|
|
228
|
+
tab.children = [powell_tab, nm_tab]
|
|
229
|
+
tab.set_title(0, "Powell's Method")
|
|
230
|
+
tab.set_title(1, "Nelder–Mead Method")
|
|
231
|
+
display(tab)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import matplotlib.pyplot as plt
|
|
3
|
+
from matplotlib.animation import FuncAnimation
|
|
4
|
+
from IPython.display import HTML, display
|
|
5
|
+
import ipywidgets as widgets
|
|
6
|
+
|
|
7
|
+
class RootFinderOpen:
|
|
8
|
+
def __init__(self, f, fprime=None, x0=None, x1=None, tol=1e-6, max_iter=20):
|
|
9
|
+
self.f = f
|
|
10
|
+
self.fprime = fprime
|
|
11
|
+
self.x0 = x0
|
|
12
|
+
self.x1 = x1
|
|
13
|
+
self.tol = tol
|
|
14
|
+
self.max_iter = max_iter
|
|
15
|
+
self.newton_guesses = self._run_newton()
|
|
16
|
+
self.secant_guesses = self._run_secant()
|
|
17
|
+
|
|
18
|
+
def _run_newton(self):
|
|
19
|
+
if self.fprime is None or self.x0 is None:
|
|
20
|
+
return []
|
|
21
|
+
x = self.x0
|
|
22
|
+
guesses = [x]
|
|
23
|
+
for _ in range(self.max_iter):
|
|
24
|
+
fx, fpx = self.f(x), self.fprime(x)
|
|
25
|
+
if abs(fpx) < 1e-10:
|
|
26
|
+
break
|
|
27
|
+
x_new = x - fx / fpx
|
|
28
|
+
guesses.append(x_new)
|
|
29
|
+
if abs(x_new - x) < self.tol:
|
|
30
|
+
break
|
|
31
|
+
x = x_new
|
|
32
|
+
return guesses
|
|
33
|
+
|
|
34
|
+
def _run_secant(self):
|
|
35
|
+
if self.x0 is None or self.x1 is None:
|
|
36
|
+
return []
|
|
37
|
+
x0, x1 = self.x0, self.x1
|
|
38
|
+
guesses = [x0, x1]
|
|
39
|
+
for _ in range(self.max_iter):
|
|
40
|
+
f0, f1 = self.f(x0), self.f(x1)
|
|
41
|
+
if abs(f1 - f0) < 1e-10:
|
|
42
|
+
break
|
|
43
|
+
x2 = x1 - f1 * (x1 - x0) / (f1 - f0)
|
|
44
|
+
guesses.append(x2)
|
|
45
|
+
if abs(x2 - x1) < self.tol:
|
|
46
|
+
break
|
|
47
|
+
x0, x1 = x1, x2
|
|
48
|
+
return guesses
|
|
49
|
+
|
|
50
|
+
def _make_animation_open(self, method='Newton', interval_ms=750):
|
|
51
|
+
if method == 'Newton':
|
|
52
|
+
guesses = self.newton_guesses
|
|
53
|
+
title_method = "Newton's Method"
|
|
54
|
+
line_label = "Tangent Line"
|
|
55
|
+
else:
|
|
56
|
+
guesses = self.secant_guesses
|
|
57
|
+
title_method = "Secant Method"
|
|
58
|
+
line_label = "Secant Line"
|
|
59
|
+
|
|
60
|
+
if not guesses or len(guesses) < 2:
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
total_steps = len(guesses)
|
|
64
|
+
min_g, max_g = min(guesses), max(guesses)
|
|
65
|
+
|
|
66
|
+
x_span = max_g - min_g if max_g > min_g else 1.0
|
|
67
|
+
x_pad = max(0.2 * x_span, 1.0)
|
|
68
|
+
x_min, x_max = min_g - x_pad, max_g + x_pad
|
|
69
|
+
|
|
70
|
+
x_vals = np.linspace(x_min, x_max, 400)
|
|
71
|
+
y_vals = self.f(x_vals)
|
|
72
|
+
|
|
73
|
+
fig, ax = plt.subplots(figsize=(10, 6))
|
|
74
|
+
ax.plot(x_vals, y_vals, label='f(x)', color='blue')
|
|
75
|
+
ax.axhline(0, color='gray', linestyle='--', alpha=0.7)
|
|
76
|
+
|
|
77
|
+
line, = ax.plot([], [], marker='o', linestyle='-', color='red', markersize=8, label='Iterations')
|
|
78
|
+
aux_line, = ax.plot([], [], color='green', linestyle='--', label=line_label)
|
|
79
|
+
current_point, = ax.plot([], [], marker='o', color='gold', markersize=10, markeredgecolor='red', label='Current $x_k$')
|
|
80
|
+
vertical_line, = ax.plot([], [], color='purple', linestyle=':', linewidth=2, label='Next guess connector')
|
|
81
|
+
|
|
82
|
+
ax.set_title(f"{title_method} Animation", fontsize=14)
|
|
83
|
+
ax.set_xlabel("x", fontsize=12)
|
|
84
|
+
ax.set_ylabel("f(x)", fontsize=12)
|
|
85
|
+
ax.grid(True, linestyle=':', alpha=0.6)
|
|
86
|
+
ax.legend()
|
|
87
|
+
ax.set_xlim(x_min, x_max)
|
|
88
|
+
|
|
89
|
+
f_g = [self.f(g) for g in guesses]
|
|
90
|
+
y_min, y_max = min(f_g), max(f_g)
|
|
91
|
+
|
|
92
|
+
y_span = y_max - y_min if y_max > y_min else 1.0
|
|
93
|
+
y_pad = max(0.2 * y_span, 1.0)
|
|
94
|
+
ax.set_ylim(y_min - y_pad, y_max + y_pad)
|
|
95
|
+
|
|
96
|
+
def update_open(frame):
|
|
97
|
+
step = frame
|
|
98
|
+
shown_guesses = guesses[:step+1]
|
|
99
|
+
f_guesses = [self.f(g) for g in shown_guesses]
|
|
100
|
+
|
|
101
|
+
line.set_data(shown_guesses, f_guesses)
|
|
102
|
+
current_point.set_data([shown_guesses[-1]], [f_guesses[-1]])
|
|
103
|
+
aux_line.set_data([], [])
|
|
104
|
+
vertical_line.set_data([], [])
|
|
105
|
+
ax.set_title(f"{title_method} - Step {step}/{total_steps-1}", fontsize=14)
|
|
106
|
+
|
|
107
|
+
if step > 0:
|
|
108
|
+
x_k_prev = shown_guesses[step - 1]
|
|
109
|
+
f_k_prev = self.f(x_k_prev)
|
|
110
|
+
|
|
111
|
+
if method == 'Newton':
|
|
112
|
+
m = self.fprime(x_k_prev)
|
|
113
|
+
elif method == 'Secant' and step > 1:
|
|
114
|
+
x_k_minus_2 = shown_guesses[step - 2]
|
|
115
|
+
f_k_minus_2 = self.f(x_k_minus_2)
|
|
116
|
+
m = (f_k_prev - f_k_minus_2) / (x_k_prev - x_k_minus_2) if abs(x_k_prev - x_k_minus_2) > 1e-10 else None
|
|
117
|
+
else:
|
|
118
|
+
m = None
|
|
119
|
+
|
|
120
|
+
if m is not None:
|
|
121
|
+
x_line = np.linspace(x_min, x_max, 400)
|
|
122
|
+
y_line = f_k_prev + m * (x_line - x_k_prev)
|
|
123
|
+
aux_line.set_data(x_line, y_line)
|
|
124
|
+
if abs(m) > 1e-12:
|
|
125
|
+
x_intersect = x_k_prev - f_k_prev / m
|
|
126
|
+
vertical_line.set_data([x_intersect, x_intersect], [0, self.f(shown_guesses[step])])
|
|
127
|
+
|
|
128
|
+
return line, current_point, aux_line, vertical_line
|
|
129
|
+
|
|
130
|
+
anim = FuncAnimation(
|
|
131
|
+
fig, update_open,
|
|
132
|
+
frames=total_steps,
|
|
133
|
+
interval=interval_ms,
|
|
134
|
+
blit=True,
|
|
135
|
+
repeat=False
|
|
136
|
+
)
|
|
137
|
+
plt.close(fig)
|
|
138
|
+
return HTML(anim.to_jshtml())
|
|
139
|
+
|
|
140
|
+
def show_toggle_open(self, interval_ms=750):
|
|
141
|
+
newton_anim_html = self._make_animation_open('Newton', interval_ms)
|
|
142
|
+
secant_anim_html = self._make_animation_open('Secant', interval_ms)
|
|
143
|
+
|
|
144
|
+
if newton_anim_html is None or secant_anim_html is None:
|
|
145
|
+
print("Cannot create animations. Check function definition or initial points (x0, x1).")
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
newton_content = widgets.HTML(value=newton_anim_html.data)
|
|
149
|
+
secant_content = widgets.HTML(value=secant_anim_html.data)
|
|
150
|
+
|
|
151
|
+
tab_container = widgets.Tab()
|
|
152
|
+
tab_container.children = [newton_content, secant_content]
|
|
153
|
+
|
|
154
|
+
tab_container.set_title(0, 'Newton')
|
|
155
|
+
tab_container.set_title(1, 'Secant')
|
|
156
|
+
|
|
157
|
+
display(tab_container)
|