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.
@@ -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)