executable-engineering 0.1.32__tar.gz → 0.1.34__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.32 → executable_engineering-0.1.34}/PKG-INFO +1 -1
- {executable_engineering-0.1.32 → executable_engineering-0.1.34}/executable_engineering/__init__.py +6 -1
- executable_engineering-0.1.34/executable_engineering/linearsystems_module.py +442 -0
- {executable_engineering-0.1.32 → executable_engineering-0.1.34}/executable_engineering.egg-info/PKG-INFO +1 -1
- {executable_engineering-0.1.32 → executable_engineering-0.1.34}/setup.py +1 -1
- executable_engineering-0.1.32/executable_engineering/linearsystems_module.py +0 -95
- {executable_engineering-0.1.32 → executable_engineering-0.1.34}/LICENSE +0 -0
- {executable_engineering-0.1.32 → executable_engineering-0.1.34}/README.md +0 -0
- {executable_engineering-0.1.32 → executable_engineering-0.1.34}/executable_engineering/closedoptimization_module.py +0 -0
- {executable_engineering-0.1.32 → executable_engineering-0.1.34}/executable_engineering/closedrootfinder_module.py +0 -0
- {executable_engineering-0.1.32 → executable_engineering-0.1.34}/executable_engineering/gradientoptimization_module.py +0 -0
- {executable_engineering-0.1.32 → executable_engineering-0.1.34}/executable_engineering/neuralnet_module.py +0 -0
- {executable_engineering-0.1.32 → executable_engineering-0.1.34}/executable_engineering/numerical_error_module.py +0 -0
- {executable_engineering-0.1.32 → executable_engineering-0.1.34}/executable_engineering/openoptimization_module.py +0 -0
- {executable_engineering-0.1.32 → executable_engineering-0.1.34}/executable_engineering/openrootfinder_module.py +0 -0
- {executable_engineering-0.1.32 → executable_engineering-0.1.34}/executable_engineering.egg-info/SOURCES.txt +0 -0
- {executable_engineering-0.1.32 → executable_engineering-0.1.34}/executable_engineering.egg-info/dependency_links.txt +0 -0
- {executable_engineering-0.1.32 → executable_engineering-0.1.34}/executable_engineering.egg-info/requires.txt +0 -0
- {executable_engineering-0.1.32 → executable_engineering-0.1.34}/executable_engineering.egg-info/top_level.txt +0 -0
- {executable_engineering-0.1.32 → executable_engineering-0.1.34}/pyproject.toml +0 -0
- {executable_engineering-0.1.32 → executable_engineering-0.1.34}/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.34
|
|
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.32 → executable_engineering-0.1.34}/executable_engineering/__init__.py
RENAMED
|
@@ -5,10 +5,15 @@ from .closedoptimization_module import OptimizerClosed
|
|
|
5
5
|
from .openoptimization_module import OptimizerOpen
|
|
6
6
|
from .gradientoptimization_module import OptimizerGrad
|
|
7
7
|
from .numerical_error_module import python_internal_binary, decimal_to_binary
|
|
8
|
-
from .linearsystems_module import visual_solve_2d
|
|
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
|
|
9
9
|
|
|
10
10
|
__all__ = [
|
|
11
11
|
"visual_solve_2d",
|
|
12
|
+
"visualize_conditioning",
|
|
13
|
+
"visualize_matrix_norms",
|
|
14
|
+
"jacobi_step",
|
|
15
|
+
"gauss_seidel_step",
|
|
16
|
+
"sor_step, visualize_convergence_2d, create_diagonally_dominant_matrix, iter_solve, steepest_descent, conjugate_gradient, IterationTracker",
|
|
12
17
|
"init_weights",
|
|
13
18
|
"tanh",
|
|
14
19
|
"tanh_derivative",
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import plotly.graph_objects as go
|
|
3
|
+
from plotly.subplots import make_subplots
|
|
4
|
+
import plotly.colors as pcolors
|
|
5
|
+
|
|
6
|
+
def visual_solve_2d(A, b, method='exact'):
|
|
7
|
+
"""
|
|
8
|
+
Solves and visualizes a 2D linear system Ax = b.
|
|
9
|
+
Plots the lines represented by the rows of A and b, and the solution point.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
A: (N, 2) array-like of coefficients.
|
|
13
|
+
b: (N,) array-like of constants.
|
|
14
|
+
method: 'exact' (uses np.linalg.solve) or 'pseudo' (uses np.linalg.pinv).
|
|
15
|
+
"""
|
|
16
|
+
A = np.array(A, dtype=float)
|
|
17
|
+
b = np.array(b, dtype=float).flatten()
|
|
18
|
+
|
|
19
|
+
if A.shape[1] != 2:
|
|
20
|
+
raise ValueError("visual_solve_2d only supports 2D systems (A must have 2 columns).")
|
|
21
|
+
|
|
22
|
+
# 1. Solve the system based on chosen method
|
|
23
|
+
sol = None
|
|
24
|
+
plot_star = False
|
|
25
|
+
|
|
26
|
+
if method == 'exact':
|
|
27
|
+
try:
|
|
28
|
+
if A.shape[0] == A.shape[1]:
|
|
29
|
+
sol = np.linalg.solve(A, b)
|
|
30
|
+
if np.allclose(A @ sol, b):
|
|
31
|
+
plot_star = True
|
|
32
|
+
sol_label = "Exact Solution"
|
|
33
|
+
except np.linalg.LinAlgError:
|
|
34
|
+
pass # No exact unique solution
|
|
35
|
+
|
|
36
|
+
elif method == 'pseudo':
|
|
37
|
+
sol = np.linalg.pinv(A) @ b
|
|
38
|
+
plot_star = True
|
|
39
|
+
|
|
40
|
+
if np.allclose(A @ sol, b):
|
|
41
|
+
sol_label = "Exact Solution (via Pseudoinverse)"
|
|
42
|
+
else:
|
|
43
|
+
sol_label = "Best Fit Solution"
|
|
44
|
+
else:
|
|
45
|
+
raise ValueError("method must be 'exact' or 'pseudo'")
|
|
46
|
+
|
|
47
|
+
# 2. Determine plotting range
|
|
48
|
+
if sol is not None:
|
|
49
|
+
center_x = sol[0]
|
|
50
|
+
center_y = sol[1]
|
|
51
|
+
else:
|
|
52
|
+
# Fallback if no solution is found
|
|
53
|
+
center_x = 10
|
|
54
|
+
center_y = 10
|
|
55
|
+
|
|
56
|
+
x_margin = 10
|
|
57
|
+
x_range = (center_x - x_margin, center_x + x_margin)
|
|
58
|
+
x_vals = np.linspace(x_range[0], x_range[1], 100)
|
|
59
|
+
|
|
60
|
+
fig = go.Figure()
|
|
61
|
+
colors = pcolors.qualitative.Plotly
|
|
62
|
+
|
|
63
|
+
# 3. Plot each line
|
|
64
|
+
for i in range(A.shape[0]):
|
|
65
|
+
a1, a2 = A[i]
|
|
66
|
+
bi = b[i]
|
|
67
|
+
line_name = f"Row {i+1}"
|
|
68
|
+
|
|
69
|
+
if np.abs(a2) > 1e-10:
|
|
70
|
+
y_vals = (bi - a1 * x_vals) / a2
|
|
71
|
+
fig.add_trace(go.Scatter(x=x_vals, y=y_vals, mode='lines', name=line_name, line=dict(color=colors[i % len(colors)])))
|
|
72
|
+
else:
|
|
73
|
+
if np.abs(a1) > 1e-10:
|
|
74
|
+
x_val = bi / a1
|
|
75
|
+
fig.add_trace(go.Scatter(x=[x_val, x_val], y=[center_y - 100, center_y + 100], mode='lines', name=line_name, line=dict(color=colors[i % len(colors)])))
|
|
76
|
+
|
|
77
|
+
# 4. Plot solution point if required
|
|
78
|
+
if plot_star and sol is not None:
|
|
79
|
+
hover_text = f"x={sol[0]:.4g}, y={sol[1]:.4g}"
|
|
80
|
+
fig.add_trace(go.Scatter(
|
|
81
|
+
x=[sol[0]],
|
|
82
|
+
y=[sol[1]],
|
|
83
|
+
mode='markers',
|
|
84
|
+
marker=dict(color='black', size=12, symbol='star'),
|
|
85
|
+
name=sol_label,
|
|
86
|
+
hoverinfo='text',
|
|
87
|
+
hovertext=hover_text
|
|
88
|
+
))
|
|
89
|
+
|
|
90
|
+
# 5. Auto-adjust y-range nicely around the center
|
|
91
|
+
y_margin = (x_range[1] - x_range[0]) / 2.0
|
|
92
|
+
yaxis_dict = dict(range=[center_y - y_margin, center_y + y_margin], scaleanchor="x", scaleratio=1)
|
|
93
|
+
|
|
94
|
+
fig.update_layout(
|
|
95
|
+
xaxis=dict(range=[x_range[0], x_range[1]]),
|
|
96
|
+
yaxis=yaxis_dict,
|
|
97
|
+
width=700,
|
|
98
|
+
height=500,
|
|
99
|
+
margin=dict(l=20, r=20, t=50, b=20)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
return fig
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def visualize_conditioning(A):
|
|
106
|
+
"""
|
|
107
|
+
Visualizes the effect of the condition number by applying the matrix transformation
|
|
108
|
+
and its inverse to the unit circle.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
A: (2, 2) array-like matrix
|
|
112
|
+
"""
|
|
113
|
+
A = np.array(A, dtype=float)
|
|
114
|
+
if A.shape != (2, 2):
|
|
115
|
+
raise ValueError("visualize_conditioning requires a 2x2 matrix.")
|
|
116
|
+
|
|
117
|
+
# Create the unit circle
|
|
118
|
+
theta = np.linspace(0, 2*np.pi, 200)
|
|
119
|
+
circle = np.array([np.cos(theta), np.sin(theta)])
|
|
120
|
+
|
|
121
|
+
# Apply transformation A to the unit circle
|
|
122
|
+
transformed_A = A @ circle
|
|
123
|
+
|
|
124
|
+
# Check if invertible, if so, apply A^-1 to the unit circle
|
|
125
|
+
try:
|
|
126
|
+
A_inv = np.linalg.inv(A)
|
|
127
|
+
transformed_Ainv = A_inv @ circle
|
|
128
|
+
cond = np.linalg.cond(A)
|
|
129
|
+
subtitle = f"Condition Number: {cond:.2f}"
|
|
130
|
+
except np.linalg.LinAlgError:
|
|
131
|
+
transformed_Ainv = np.zeros_like(circle)
|
|
132
|
+
subtitle = "Matrix is singular (Condition Number: ∞)"
|
|
133
|
+
|
|
134
|
+
fig = make_subplots(
|
|
135
|
+
rows=1, cols=2,
|
|
136
|
+
subplot_titles=(f"Forward Transformation (A*x)", f"Inverse Transformation (A⁻¹*b)"),
|
|
137
|
+
horizontal_spacing=0.1
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Subplot 1: Forward transformation
|
|
141
|
+
fig.add_trace(go.Scatter(x=circle[0], y=circle[1], mode='lines', name='Unit Circle', line=dict(color='gray', dash='dash')), row=1, col=1)
|
|
142
|
+
fig.add_trace(go.Scatter(x=transformed_A[0], y=transformed_A[1], mode='lines', name='A * Circle', line=dict(color='blue')), row=1, col=1)
|
|
143
|
+
|
|
144
|
+
# Subplot 2: Inverse transformation
|
|
145
|
+
fig.add_trace(go.Scatter(x=circle[0], y=circle[1], mode='lines', name='Unit Circle', line=dict(color='gray', dash='dash'), showlegend=False), row=1, col=2)
|
|
146
|
+
fig.add_trace(go.Scatter(x=transformed_Ainv[0], y=transformed_Ainv[1], mode='lines', name='A⁻¹ * Circle', line=dict(color='red')), row=1, col=2)
|
|
147
|
+
|
|
148
|
+
# Update axes to be equal aspect ratio
|
|
149
|
+
max_A = np.max(np.abs(transformed_A)) * 1.1 if np.max(np.abs(transformed_A)) > 0 else 1
|
|
150
|
+
max_Ainv = np.max(np.abs(transformed_Ainv)) * 1.1 if np.max(np.abs(transformed_Ainv)) > 0 else 1
|
|
151
|
+
|
|
152
|
+
# Ensure they are at least 1.1 so the unit circle is visible
|
|
153
|
+
max_A = max(max_A, 1.1)
|
|
154
|
+
max_Ainv = max(max_Ainv, 1.1)
|
|
155
|
+
|
|
156
|
+
fig.update_xaxes(range=[-max_A, max_A], row=1, col=1)
|
|
157
|
+
fig.update_yaxes(range=[-max_A, max_A], scaleanchor="x", scaleratio=1, row=1, col=1)
|
|
158
|
+
|
|
159
|
+
fig.update_xaxes(range=[-max_Ainv, max_Ainv], row=1, col=2)
|
|
160
|
+
fig.update_yaxes(range=[-max_Ainv, max_Ainv], scaleanchor="x2", scaleratio=1, row=1, col=2)
|
|
161
|
+
|
|
162
|
+
fig.update_layout(
|
|
163
|
+
title=f"Visualizing Matrix Distortion ({subtitle})",
|
|
164
|
+
width=900,
|
|
165
|
+
height=500,
|
|
166
|
+
margin=dict(l=20, r=20, t=60, b=20)
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
return fig
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def visualize_matrix_norms(A):
|
|
173
|
+
"""
|
|
174
|
+
Visualizes matrix norms by plotting the transformation of a unit circle
|
|
175
|
+
and overlaying bounding circles based on different matrix norms.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
A: (2, 2) array-like matrix
|
|
179
|
+
"""
|
|
180
|
+
import numpy as np
|
|
181
|
+
import plotly.graph_objects as go
|
|
182
|
+
|
|
183
|
+
A = np.array(A, dtype=float)
|
|
184
|
+
if A.shape != (2, 2):
|
|
185
|
+
raise ValueError("visualize_matrix_norms requires a 2x2 matrix.")
|
|
186
|
+
|
|
187
|
+
# 1. Calculate the norms
|
|
188
|
+
norm_2 = np.linalg.norm(A, ord=2)
|
|
189
|
+
norm_F = np.linalg.norm(A, ord='fro')
|
|
190
|
+
norm_inf = np.linalg.norm(A, ord=np.inf)
|
|
191
|
+
|
|
192
|
+
# 2. Transform the unit circle
|
|
193
|
+
theta = np.linspace(0, 2*np.pi, 200)
|
|
194
|
+
circle = np.array([np.cos(theta), np.sin(theta)])
|
|
195
|
+
ellipse = A @ circle
|
|
196
|
+
|
|
197
|
+
# 3. Create circles for the norms
|
|
198
|
+
circle_2 = norm_2 * circle
|
|
199
|
+
circle_F = norm_F * circle
|
|
200
|
+
circle_inf = norm_inf * circle
|
|
201
|
+
|
|
202
|
+
# 4. Plot!
|
|
203
|
+
fig = go.Figure()
|
|
204
|
+
|
|
205
|
+
# Plot the transformed ellipse
|
|
206
|
+
fig.add_trace(go.Scatter(x=ellipse[0], y=ellipse[1], mode='lines', name='A * Unit Circle (Ellipse)', line=dict(color='blue', width=3)))
|
|
207
|
+
|
|
208
|
+
# Plot the norm bounding circles
|
|
209
|
+
fig.add_trace(go.Scatter(x=circle_2[0], y=circle_2[1], mode='lines', name=f'2-Norm (Radius {norm_2:.2f})', line=dict(color='green', dash='dash')))
|
|
210
|
+
fig.add_trace(go.Scatter(x=circle_F[0], y=circle_F[1], mode='lines', name=f'Frobenius Norm (Radius {norm_F:.2f})', line=dict(color='orange', dash='dash')))
|
|
211
|
+
fig.add_trace(go.Scatter(x=circle_inf[0], y=circle_inf[1], mode='lines', name=f'Infinity Norm (Radius {norm_inf:.2f})', line=dict(color='red', dash='dash')))
|
|
212
|
+
|
|
213
|
+
# Setup axes
|
|
214
|
+
max_val = norm_inf * 1.1
|
|
215
|
+
fig.update_xaxes(range=[-max_val, max_val])
|
|
216
|
+
fig.update_yaxes(range=[-max_val, max_val], scaleanchor="x", scaleratio=1)
|
|
217
|
+
fig.update_layout(title="Matrix Norms as Bounds on Geometric Distortion", width=700, height=700)
|
|
218
|
+
|
|
219
|
+
return fig
|
|
220
|
+
|
|
221
|
+
def jacobi_step(A, b, x):
|
|
222
|
+
"""Performs a single Jacobi iteration step."""
|
|
223
|
+
n = len(x)
|
|
224
|
+
x_new = np.zeros_like(x)
|
|
225
|
+
for i in range(n):
|
|
226
|
+
x_new[i] = (b[i] - np.dot(A[i, :i], x[:i]) - np.dot(A[i, i + 1:], x[i + 1:])) / A[i, i]
|
|
227
|
+
return x_new
|
|
228
|
+
|
|
229
|
+
def gauss_seidel_step(A, b, x, omega = 1):
|
|
230
|
+
"""Performs a single Gauss-Seidel iteration step."""
|
|
231
|
+
n = len(x)
|
|
232
|
+
for i in range(n):
|
|
233
|
+
x[i] = (b[i] - np.dot(A[i, :i], x[:i]) - np.dot(A[i, i + 1:], x[i + 1:])) / A[i, i]
|
|
234
|
+
return x
|
|
235
|
+
|
|
236
|
+
def sor_step(A, b, x, omega=1.05):
|
|
237
|
+
"""Performs a single SOR iteration step."""
|
|
238
|
+
n = len(x)
|
|
239
|
+
for i in range(n):
|
|
240
|
+
x[i] = (1 - omega) * x[i] + (omega / A[i, i]) * (b[i] - np.dot(A[i, :i], x[:i]) - np.dot(A[i, i + 1:], x[i + 1:]))
|
|
241
|
+
return x
|
|
242
|
+
|
|
243
|
+
def visualize_convergence_2d(A, b, iterations, surface_type='residual'):
|
|
244
|
+
"""
|
|
245
|
+
Visualizes the convergence path of an iterative solver over a 2D contour surface.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
A: (2, 2) coefficient matrix.
|
|
249
|
+
b: (2,) right-hand side vector.
|
|
250
|
+
iterations: List or array of (2,) vectors representing the guess at each step.
|
|
251
|
+
surface_type: 'residual' plots ||Ax - b||. 'quadratic' plots 0.5 x^T A x - b^T x.
|
|
252
|
+
"""
|
|
253
|
+
import numpy as np
|
|
254
|
+
import plotly.graph_objects as go
|
|
255
|
+
|
|
256
|
+
A = np.array(A, dtype=float)
|
|
257
|
+
b = np.array(b, dtype=float).flatten()
|
|
258
|
+
iterations = np.array(iterations)
|
|
259
|
+
|
|
260
|
+
if A.shape != (2, 2):
|
|
261
|
+
raise ValueError("visualize_convergence_2d only supports 2D systems.")
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
x_true = np.linalg.solve(A, b)
|
|
265
|
+
except np.linalg.LinAlgError:
|
|
266
|
+
x_true = np.zeros(2)
|
|
267
|
+
|
|
268
|
+
# Determine bounds
|
|
269
|
+
all_points = np.vstack([iterations, x_true])
|
|
270
|
+
min_x, max_x = np.min(all_points[:, 0]), np.max(all_points[:, 0])
|
|
271
|
+
min_y, max_y = np.min(all_points[:, 1]), np.max(all_points[:, 1])
|
|
272
|
+
|
|
273
|
+
# Add padding
|
|
274
|
+
pad_x = max((max_x - min_x) * 0.3, 0.5)
|
|
275
|
+
pad_y = max((max_y - min_y) * 0.3, 0.5)
|
|
276
|
+
|
|
277
|
+
x_range = np.linspace(min_x - pad_x, max_x + pad_x, 100)
|
|
278
|
+
y_range = np.linspace(min_y - pad_y, max_y + pad_y, 100)
|
|
279
|
+
X, Y = np.meshgrid(x_range, y_range)
|
|
280
|
+
|
|
281
|
+
if surface_type == 'residual':
|
|
282
|
+
# ||Ax - b||
|
|
283
|
+
Z = np.sqrt((A[0,0]*X + A[0,1]*Y - b[0])**2 + (A[1,0]*X + A[1,1]*Y - b[1])**2)
|
|
284
|
+
title = "Convergence Path over Residual Surface ||Ax - b||"
|
|
285
|
+
elif surface_type == 'quadratic':
|
|
286
|
+
# 0.5 x^T A x - b^T x
|
|
287
|
+
Z = 0.5 * (A[0,0]*X**2 + (A[0,1] + A[1,0])*X*Y + A[1,1]*Y**2) - (b[0]*X + b[1]*Y)
|
|
288
|
+
title = "Convergence Path over Quadratic Surface f(x)"
|
|
289
|
+
else:
|
|
290
|
+
raise ValueError("surface_type must be 'residual' or 'quadratic'")
|
|
291
|
+
|
|
292
|
+
fig = go.Figure()
|
|
293
|
+
|
|
294
|
+
# Contour plot
|
|
295
|
+
fig.add_trace(go.Contour(
|
|
296
|
+
x=x_range, y=y_range, z=Z,
|
|
297
|
+
colorscale='Viridis',
|
|
298
|
+
contours=dict(showlabels=True, labelfont=dict(size=12, color='white')),
|
|
299
|
+
showscale=False,
|
|
300
|
+
opacity=0.7
|
|
301
|
+
))
|
|
302
|
+
|
|
303
|
+
# Convergence path
|
|
304
|
+
import numpy as np
|
|
305
|
+
import plotly.express as px
|
|
306
|
+
|
|
307
|
+
# We want to draw each segment with a color from the colormap
|
|
308
|
+
if len(iterations) > 1:
|
|
309
|
+
colors = px.colors.sample_colorscale('magma', np.linspace(0, 1, len(iterations)-1))
|
|
310
|
+
for i in range(len(iterations)-1):
|
|
311
|
+
fig.add_trace(go.Scatter(
|
|
312
|
+
x=iterations[i:i+2, 0], y=iterations[i:i+2, 1],
|
|
313
|
+
mode='lines',
|
|
314
|
+
line=dict(color=colors[i], width=3),
|
|
315
|
+
showlegend=False
|
|
316
|
+
))
|
|
317
|
+
|
|
318
|
+
# Add a dummy trace just to show the colorbar
|
|
319
|
+
fig.add_trace(go.Scatter(
|
|
320
|
+
x=[None], y=[None],
|
|
321
|
+
mode='markers',
|
|
322
|
+
marker=dict(
|
|
323
|
+
size=0,
|
|
324
|
+
color=[0, len(iterations)-1],
|
|
325
|
+
colorscale='magma',
|
|
326
|
+
showscale=True,
|
|
327
|
+
colorbar=dict(title="Iteration", len=0.5, y=0.5, x=1.1)
|
|
328
|
+
),
|
|
329
|
+
showlegend=False
|
|
330
|
+
))
|
|
331
|
+
else:
|
|
332
|
+
# Just in case there is no path
|
|
333
|
+
fig.add_trace(go.Scatter(
|
|
334
|
+
x=iterations[:, 0], y=iterations[:, 1],
|
|
335
|
+
mode='markers',
|
|
336
|
+
marker=dict(size=8, color='white'),
|
|
337
|
+
name='Iterations'
|
|
338
|
+
))
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
fig.update_layout(
|
|
343
|
+
title=title,
|
|
344
|
+
width=700, height=600,
|
|
345
|
+
xaxis_title="x₁",
|
|
346
|
+
yaxis_title="x₂",
|
|
347
|
+
yaxis=dict(scaleanchor="x", scaleratio=1),
|
|
348
|
+
margin=dict(l=20, r=20, t=50, b=20)
|
|
349
|
+
)
|
|
350
|
+
return fig
|
|
351
|
+
|
|
352
|
+
def create_diagonally_dominant_matrix(n):
|
|
353
|
+
"""Creates a diagonally dominant random matrix.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
n: The size of the matrix.
|
|
357
|
+
|
|
358
|
+
Returns:
|
|
359
|
+
A diagonally dominant random matrix.
|
|
360
|
+
"""
|
|
361
|
+
import numpy as np
|
|
362
|
+
A = np.random.rand(n, n)
|
|
363
|
+
for i in range(n):
|
|
364
|
+
A[i, i] = np.sum(np.abs(A[i, :])) + np.random.rand()
|
|
365
|
+
return A
|
|
366
|
+
|
|
367
|
+
def iter_solve(A, b, method, tol=1e-6, track_history=False):
|
|
368
|
+
"""
|
|
369
|
+
General wrapper function that repeatedly applies an iterative step
|
|
370
|
+
until the residual error falls below a tolerance.
|
|
371
|
+
"""
|
|
372
|
+
import numpy as np
|
|
373
|
+
x = np.zeros(len(b))
|
|
374
|
+
max_iter = 1000
|
|
375
|
+
|
|
376
|
+
if track_history:
|
|
377
|
+
iterations = [x.copy()]
|
|
378
|
+
|
|
379
|
+
for i in range(max_iter):
|
|
380
|
+
x_new = method(A, b, x.copy())
|
|
381
|
+
if track_history:
|
|
382
|
+
iterations.append(x_new.copy())
|
|
383
|
+
|
|
384
|
+
if np.linalg.norm(A @ x_new - b) < tol:
|
|
385
|
+
print(f'Converged after {i+1} iterations.')
|
|
386
|
+
break
|
|
387
|
+
x = x_new.copy()
|
|
388
|
+
|
|
389
|
+
if track_history:
|
|
390
|
+
return x, iterations
|
|
391
|
+
return x
|
|
392
|
+
|
|
393
|
+
def steepest_descent(A, b, x0, tol=1e-6, max_iter=100, track_history=False):
|
|
394
|
+
"""Solves a linear system Ax = b using the method of steepest descent."""
|
|
395
|
+
import numpy as np
|
|
396
|
+
x = x0.copy()
|
|
397
|
+
if track_history:
|
|
398
|
+
iterations = [x.copy()]
|
|
399
|
+
|
|
400
|
+
for i in range(max_iter):
|
|
401
|
+
r = A @ x - b
|
|
402
|
+
if np.linalg.norm(r) < tol:
|
|
403
|
+
if track_history: print(f'Converged after {i+1} iterations.')
|
|
404
|
+
break
|
|
405
|
+
alpha = np.dot(r, r) / np.dot(r, A @ r)
|
|
406
|
+
x = x - alpha * r
|
|
407
|
+
if track_history:
|
|
408
|
+
iterations.append(x.copy())
|
|
409
|
+
|
|
410
|
+
if track_history:
|
|
411
|
+
return x, iterations
|
|
412
|
+
return x
|
|
413
|
+
|
|
414
|
+
def conjugate_gradient(A, b, x0, tol=1e-6, track_history=False):
|
|
415
|
+
"""Wrapper around SciPy CG to track iterations if needed."""
|
|
416
|
+
import numpy as np
|
|
417
|
+
from scipy.sparse.linalg import cg
|
|
418
|
+
|
|
419
|
+
if track_history:
|
|
420
|
+
iterations = [x0.copy()]
|
|
421
|
+
def callback(xk):
|
|
422
|
+
iterations.append(np.copy(xk))
|
|
423
|
+
solution, info = cg(A, b, x0=x0, tol=tol, callback=callback)
|
|
424
|
+
return solution, np.array(iterations)
|
|
425
|
+
else:
|
|
426
|
+
solution, info = cg(A, b, x0=x0, tol=tol)
|
|
427
|
+
return solution
|
|
428
|
+
|
|
429
|
+
class IterationTracker:
|
|
430
|
+
"""
|
|
431
|
+
A clean callback tracker for native SciPy solvers (cg, gmres, etc.).
|
|
432
|
+
"""
|
|
433
|
+
def __init__(self, x0=None):
|
|
434
|
+
import numpy as np
|
|
435
|
+
if x0 is not None:
|
|
436
|
+
self.iterations = [np.array(x0).copy()]
|
|
437
|
+
else:
|
|
438
|
+
self.iterations = []
|
|
439
|
+
|
|
440
|
+
def __call__(self, xk):
|
|
441
|
+
import numpy as np
|
|
442
|
+
self.iterations.append(np.array(xk).copy())
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: executable_engineering
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.34
|
|
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
|
|
@@ -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.34",
|
|
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,95 +0,0 @@
|
|
|
1
|
-
import numpy as np
|
|
2
|
-
import plotly.graph_objects as go
|
|
3
|
-
import plotly.colors as pcolors
|
|
4
|
-
|
|
5
|
-
def visual_solve_2d(A, b, x_range=None, title="Linear System", labels=None):
|
|
6
|
-
"""
|
|
7
|
-
Solves and visualizes a 2D linear system Ax = b.
|
|
8
|
-
Plots the lines represented by the rows of A and b, and the solution point.
|
|
9
|
-
Falls back to the pseudoinverse for singular or non-square systems.
|
|
10
|
-
|
|
11
|
-
Args:
|
|
12
|
-
A: (N, 2) array-like of coefficients.
|
|
13
|
-
b: (N,) array-like of constants.
|
|
14
|
-
x_range: Tuple of (xmin, xmax) for plotting. If None, auto-calculated.
|
|
15
|
-
title: Plot title.
|
|
16
|
-
labels: Tuple of (x_label, y_label) for axes.
|
|
17
|
-
"""
|
|
18
|
-
if labels is None:
|
|
19
|
-
labels = ("x", "y")
|
|
20
|
-
|
|
21
|
-
A = np.array(A, dtype=float)
|
|
22
|
-
b = np.array(b, dtype=float).flatten()
|
|
23
|
-
|
|
24
|
-
if A.shape[1] != 2:
|
|
25
|
-
raise ValueError("visual_solve_2d only supports 2D systems (A must have 2 columns).")
|
|
26
|
-
|
|
27
|
-
# 1. Solve the system using the pseudoinverse
|
|
28
|
-
# For a square, full-rank matrix, pinv is mathematically identical to the standard inverse.
|
|
29
|
-
# For singular or non-square matrices, it finds the least-squares/minimum-norm solution.
|
|
30
|
-
sol = np.linalg.pinv(A) @ b
|
|
31
|
-
|
|
32
|
-
# Check if the solution is exact AND unique.
|
|
33
|
-
# It is exact if A*x = b. It is unique if the rank of A equals the number of variables (columns).
|
|
34
|
-
is_exact = np.allclose(A @ sol, b)
|
|
35
|
-
is_unique = np.linalg.matrix_rank(A) == A.shape[1]
|
|
36
|
-
is_exact_unique = is_exact and is_unique
|
|
37
|
-
|
|
38
|
-
# 2. Determine plotting range
|
|
39
|
-
if x_range is None:
|
|
40
|
-
center_x = sol[0]
|
|
41
|
-
x_range = (center_x - 10, center_x + 10)
|
|
42
|
-
|
|
43
|
-
x_vals = np.linspace(x_range[0], x_range[1], 100)
|
|
44
|
-
|
|
45
|
-
fig = go.Figure()
|
|
46
|
-
|
|
47
|
-
colors = pcolors.qualitative.Plotly
|
|
48
|
-
|
|
49
|
-
# 3. Plot each line
|
|
50
|
-
for i in range(A.shape[0]):
|
|
51
|
-
a1, a2 = A[i]
|
|
52
|
-
bi = b[i]
|
|
53
|
-
|
|
54
|
-
if np.abs(a2) > 1e-10:
|
|
55
|
-
# y = (bi - a1*x) / a2
|
|
56
|
-
y_vals = (bi - a1 * x_vals) / a2
|
|
57
|
-
line_name = f"{a1:g}{labels[0]} + {a2:g}{labels[1]} = {bi:g}"
|
|
58
|
-
fig.add_trace(go.Scatter(x=x_vals, y=y_vals, mode='lines', name=line_name, line=dict(color=colors[i % len(colors)])))
|
|
59
|
-
else:
|
|
60
|
-
# Vertical line: x = bi / a1
|
|
61
|
-
if np.abs(a1) > 1e-10:
|
|
62
|
-
x_val = bi / a1
|
|
63
|
-
line_name = f"{a1:g}{labels[0]} = {bi:g}"
|
|
64
|
-
fig.add_trace(go.Scatter(x=[x_val, x_val], y=[-100, 100], mode='lines', name=line_name, line=dict(color=colors[i % len(colors)])))
|
|
65
|
-
|
|
66
|
-
# 4. Plot solution point
|
|
67
|
-
if is_unique:
|
|
68
|
-
if is_exact:
|
|
69
|
-
sol_label = "Exact Solution"
|
|
70
|
-
else:
|
|
71
|
-
sol_label = "Best Fit Solution"
|
|
72
|
-
|
|
73
|
-
hover_text = f"{labels[0]}={sol[0]:.4g}, {labels[1]}={sol[1]:.4g}"
|
|
74
|
-
fig.add_trace(go.Scatter(
|
|
75
|
-
x=[sol[0]],
|
|
76
|
-
y=[sol[1]],
|
|
77
|
-
mode='markers',
|
|
78
|
-
marker=dict(color='black', size=12, symbol='star'),
|
|
79
|
-
name=sol_label,
|
|
80
|
-
hoverinfo='text',
|
|
81
|
-
hovertext=hover_text
|
|
82
|
-
))
|
|
83
|
-
|
|
84
|
-
# Auto-adjust y-range nicely around the solution if not a vertical-only mess
|
|
85
|
-
y_margin = (x_range[1] - x_range[0]) / 2.0
|
|
86
|
-
|
|
87
|
-
fig.update_layout(
|
|
88
|
-
title=title,
|
|
89
|
-
xaxis_title=labels[0],
|
|
90
|
-
yaxis_title=labels[1],
|
|
91
|
-
yaxis=dict(range=[sol[1] - y_margin, sol[1] + y_margin]),
|
|
92
|
-
template="plotly_white"
|
|
93
|
-
)
|
|
94
|
-
|
|
95
|
-
return fig
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|