executable-engineering 0.1.44__py3-none-any.whl → 0.1.47__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 +124 -11
- executable_engineering/differentiation_module.py +678 -0
- executable_engineering/integration_module.py +699 -0
- executable_engineering/neuralnet_module.py +14 -18
- executable_engineering/numerical_integration_module.py +902 -0
- {executable_engineering-0.1.44.dist-info → executable_engineering-0.1.47.dist-info}/METADATA +1 -1
- {executable_engineering-0.1.44.dist-info → executable_engineering-0.1.47.dist-info}/RECORD +10 -7
- {executable_engineering-0.1.44.dist-info → executable_engineering-0.1.47.dist-info}/WHEEL +1 -1
- {executable_engineering-0.1.44.dist-info → executable_engineering-0.1.47.dist-info}/licenses/LICENSE +0 -0
- {executable_engineering-0.1.44.dist-info → executable_engineering-0.1.47.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
"""
|
|
2
|
+
differentiation_module.py
|
|
3
|
+
Analytical models, finite difference stencils, and interactive Plotly visualizers
|
|
4
|
+
for numerical differentiation in ExecutableEngineering.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import math
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
import numpy as np
|
|
11
|
+
_HAVE_NUMPY = True
|
|
12
|
+
except ImportError:
|
|
13
|
+
_HAVE_NUMPY = False
|
|
14
|
+
|
|
15
|
+
class _NumpyFallback:
|
|
16
|
+
pi = math.pi
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def isscalar(val):
|
|
20
|
+
return not hasattr(val, '__iter__')
|
|
21
|
+
|
|
22
|
+
class ndarray(list):
|
|
23
|
+
def __getitem__(self, key):
|
|
24
|
+
if isinstance(key, tuple):
|
|
25
|
+
row_slice, col_slice = key
|
|
26
|
+
sub_rows = super().__getitem__(row_slice)
|
|
27
|
+
if isinstance(sub_rows, list):
|
|
28
|
+
return _NumpyFallback.array([_NumpyFallback.array(r)[col_slice] for r in sub_rows])
|
|
29
|
+
return sub_rows[col_slice]
|
|
30
|
+
res = super().__getitem__(key)
|
|
31
|
+
if isinstance(res, list):
|
|
32
|
+
return _NumpyFallback.array(res)
|
|
33
|
+
return res
|
|
34
|
+
|
|
35
|
+
def __add__(self, other):
|
|
36
|
+
if hasattr(other, '__iter__'):
|
|
37
|
+
return _NumpyFallback.array([x + y for x, y in zip(self, other)])
|
|
38
|
+
return _NumpyFallback.array([x + other for x in self])
|
|
39
|
+
|
|
40
|
+
def __radd__(self, other):
|
|
41
|
+
return self.__add__(other)
|
|
42
|
+
|
|
43
|
+
def __sub__(self, other):
|
|
44
|
+
if hasattr(other, '__iter__'):
|
|
45
|
+
return _NumpyFallback.array([x - y for x, y in zip(self, other)])
|
|
46
|
+
return _NumpyFallback.array([x - other for x in self])
|
|
47
|
+
|
|
48
|
+
def __rsub__(self, other):
|
|
49
|
+
if hasattr(other, '__iter__'):
|
|
50
|
+
return _NumpyFallback.array([y - x for x, y in zip(self, other)])
|
|
51
|
+
return _NumpyFallback.array([other - x for x in self])
|
|
52
|
+
|
|
53
|
+
def __mul__(self, other):
|
|
54
|
+
if hasattr(other, '__iter__'):
|
|
55
|
+
return _NumpyFallback.array([x * y for x, y in zip(self, other)])
|
|
56
|
+
return _NumpyFallback.array([x * other for x in self])
|
|
57
|
+
|
|
58
|
+
def __rmul__(self, other):
|
|
59
|
+
return self.__mul__(other)
|
|
60
|
+
|
|
61
|
+
def __truediv__(self, other):
|
|
62
|
+
if hasattr(other, '__iter__'):
|
|
63
|
+
return _NumpyFallback.array([x / y for x, y in zip(self, other)])
|
|
64
|
+
return _NumpyFallback.array([x / other for x in self])
|
|
65
|
+
|
|
66
|
+
def __rtruediv__(self, other):
|
|
67
|
+
if hasattr(other, '__iter__'):
|
|
68
|
+
return _NumpyFallback.array([y / x for x, y in zip(self, other)])
|
|
69
|
+
return _NumpyFallback.array([other / x for x in self])
|
|
70
|
+
|
|
71
|
+
def __pow__(self, other):
|
|
72
|
+
return _NumpyFallback.array([x ** other for x in self])
|
|
73
|
+
|
|
74
|
+
def __neg__(self):
|
|
75
|
+
return _NumpyFallback.array([-x for x in self])
|
|
76
|
+
|
|
77
|
+
def __gt__(self, other):
|
|
78
|
+
if hasattr(other, '__iter__'):
|
|
79
|
+
return _NumpyFallback.array([x > y for x, y in zip(self, other)])
|
|
80
|
+
return _NumpyFallback.array([x > other for x in self])
|
|
81
|
+
|
|
82
|
+
def flatten(self):
|
|
83
|
+
flat = []
|
|
84
|
+
for item in self:
|
|
85
|
+
if hasattr(item, '__iter__'):
|
|
86
|
+
flat.extend(_NumpyFallback.array(item).flatten())
|
|
87
|
+
else:
|
|
88
|
+
flat.append(item)
|
|
89
|
+
return _NumpyFallback.array(flat)
|
|
90
|
+
|
|
91
|
+
@classmethod
|
|
92
|
+
def array(cls, data, dtype=float):
|
|
93
|
+
if isinstance(data, cls.ndarray):
|
|
94
|
+
return data
|
|
95
|
+
return cls.ndarray(data)
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def asarray(cls, data, dtype=float):
|
|
99
|
+
if hasattr(data, '__iter__'):
|
|
100
|
+
return cls.array([float(x) for x in data], dtype=dtype)
|
|
101
|
+
return float(data)
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def linspace(cls, start, stop, num=50):
|
|
105
|
+
step = (stop - start) / float(num - 1) if num > 1 else 0.0
|
|
106
|
+
return cls.array([start + i * step for i in range(num)])
|
|
107
|
+
|
|
108
|
+
@classmethod
|
|
109
|
+
def log(cls, x):
|
|
110
|
+
if hasattr(x, '__iter__'):
|
|
111
|
+
return cls.array([math.log(v) for v in x])
|
|
112
|
+
return math.log(x)
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def abs(cls, x):
|
|
116
|
+
if hasattr(x, '__iter__'):
|
|
117
|
+
return cls.array([math.fabs(v) for v in x])
|
|
118
|
+
return math.fabs(x)
|
|
119
|
+
|
|
120
|
+
@classmethod
|
|
121
|
+
def where(cls, cond, a, b):
|
|
122
|
+
if hasattr(cond, '__iter__'):
|
|
123
|
+
return cls.array([av if cv else bv for cv, av, bv in zip(cond, a, b)])
|
|
124
|
+
return a if cond else b
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
def meshgrid(cls, x, y):
|
|
128
|
+
X = cls.array([list(x) for _ in y])
|
|
129
|
+
Y = cls.array([[yj for _ in x] for yj in y])
|
|
130
|
+
return X, Y
|
|
131
|
+
|
|
132
|
+
@classmethod
|
|
133
|
+
def gradient(cls, Z, *varargs):
|
|
134
|
+
dy = varargs[0] if len(varargs) > 0 else 1.0
|
|
135
|
+
dx = varargs[1] if len(varargs) > 1 else 1.0
|
|
136
|
+
rows = len(Z)
|
|
137
|
+
cols = len(Z[0])
|
|
138
|
+
dZdx = []
|
|
139
|
+
dZdy = []
|
|
140
|
+
for r in range(rows):
|
|
141
|
+
row_dx = []
|
|
142
|
+
for c in range(cols):
|
|
143
|
+
if c == 0:
|
|
144
|
+
row_dx.append((Z[r][1] - Z[r][0]) / dx)
|
|
145
|
+
elif c == cols - 1:
|
|
146
|
+
row_dx.append((Z[r][-1] - Z[r][-2]) / dx)
|
|
147
|
+
else:
|
|
148
|
+
row_dx.append((Z[r][c + 1] - Z[r][c - 1]) / (2.0 * dx))
|
|
149
|
+
dZdx.append(row_dx)
|
|
150
|
+
for r in range(rows):
|
|
151
|
+
row_dy = []
|
|
152
|
+
for c in range(cols):
|
|
153
|
+
if r == 0:
|
|
154
|
+
row_dy.append((Z[1][c] - Z[0][c]) / dy)
|
|
155
|
+
elif r == rows - 1:
|
|
156
|
+
row_dy.append((Z[-1][c] - Z[-2][c]) / dy)
|
|
157
|
+
else:
|
|
158
|
+
row_dy.append((Z[r + 1][c] - Z[r - 1][c]) / (2.0 * dy))
|
|
159
|
+
dZdy.append(row_dy)
|
|
160
|
+
return cls.array(dZdy), cls.array(dZdx)
|
|
161
|
+
|
|
162
|
+
np = _NumpyFallback()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
import plotly.graph_objects as go
|
|
167
|
+
from plotly.subplots import make_subplots
|
|
168
|
+
_HAVE_PLOTLY = True
|
|
169
|
+
except ImportError:
|
|
170
|
+
_HAVE_PLOTLY = False
|
|
171
|
+
|
|
172
|
+
class _FigureFallback:
|
|
173
|
+
def __init__(self, data=None, layout=None):
|
|
174
|
+
self.data = list(data) if data else []
|
|
175
|
+
self.layout = layout or {}
|
|
176
|
+
|
|
177
|
+
def add_trace(self, trace, row=None, col=None):
|
|
178
|
+
self.data.append(trace)
|
|
179
|
+
return self
|
|
180
|
+
|
|
181
|
+
def update_layout(self, **kwargs):
|
|
182
|
+
self.layout.update(kwargs)
|
|
183
|
+
return self
|
|
184
|
+
|
|
185
|
+
def update_xaxes(self, **kwargs):
|
|
186
|
+
return self
|
|
187
|
+
|
|
188
|
+
def update_yaxes(self, **kwargs):
|
|
189
|
+
return self
|
|
190
|
+
|
|
191
|
+
def add_vline(self, **kwargs):
|
|
192
|
+
return self
|
|
193
|
+
|
|
194
|
+
def show(self):
|
|
195
|
+
pass
|
|
196
|
+
|
|
197
|
+
class _GoFallback:
|
|
198
|
+
Figure = _FigureFallback
|
|
199
|
+
Scatter = staticmethod(lambda **kwargs: kwargs)
|
|
200
|
+
Bar = staticmethod(lambda **kwargs: kwargs)
|
|
201
|
+
Surface = staticmethod(lambda **kwargs: kwargs)
|
|
202
|
+
Cone = staticmethod(lambda **kwargs: kwargs)
|
|
203
|
+
Heatmap = staticmethod(lambda **kwargs: kwargs)
|
|
204
|
+
|
|
205
|
+
go = _GoFallback()
|
|
206
|
+
make_subplots = staticmethod(lambda **kwargs: _FigureFallback())
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# --- Analytical Benchmark Models ---
|
|
210
|
+
|
|
211
|
+
def rocket_velocity(t):
|
|
212
|
+
"""
|
|
213
|
+
Analytical rocket velocity benchmark:
|
|
214
|
+
v(t) = u * ln(m0 / (m0 - q * t)) - g * t
|
|
215
|
+
where:
|
|
216
|
+
u = 2000.0 m/s (exhaust velocity)
|
|
217
|
+
m0 = 150000.0 kg (initial rocket mass)
|
|
218
|
+
q = 2500.0 kg/s (fuel consumption rate)
|
|
219
|
+
g = 9.81 m/s^2 (gravitational acceleration)
|
|
220
|
+
"""
|
|
221
|
+
u = 2000.0
|
|
222
|
+
m0 = 150000.0
|
|
223
|
+
q = 2500.0
|
|
224
|
+
g = 9.81
|
|
225
|
+
t_arr = np.asarray(t, dtype=float)
|
|
226
|
+
res = u * np.log(m0 / (m0 - q * t_arr)) - g * t_arr
|
|
227
|
+
if np.isscalar(t):
|
|
228
|
+
return float(res)
|
|
229
|
+
return res
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def rocket_acceleration(t):
|
|
233
|
+
"""
|
|
234
|
+
Exact analytical rocket acceleration (time derivative of rocket_velocity):
|
|
235
|
+
a(t) = (u * q) / (m0 - q * t) - g
|
|
236
|
+
"""
|
|
237
|
+
u = 2000.0
|
|
238
|
+
m0 = 150000.0
|
|
239
|
+
q = 2500.0
|
|
240
|
+
g = 9.81
|
|
241
|
+
t_arr = np.asarray(t, dtype=float)
|
|
242
|
+
res = (u * q) / (m0 - q * t_arr) - g
|
|
243
|
+
if np.isscalar(t):
|
|
244
|
+
return float(res)
|
|
245
|
+
return res
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
# --- Finite Difference Stencils ---
|
|
249
|
+
|
|
250
|
+
def forward_difference(f, x, h):
|
|
251
|
+
"""
|
|
252
|
+
First-order forward finite difference stencil:
|
|
253
|
+
f'(x) ≈ (f(x + h) - f(x)) / h
|
|
254
|
+
"""
|
|
255
|
+
if callable(f):
|
|
256
|
+
return (f(x + h) - f(x)) / h
|
|
257
|
+
raise TypeError("f must be a callable function f(x)")
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def backward_difference(f, x, h):
|
|
261
|
+
"""
|
|
262
|
+
First-order backward finite difference stencil:
|
|
263
|
+
f'(x) ≈ (f(x) - f(x - h)) / h
|
|
264
|
+
"""
|
|
265
|
+
if callable(f):
|
|
266
|
+
return (f(x) - f(x - h)) / h
|
|
267
|
+
raise TypeError("f must be a callable function f(x)")
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def central_difference(f, x, h):
|
|
271
|
+
"""
|
|
272
|
+
Second-order central finite difference stencil:
|
|
273
|
+
f'(x) ≈ (f(x + h) - f(x - h)) / (2 * h)
|
|
274
|
+
"""
|
|
275
|
+
if callable(f):
|
|
276
|
+
return (f(x + h) - f(x - h)) / (2.0 * h)
|
|
277
|
+
raise TypeError("f must be a callable function f(x)")
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def second_derivative(f, x, h):
|
|
281
|
+
"""
|
|
282
|
+
Second-order central difference for the second derivative:
|
|
283
|
+
f''(x) ≈ (f(x + h) - 2 * f(x) + f(x - h)) / h^2
|
|
284
|
+
"""
|
|
285
|
+
if callable(f):
|
|
286
|
+
return (f(x + h) - 2.0 * f(x) + f(x - h)) / (h ** 2)
|
|
287
|
+
raise TypeError("f must be a callable function f(x)")
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
# Stencil aliases for flexibility
|
|
291
|
+
forward_diff = forward_difference
|
|
292
|
+
backward_diff = backward_difference
|
|
293
|
+
central_diff = central_difference
|
|
294
|
+
second_central_diff = second_derivative
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
# --- Interactive Plotly Visualizers ---
|
|
298
|
+
|
|
299
|
+
def plot_secant_tangent_limit(f, x0=2.0, h_values=(1.0, 0.5, 0.2, 0.05), x_range=(0.5, 4.0), title="Secant to Tangent Convergence"):
|
|
300
|
+
"""
|
|
301
|
+
Interactive Plotly figure illustrating the geometric limit of secant lines
|
|
302
|
+
approaching the true tangent line as step size h -> 0.
|
|
303
|
+
"""
|
|
304
|
+
x = np.linspace(x_range[0], x_range[1], 400)
|
|
305
|
+
y = f(x)
|
|
306
|
+
y0 = f(x0)
|
|
307
|
+
|
|
308
|
+
# High-accuracy numerical slope for true tangent reference
|
|
309
|
+
eps = 1e-6
|
|
310
|
+
true_slope = (f(x0 + eps) - f(x0 - eps)) / (2.0 * eps)
|
|
311
|
+
tangent_line = y0 + true_slope * (x - x0)
|
|
312
|
+
|
|
313
|
+
fig = go.Figure()
|
|
314
|
+
fig.add_trace(go.Scatter(
|
|
315
|
+
x=x, y=y, mode='lines', name='f(x)',
|
|
316
|
+
line=dict(color='#111827', width=3)
|
|
317
|
+
))
|
|
318
|
+
fig.add_trace(go.Scatter(
|
|
319
|
+
x=x, y=tangent_line, mode='lines',
|
|
320
|
+
name=f"True Tangent (slope = {true_slope:.4f})",
|
|
321
|
+
line=dict(color='#DC2626', width=2.5, dash='dash')
|
|
322
|
+
))
|
|
323
|
+
|
|
324
|
+
colors = ['#2563EB', '#D97706', '#059669', '#7C3AED', '#DB2777']
|
|
325
|
+
for idx, h in enumerate(h_values):
|
|
326
|
+
x1 = x0 + h
|
|
327
|
+
y1 = f(x1)
|
|
328
|
+
slope = (y1 - y0) / h
|
|
329
|
+
secant_line = y0 + slope * (x - x0)
|
|
330
|
+
c = colors[idx % len(colors)]
|
|
331
|
+
fig.add_trace(go.Scatter(
|
|
332
|
+
x=x, y=secant_line, mode='lines',
|
|
333
|
+
name=f"Secant h={h} (slope = {slope:.4f})",
|
|
334
|
+
line=dict(color=c, width=1.8, dash='dot')
|
|
335
|
+
))
|
|
336
|
+
fig.add_trace(go.Scatter(
|
|
337
|
+
x=[x1], y=[y1], mode='markers',
|
|
338
|
+
name=f"f(x0 + {h})",
|
|
339
|
+
marker=dict(color=c, size=7, symbol='diamond'),
|
|
340
|
+
showlegend=False
|
|
341
|
+
))
|
|
342
|
+
|
|
343
|
+
fig.add_trace(go.Scatter(
|
|
344
|
+
x=[x0], y=[y0], mode='markers',
|
|
345
|
+
name=f"Anchor Point (x0, f(x0))",
|
|
346
|
+
marker=dict(color='#111827', size=11, symbol='circle')
|
|
347
|
+
))
|
|
348
|
+
|
|
349
|
+
fig.update_layout(
|
|
350
|
+
title=title,
|
|
351
|
+
xaxis_title="x",
|
|
352
|
+
yaxis_title="f(x)",
|
|
353
|
+
template="plotly_white",
|
|
354
|
+
hovermode="x unified",
|
|
355
|
+
legend=dict(x=0.02, y=0.98, bgcolor="rgba(255, 255, 255, 0.8)")
|
|
356
|
+
)
|
|
357
|
+
return fig
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def plot_derivative_comparison(t, a_true, approximations, title="Numerical vs Analytical Derivative"):
|
|
361
|
+
"""
|
|
362
|
+
Compares numerical derivatives against analytical truth using Plotly subplots.
|
|
363
|
+
t: 1D array of evaluation coordinates (e.g. time)
|
|
364
|
+
a_true: 1D array of exact analytical derivative values
|
|
365
|
+
approximations: dict of {'Scheme Label': numerical_derivative_array}
|
|
366
|
+
"""
|
|
367
|
+
fig = make_subplots(
|
|
368
|
+
rows=2, cols=1, shared_xaxes=True, vertical_spacing=0.08,
|
|
369
|
+
subplot_titles=("Derivative Comparison", "Absolute Error |Approx - True|"),
|
|
370
|
+
row_heights=[0.65, 0.35]
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
fig.add_trace(
|
|
374
|
+
go.Scatter(
|
|
375
|
+
x=t, y=a_true, mode='lines', name='Analytical True',
|
|
376
|
+
line=dict(color='#111827', width=2.5)
|
|
377
|
+
),
|
|
378
|
+
row=1, col=1
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
palette = ['#2563EB', '#059669', '#D97706', '#7C3AED', '#DC2626', '#0891B2']
|
|
382
|
+
t_list = list(t)
|
|
383
|
+
a_true_list = [float(val) for val in a_true]
|
|
384
|
+
|
|
385
|
+
for idx, (label, a_num) in enumerate(approximations.items()):
|
|
386
|
+
c = palette[idx % len(palette)]
|
|
387
|
+
a_num_list = [float(val) for val in a_num]
|
|
388
|
+
fig.add_trace(
|
|
389
|
+
go.Scatter(
|
|
390
|
+
x=t, y=a_num, mode='lines+markers', name=label,
|
|
391
|
+
line=dict(color=c, width=1.5), marker=dict(size=4)
|
|
392
|
+
),
|
|
393
|
+
row=1, col=1
|
|
394
|
+
)
|
|
395
|
+
err_clean = [max(abs(an - at), 1e-16) for an, at in zip(a_num_list, a_true_list)]
|
|
396
|
+
fig.add_trace(
|
|
397
|
+
go.Scatter(
|
|
398
|
+
x=t, y=err_clean, mode='lines', name=f"{label} Error",
|
|
399
|
+
line=dict(color=c, width=1.5)
|
|
400
|
+
),
|
|
401
|
+
row=2, col=1
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
fig.update_layout(
|
|
405
|
+
title=title,
|
|
406
|
+
template="plotly_white",
|
|
407
|
+
hovermode="x unified",
|
|
408
|
+
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1)
|
|
409
|
+
)
|
|
410
|
+
fig.update_yaxes(title_text="Derivative f'(t)", row=1, col=1)
|
|
411
|
+
fig.update_yaxes(title_text="Absolute Error", type="log", row=2, col=1)
|
|
412
|
+
fig.update_xaxes(title_text="t", row=2, col=1)
|
|
413
|
+
return fig
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def plot_error_scaling(h_values, errors_dict, expected_orders=None, title="Finite Difference Error vs Step Size h"):
|
|
417
|
+
"""
|
|
418
|
+
Log-log Plotly convergence plot showing error decay against step size h.
|
|
419
|
+
h_values: 1D array-like of step sizes
|
|
420
|
+
errors_dict: dict of {'Scheme Name': errors_array}
|
|
421
|
+
expected_orders: optional dict of {'Scheme Name': int_order}
|
|
422
|
+
"""
|
|
423
|
+
h_arr = np.asarray(h_values, dtype=float)
|
|
424
|
+
fig = go.Figure()
|
|
425
|
+
palette = ['#2563EB', '#059669', '#D97706', '#7C3AED', '#DC2626']
|
|
426
|
+
|
|
427
|
+
for idx, (name, errs) in enumerate(errors_dict.items()):
|
|
428
|
+
c = palette[idx % len(palette)]
|
|
429
|
+
err_arr = np.asarray(errs, dtype=float)
|
|
430
|
+
fig.add_trace(go.Scatter(
|
|
431
|
+
x=h_arr, y=err_arr, mode='lines+markers', name=name,
|
|
432
|
+
line=dict(color=c, width=2), marker=dict(size=7, symbol='circle')
|
|
433
|
+
))
|
|
434
|
+
|
|
435
|
+
if expected_orders:
|
|
436
|
+
for name, order in expected_orders.items():
|
|
437
|
+
if name in errors_dict:
|
|
438
|
+
y0 = errors_dict[name][0]
|
|
439
|
+
h0 = h_arr[0]
|
|
440
|
+
ref_line = (h_arr ** order) * (y0 / (h0 ** order))
|
|
441
|
+
fig.add_trace(go.Scatter(
|
|
442
|
+
x=h_arr, y=ref_line, mode='lines',
|
|
443
|
+
name=f"Theoretical O(h^{order})",
|
|
444
|
+
line=dict(dash='dash', color='#6B7280', width=1.3)
|
|
445
|
+
))
|
|
446
|
+
|
|
447
|
+
fig.update_layout(
|
|
448
|
+
title=title,
|
|
449
|
+
xaxis_title="Step Size h",
|
|
450
|
+
yaxis_title="Absolute Error",
|
|
451
|
+
xaxis_type="log",
|
|
452
|
+
yaxis_type="log",
|
|
453
|
+
template="plotly_white",
|
|
454
|
+
legend=dict(x=0.02, y=0.98)
|
|
455
|
+
)
|
|
456
|
+
return fig
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def plot_surface_gradient_3d(func, x_range=(-2, 2), y_range=(-2, 2), n_grid=40, arrow_stride=4, title="3D Surface and Numerical Gradient Vector Field", subplots=False):
|
|
460
|
+
"""
|
|
461
|
+
Interactive 3D Plotly visualization of a 2D scalar field z = f(x, y)
|
|
462
|
+
with 3D gradient vector cones, optionally expanded to subplots for
|
|
463
|
+
gradient components in x and y.
|
|
464
|
+
"""
|
|
465
|
+
x = np.linspace(x_range[0], x_range[1], n_grid)
|
|
466
|
+
y = np.linspace(y_range[0], y_range[1], n_grid)
|
|
467
|
+
X, Y = np.meshgrid(x, y)
|
|
468
|
+
Z = func(X, Y)
|
|
469
|
+
|
|
470
|
+
dx = x[1] - x[0]
|
|
471
|
+
dy = y[1] - y[0]
|
|
472
|
+
dZdx = np.gradient(Z, dx, axis=1)
|
|
473
|
+
dZdy = np.gradient(Z, dy, axis=0)
|
|
474
|
+
|
|
475
|
+
# Downsampled cone vectors
|
|
476
|
+
Xs = X[::arrow_stride, ::arrow_stride].flatten()
|
|
477
|
+
Ys = Y[::arrow_stride, ::arrow_stride].flatten()
|
|
478
|
+
Zs = Z[::arrow_stride, ::arrow_stride].flatten()
|
|
479
|
+
Us = dZdx[::arrow_stride, ::arrow_stride].flatten()
|
|
480
|
+
Vs = dZdy[::arrow_stride, ::arrow_stride].flatten()
|
|
481
|
+
Ws = np.asarray([0.0 for _ in Us])
|
|
482
|
+
|
|
483
|
+
if subplots:
|
|
484
|
+
fig = make_subplots(
|
|
485
|
+
rows=1, cols=3,
|
|
486
|
+
specs=[[{"type": "scene"}, {"type": "scene"}, {"type": "scene"}]],
|
|
487
|
+
subplot_titles=(
|
|
488
|
+
"Surface f(x, y) with ∇f Cones",
|
|
489
|
+
"Gradient Component: ∂f/∂x",
|
|
490
|
+
"Gradient Component: ∂f/∂y"
|
|
491
|
+
),
|
|
492
|
+
horizontal_spacing=0.03
|
|
493
|
+
)
|
|
494
|
+
# Panel 1: Surface with gradient cones
|
|
495
|
+
fig.add_trace(go.Surface(
|
|
496
|
+
x=X, y=Y, z=Z, colorscale='Viridis', opacity=0.85,
|
|
497
|
+
name='f(x, y)', showscale=False
|
|
498
|
+
), row=1, col=1)
|
|
499
|
+
fig.add_trace(go.Cone(
|
|
500
|
+
x=Xs, y=Ys, z=Zs, u=Us, v=Vs, w=Ws,
|
|
501
|
+
sizemode='scaled', sizeref=0.35,
|
|
502
|
+
colorscale='Blues', showscale=False,
|
|
503
|
+
name='∇f'
|
|
504
|
+
), row=1, col=1)
|
|
505
|
+
|
|
506
|
+
# Panel 2: x component ∂f/∂x
|
|
507
|
+
fig.add_trace(go.Surface(
|
|
508
|
+
x=X, y=Y, z=dZdx, colorscale='RdBu', showscale=False,
|
|
509
|
+
name='∂f/∂x'
|
|
510
|
+
), row=1, col=2)
|
|
511
|
+
|
|
512
|
+
# Panel 3: y component ∂f/∂y
|
|
513
|
+
fig.add_trace(go.Surface(
|
|
514
|
+
x=X, y=Y, z=dZdy, colorscale='RdBu', showscale=False,
|
|
515
|
+
name='∂f/∂y'
|
|
516
|
+
), row=1, col=3)
|
|
517
|
+
|
|
518
|
+
fig.update_layout(
|
|
519
|
+
title=title,
|
|
520
|
+
height=550,
|
|
521
|
+
template="plotly_white",
|
|
522
|
+
margin=dict(l=10, r=10, b=10, t=50)
|
|
523
|
+
)
|
|
524
|
+
return fig
|
|
525
|
+
|
|
526
|
+
fig = go.Figure()
|
|
527
|
+
fig.add_trace(go.Surface(
|
|
528
|
+
x=X, y=Y, z=Z, colorscale='Viridis', opacity=0.85,
|
|
529
|
+
name='f(x, y)', showscale=False
|
|
530
|
+
))
|
|
531
|
+
|
|
532
|
+
fig.add_trace(go.Cone(
|
|
533
|
+
x=Xs, y=Ys, z=Zs, u=Us, v=Vs, w=Ws,
|
|
534
|
+
sizemode='scaled', sizeref=0.35,
|
|
535
|
+
colorscale='Reds', showscale=False,
|
|
536
|
+
name='Gradient ∇f'
|
|
537
|
+
))
|
|
538
|
+
|
|
539
|
+
fig.update_layout(
|
|
540
|
+
title=title,
|
|
541
|
+
scene=dict(
|
|
542
|
+
xaxis_title="x",
|
|
543
|
+
yaxis_title="y",
|
|
544
|
+
zaxis_title="f(x, y)"
|
|
545
|
+
),
|
|
546
|
+
template="plotly_white",
|
|
547
|
+
margin=dict(l=10, r=10, b=10, t=40)
|
|
548
|
+
)
|
|
549
|
+
return fig
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def plot_noise_amplification(t, clean_signal, noisy_signal, d_clean, d_noisy, title="Noise Amplification in Numerical Differentiation"):
|
|
553
|
+
"""
|
|
554
|
+
Two-panel Plotly display contrasting clean vs noisy raw signals and their derivatives.
|
|
555
|
+
"""
|
|
556
|
+
fig = make_subplots(
|
|
557
|
+
rows=2, cols=1, shared_xaxes=True, vertical_spacing=0.09,
|
|
558
|
+
subplot_titles=("Original vs Noisy Signal", "Analytical vs Finite Difference Derivative")
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
fig.add_trace(
|
|
562
|
+
go.Scatter(
|
|
563
|
+
x=t, y=clean_signal, mode='lines', name='Clean Signal',
|
|
564
|
+
line=dict(color='#2563EB', width=2)
|
|
565
|
+
),
|
|
566
|
+
row=1, col=1
|
|
567
|
+
)
|
|
568
|
+
fig.add_trace(
|
|
569
|
+
go.Scatter(
|
|
570
|
+
x=t, y=noisy_signal, mode='lines', name='Noisy Signal',
|
|
571
|
+
line=dict(color='#9CA3AF', width=1, dash='dot')
|
|
572
|
+
),
|
|
573
|
+
row=1, col=1
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
fig.add_trace(
|
|
577
|
+
go.Scatter(
|
|
578
|
+
x=t, y=d_clean, mode='lines', name='Clean Derivative',
|
|
579
|
+
line=dict(color='#2563EB', width=2)
|
|
580
|
+
),
|
|
581
|
+
row=2, col=1
|
|
582
|
+
)
|
|
583
|
+
fig.add_trace(
|
|
584
|
+
go.Scatter(
|
|
585
|
+
x=t, y=d_noisy, mode='lines', name='Noisy Derivative',
|
|
586
|
+
line=dict(color='#DC2626', width=1)
|
|
587
|
+
),
|
|
588
|
+
row=2, col=1
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
fig.update_layout(
|
|
592
|
+
title=title,
|
|
593
|
+
template="plotly_white",
|
|
594
|
+
hovermode="x unified",
|
|
595
|
+
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1)
|
|
596
|
+
)
|
|
597
|
+
fig.update_yaxes(title_text="Signal f(t)", row=1, col=1)
|
|
598
|
+
fig.update_yaxes(title_text="Derivative f'(t)", row=2, col=1)
|
|
599
|
+
fig.update_xaxes(title_text="Time t", row=2, col=1)
|
|
600
|
+
return fig
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
def plot_frequency_noise_spectrum(frequencies, fft_signal, fft_derivative, title="Frequency Spectrum of Signal vs Derivative Noise"):
|
|
604
|
+
"""
|
|
605
|
+
Plotly frequency spectrum safely filtering positive frequencies to eliminate zero-division warnings.
|
|
606
|
+
"""
|
|
607
|
+
freqs = list(frequencies)
|
|
608
|
+
pos_indices = [i for i, f in enumerate(freqs) if f > 0]
|
|
609
|
+
f_pos = [freqs[i] for i in pos_indices]
|
|
610
|
+
sig_mag = [math.fabs(float(fft_signal[i])) for i in pos_indices]
|
|
611
|
+
deriv_mag = [math.fabs(float(fft_derivative[i])) for i in pos_indices]
|
|
612
|
+
|
|
613
|
+
fig = go.Figure()
|
|
614
|
+
fig.add_trace(go.Scatter(
|
|
615
|
+
x=f_pos, y=sig_mag, mode='lines', name='Signal Magnitude |X(f)|',
|
|
616
|
+
line=dict(color='#2563EB', width=2)
|
|
617
|
+
))
|
|
618
|
+
fig.add_trace(go.Scatter(
|
|
619
|
+
x=f_pos, y=deriv_mag, mode='lines', name='Derivative Magnitude |(2πif)X(f)|',
|
|
620
|
+
line=dict(color='#DC2626', width=2)
|
|
621
|
+
))
|
|
622
|
+
|
|
623
|
+
fig.update_layout(
|
|
624
|
+
title=title,
|
|
625
|
+
xaxis_title="Frequency f (Hz)",
|
|
626
|
+
yaxis_title="Magnitude",
|
|
627
|
+
xaxis_type="log",
|
|
628
|
+
yaxis_type="log",
|
|
629
|
+
template="plotly_white",
|
|
630
|
+
legend=dict(x=0.02, y=0.98)
|
|
631
|
+
)
|
|
632
|
+
return fig
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
def plot_uneven_spacing_derivative(x, y, dy_num, dy_true=None, title="Derivative on Unevenly Spaced Grid"):
|
|
636
|
+
"""
|
|
637
|
+
Plotly comparison showing derivative evaluation on an irregularly sampled grid.
|
|
638
|
+
"""
|
|
639
|
+
fig = make_subplots(
|
|
640
|
+
rows=2, cols=1, shared_xaxes=True, vertical_spacing=0.08,
|
|
641
|
+
subplot_titles=("Uneven Grid Samples f(x)", "Numerical Derivative f'(x)")
|
|
642
|
+
)
|
|
643
|
+
fig.add_trace(
|
|
644
|
+
go.Scatter(
|
|
645
|
+
x=x, y=y, mode='lines+markers', name='f(x) Samples',
|
|
646
|
+
line=dict(color='#2563EB', width=1.5),
|
|
647
|
+
marker=dict(size=6, symbol='circle')
|
|
648
|
+
),
|
|
649
|
+
row=1, col=1
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
if dy_true is not None:
|
|
653
|
+
fig.add_trace(
|
|
654
|
+
go.Scatter(
|
|
655
|
+
x=x, y=dy_true, mode='lines', name="True f'(x)",
|
|
656
|
+
line=dict(color='#111827', width=2)
|
|
657
|
+
),
|
|
658
|
+
row=2, col=1
|
|
659
|
+
)
|
|
660
|
+
fig.add_trace(
|
|
661
|
+
go.Scatter(
|
|
662
|
+
x=x, y=dy_num, mode='lines+markers', name="Uneven Stencil f'(x)",
|
|
663
|
+
line=dict(color='#DC2626', width=1.5),
|
|
664
|
+
marker=dict(size=6, symbol='cross')
|
|
665
|
+
),
|
|
666
|
+
row=2, col=1
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
fig.update_layout(
|
|
670
|
+
title=title,
|
|
671
|
+
template="plotly_white",
|
|
672
|
+
hovermode="x unified",
|
|
673
|
+
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1)
|
|
674
|
+
)
|
|
675
|
+
fig.update_yaxes(title_text="f(x)", row=1, col=1)
|
|
676
|
+
fig.update_yaxes(title_text="f'(x)", row=2, col=1)
|
|
677
|
+
fig.update_xaxes(title_text="x", row=2, col=1)
|
|
678
|
+
return fig
|