129 lines
3.3 KiB
Python
129 lines
3.3 KiB
Python
import numpy as np
|
|
import plotly.graph_objects as go
|
|
|
|
"""
|
|
Interactive 3D Plotly animation of the scalar line integral
|
|
∮_C f(x, y) ds with f(x, y) = x · y
|
|
along the unit circle C, plotted on the surface z = f(x,y).
|
|
No files are written; the animation is displayed with interactive controls.
|
|
"""
|
|
|
|
# Parameters
|
|
t_frame = np.linspace(0.0, 2 * np.pi, 120)
|
|
|
|
|
|
# Scalar field definition
|
|
def scalar_field(x, y):
|
|
return x * y
|
|
|
|
|
|
# Surface mesh for scalar field
|
|
lin = np.linspace(-1.2, 1.2, 50)
|
|
X, Y = np.meshgrid(lin, lin)
|
|
Z = scalar_field(X, Y)
|
|
|
|
# Compute running integral along circle
|
|
# Midpoint rule
|
|
mid_ts = (t_frame[1:] + t_frame[:-1]) / 2
|
|
f_mid = scalar_field(np.cos(mid_ts), np.sin(mid_ts))
|
|
dt = t_frame[1:] - t_frame[:-1]
|
|
running = np.concatenate([[0], np.cumsum(f_mid * dt)])
|
|
|
|
# Prepare static surface trace
|
|
surface = go.Surface(
|
|
x=X, y=Y, z=Z, colorscale="RdBu", cmin=-1, cmax=1, opacity=0.6, showscale=False
|
|
)
|
|
|
|
# Full path on surface
|
|
curve_x = np.cos(t_frame)
|
|
curve_y = np.sin(t_frame)
|
|
curve_z = scalar_field(curve_x, curve_y)
|
|
path = go.Scatter3d(
|
|
x=curve_x, y=curve_y, z=curve_z, mode="lines", line=dict(color="black", width=2)
|
|
)
|
|
|
|
# Initial marker at t=0
|
|
t0 = t_frame[0]
|
|
marker = go.Scatter3d(
|
|
x=[np.cos(t0)],
|
|
y=[np.sin(t0)],
|
|
z=[scalar_field(np.cos(t0), np.sin(t0))],
|
|
mode="markers",
|
|
marker=dict(size=5, color="black"),
|
|
)
|
|
|
|
# Build frames for animation
|
|
frames = []
|
|
for i, t in enumerate(t_frame):
|
|
x0, y0 = np.cos(t), np.sin(t)
|
|
z0 = scalar_field(x0, y0)
|
|
frame = go.Frame(
|
|
data=[marker.update(x=[x0], y=[y0], z=[z0])],
|
|
layout=go.Layout(title_text=f"Running integral: {running[i]:.3f}"),
|
|
name=f"frame{i}",
|
|
)
|
|
frames.append(frame)
|
|
|
|
# Layout with sliders and buttons
|
|
graph = go.Figure(
|
|
data=[surface, path, marker],
|
|
layout=go.Layout(
|
|
title="Running integral: 0.000",
|
|
scene=dict(
|
|
xaxis=dict(range=[-1.3, 1.3]),
|
|
yaxis=dict(range=[-1.3, 1.3]),
|
|
zaxis=dict(range=[Z.min(), Z.max()]),
|
|
),
|
|
updatemenus=[
|
|
dict(
|
|
type="buttons",
|
|
showactive=False,
|
|
buttons=[
|
|
dict(
|
|
label="Play",
|
|
method="animate",
|
|
args=[
|
|
None,
|
|
dict(
|
|
frame=dict(duration=50, redraw=True),
|
|
fromcurrent=True,
|
|
transition=dict(duration=0),
|
|
),
|
|
],
|
|
)
|
|
],
|
|
)
|
|
],
|
|
),
|
|
frames=frames,
|
|
)
|
|
|
|
# Add slider
|
|
sliders = [
|
|
dict(
|
|
steps=[
|
|
dict(
|
|
method="animate",
|
|
args=[
|
|
[f.name],
|
|
dict(
|
|
mode="immediate",
|
|
frame=dict(duration=50, redraw=True),
|
|
transition=dict(duration=0),
|
|
),
|
|
],
|
|
label=str(i),
|
|
)
|
|
for i, f in enumerate(frames)
|
|
],
|
|
transition=dict(duration=0),
|
|
x=0,
|
|
y=0,
|
|
currentvalue=dict(prefix="Frame: "),
|
|
len=1.0,
|
|
)
|
|
]
|
|
graph.update_layout(sliders=sliders)
|
|
|
|
graph.show()
|