Contents
Legendre Transformation
import numpy as np
import sympy as sp
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from sympy import symbols, sin, cos, pi, lambdify
def get_slope_line(m, x0, y0, x):
"""Returns the y-values of a line with slope m passing through (x0, y0) evaluated at x."""
return m * (x - x0) + y0
# -- Precompute the "constant" data for U(S) --
V_fixed = np.linspace(0, 2, 100)
U_curve = 0.5 * (V_fixed - 2) **2
# -- Create base figure with subplots --
fig = make_subplots(
rows=1, cols=2,
subplot_titles=("U vs. V", "H vs. P"),
horizontal_spacing=0.1
)
# --------------------------------------------------------
# 1) Left subplot: "U vs. S"
# --------------------------------------------------------
# (a) U(S) line – does NOT change with 's', so we add it once outside frames.
fig.add_trace(
go.Scatter(
x=V_fixed,
y=U_curve,
mode="lines",
line=dict(color="blue"),
showlegend=False
),
row=1, col=1
)
# (b) A dashed horizontal line at U = 1
# We do this using add_shape; it won’t change with frames.
# fig.add_shape(
# type="line",
# x0=0, x1=2, # covers the same range as xlim
# y0=1, y1=1,
# line=dict(color="gray", dash="dash"),
# xref="x1", yref="y1"
# )
# -- Prepare placeholders for the 4 dynamic traces on the left subplot
# (we’ll update them inside frames for each new s):
# 1) Red point at (s, u)
# 2) Tangent line
# 3) Green point for the F-intercept on the y-axis
# 4) (Optional) We can place an annotation or just name the green point “F”.
# Red point (initially just a placeholder)
trace_red_point = go.Scatter(
x=[],
y=[],
mode="markers",
marker=dict(color="red", size=8),
name="(s, U)",
showlegend=False
)
# Tangent line
trace_tangent_line = go.Scatter(
x=[],
y=[],
mode="lines",
line=dict(color="orange"),
name="Tangent line",
showlegend=False
)
# Green intercept (H)
trace_green_intercept = go.Scatter(
x=[],
y=[],
mode="markers+text",
text=["H"], # We can label this point “H”
textposition="top right",
marker=dict(color="green", size=8),
name="H-intercept",
showlegend=False
)
# Add these dynamic traces to the left subplot
fig.add_trace(trace_red_point, row=1, col=1)
fig.add_trace(trace_tangent_line, row=1, col=1)
fig.add_trace(trace_green_intercept, row=1, col=1)
# --------------------------------------------------------
# 2) Right subplot: "F vs. T"
# --------------------------------------------------------
# Similarly, we’ll have 2 dynamic traces on the right subplot:
# 1) Orange point (t, f)
# 2) F(T) line
trace_orange_point = go.Scatter(
x=[],
y=[],
mode="markers",
marker=dict(color="orange", size=8),
name="(t, F)",
showlegend=False
)
trace_F_line = go.Scatter(
x=[],
y=[],
mode="lines",
line=dict(color="green"),
name="F(T)",
showlegend=False
)
# Add these dynamic traces to the right subplot
fig.add_trace(trace_orange_point, row=1, col=2)
fig.add_trace(trace_F_line, row=1, col=2)
# --------------------------------------------------------
# Create frames for each s-value in a range
# --------------------------------------------------------
v_values = np.linspace(0, 2, 51, endpoint=True)
frames = []
for v in v_values:
# Compute values needed
# 1) For left subplot
dUdV = v - 2
u = 0.5 * (v - 2) **2
xlim = (0, 2)
# Slope line endpoints
slope_endpt0 = (xlim[0], get_slope_line(dUdV, v, u, xlim[0]))
slope_endpt1 = (xlim[1], get_slope_line(dUdV, v, u, xlim[1]))
# 2) For right subplot
p = -dUdV
h = u - dUdV * v # F at that point
dUdV_line = np.linspace(xlim[0]-2, v-2, 100)
V_line = np.linspace(xlim[0], v, 100)
P_line = np.linspace(xlim[1], p, 100)
H_line = 0.5 * (V_line-2)**2 - dUdV_line * V_line
frame_data = [
go.Scatter(
x=V_fixed,
y=U_curve,
mode="lines",
line=dict(color="blue"),
),
go.Scatter(x=[v], y=[u], mode="markers"), # red point
go.Scatter(x=[slope_endpt0[0], slope_endpt1[0]],
y=[slope_endpt0[1], slope_endpt1[1]], mode="lines"), # tangent line
go.Scatter(x=[slope_endpt0[0]], y=[slope_endpt0[1]], mode="markers+text", text=["H"]), # green intercept
go.Scatter(x=[p], y=[h], mode="markers"), # orange point
go.Scatter(x=P_line, y=H_line, mode="lines") # H(P) line
]
frames.append(
go.Frame(
data=frame_data,
name=f"v={v:.2f}"
)
)
# --------------------------------------------------------
# Add frames to the figure
# --------------------------------------------------------
fig.frames = frames
# --------------------------------------------------------
# Create the slider
# --------------------------------------------------------
# We'll build one slider that goes over all s-values.
slider_steps = []
for i, v in enumerate(v_values):
step = dict(
method="animate",
args=[
[f"v={v:.2f}"], # frame name
dict(mode="immediate", frame=dict(duration=0, redraw=True), transition=dict(duration=0))
],
label=f"{v:.2f}"
)
slider_steps.append(step)
sliders = [
dict(
active=0,
currentvalue={"prefix": "v = "},
pad={"t": 50},
steps=slider_steps
)
]
# --------------------------------------------------------
# Update figure layout and axis settings
# --------------------------------------------------------
fig.update_xaxes(
range=[0, 2],
title_text="V",
title_font=dict(color="red"),
row=1, col=1
)
fig.update_yaxes(
range=[-2, 4],
title_text="U",
title_font=dict(color="blue"),
row=1, col=1
)
fig.update_xaxes(
range=[0, 2],
title_text="P",
title_font=dict(color="orange"),
row=1, col=2
)
fig.update_yaxes(
range=[-2, 4],
title_text="H",
title_font=dict(color="green"),
row=1, col=2
)
fig.update_layout(
width=700,
height=500,
showlegend=True,
sliders=sliders,
template="simple_white",
)
# --------------------------------------------------------
# Show the figure
# --------------------------------------------------------
fig.show()
# -- Precompute the "constant" data for U(S) --
S_fixed = np.linspace(0, 2, 100)
U_curve = 0.5 * S_fixed**2 + 1
# -- Create base figure with subplots --
fig = make_subplots(
rows=1, cols=2,
subplot_titles=("U vs. S", "F vs. T"),
horizontal_spacing=0.1
)
# --------------------------------------------------------
# 1) Left subplot: "U vs. S"
# --------------------------------------------------------
# (a) U(S) line – does NOT change with 's', so we add it once outside frames.
fig.add_trace(
go.Scatter(
x=S_fixed,
y=U_curve,
mode="lines",
line=dict(color="blue"),
showlegend=False
),
row=1, col=1
)
# (b) A dashed horizontal line at U = 1
# We do this using add_shape; it won’t change with frames.
# fig.add_shape(
# type="line",
# x0=0, x1=2, # covers the same range as xlim
# y0=1, y1=1,
# line=dict(color="gray", dash="dash"),
# xref="x1", yref="y1"
# )
# -- Prepare placeholders for the 4 dynamic traces on the left subplot
# (we’ll update them inside frames for each new s):
# 1) Red point at (s, u)
# 2) Tangent line
# 3) Green point for the F-intercept on the y-axis
# 4) (Optional) We can place an annotation or just name the green point “F”.
# Red point (initially just a placeholder)
trace_red_point = go.Scatter(
x=[],
y=[],
mode="markers",
marker=dict(color="red", size=8),
name="(s, U)",
showlegend=False
)
# Tangent line
trace_tangent_line = go.Scatter(
x=[],
y=[],
mode="lines",
line=dict(color="orange"),
name="Tangent line",
showlegend=False
)
# Green intercept (F)
trace_green_intercept = go.Scatter(
x=[],
y=[],
mode="markers+text",
text=["F"], # We can label this point “F”
textposition="top right",
marker=dict(color="green", size=8),
name="F-intercept",
showlegend=False
)
# Add these dynamic traces to the left subplot
fig.add_trace(trace_red_point, row=1, col=1)
fig.add_trace(trace_tangent_line, row=1, col=1)
fig.add_trace(trace_green_intercept, row=1, col=1)
# --------------------------------------------------------
# 2) Right subplot: "F vs. T"
# --------------------------------------------------------
# Similarly, we’ll have 2 dynamic traces on the right subplot:
# 1) Orange point (t, f)
# 2) F(T) line
trace_orange_point = go.Scatter(
x=[],
y=[],
mode="markers",
marker=dict(color="orange", size=8),
name="(t, F)",
showlegend=False
)
trace_F_line = go.Scatter(
x=[],
y=[],
mode="lines",
line=dict(color="green"),
name="F(T)",
showlegend=False
)
# Add these dynamic traces to the right subplot
fig.add_trace(trace_orange_point, row=1, col=2)
fig.add_trace(trace_F_line, row=1, col=2)
# --------------------------------------------------------
# Create frames for each s-value in a range
# --------------------------------------------------------
s_values = np.linspace(0, 2, 51, endpoint=True)
frames = []
for s in s_values:
# Compute values needed
# 1) For left subplot
dUdS = s
u = 0.5 * s**2 + 1
xlim = (0, 2)
# Slope line endpoints
slope_endpt0 = (xlim[0], get_slope_line(dUdS, s, u, xlim[0]))
slope_endpt1 = (xlim[1], get_slope_line(dUdS, s, u, xlim[1]))
# 2) For right subplot
t = dUdS
f = u - dUdS * s # F at that point
T_line = np.linspace(xlim[0], t, 100)
S_line = np.linspace(xlim[0], s, 100)
F_line = 0.5 * S_line**2 + 1 - T_line * S_line
# Build the data "updates" for each of our dynamic traces (in the same order they were added):
# - trace_red_point -> [s], [u]
# - trace_tangent_line -> [x0, x1], [y0, y1]
# - trace_green_intercept -> [x0], [y0]
# - trace_orange_point -> [t], [f]
# - trace_F_line -> T_line, F_line
frame_data = [
go.Scatter(
x=S_fixed,
y=U_curve,
mode="lines",
line=dict(color="blue"),
),
go.Scatter(x=[s], y=[u], mode="markers"), # red point
go.Scatter(x=[slope_endpt0[0], slope_endpt1[0]],
y=[slope_endpt0[1], slope_endpt1[1]], mode="lines"), # tangent line
go.Scatter(x=[slope_endpt0[0]], y=[slope_endpt0[1]], mode="markers+text", text=["F"]), # green intercept
go.Scatter(x=[t], y=[f], mode="markers"), # orange point
go.Scatter(x=T_line, y=F_line, mode="lines") # F(T) line
]
frames.append(
go.Frame(
data=frame_data,
name=f"s={s:.2f}"
)
)
# --------------------------------------------------------
# Add frames to the figure
# --------------------------------------------------------
fig.frames = frames
# --------------------------------------------------------
# Create the slider
# --------------------------------------------------------
# We'll build one slider that goes over all s-values.
slider_steps = []
for i, s in enumerate(s_values):
step = dict(
method="animate",
args=[
[f"s={s:.2f}"], # frame name
dict(mode="immediate", frame=dict(duration=0, redraw=True), transition=dict(duration=0))
],
label=f"{s:.2f}"
)
slider_steps.append(step)
sliders = [
dict(
active=0,
currentvalue={"prefix": "s = "},
pad={"t": 50},
steps=slider_steps
)
]
# --------------------------------------------------------
# Update figure layout and axis settings
# --------------------------------------------------------
fig.update_xaxes(
range=[0, 2],
title_text="S",
title_font=dict(color="red"),
row=1, col=1
)
fig.update_yaxes(
range=[-2, 4],
title_text="U",
title_font=dict(color="blue"),
row=1, col=1
)
fig.update_xaxes(
range=[0, 2],
title_text="T",
title_font=dict(color="orange"),
row=1, col=2
)
fig.update_yaxes(
range=[-2, 4],
title_text="F",
title_font=dict(color="green"),
row=1, col=2
)
fig.update_layout(
width=700,
height=500,
showlegend=True,
sliders=sliders,
template="simple_white",
# # Buttons to play/pause animation
# updatemenus=[{
# "type": "buttons",
# "buttons": [
# {
# "label": "Play",
# "method": "animate",
# "args": [
# None,
# dict(
# frame=dict(duration=300, redraw=True),
# fromcurrent=True
# )
# ]
# },
# {
# "label": "Pause",
# "method": "animate",
# "args": [
# [None],
# dict(frame=dict(duration=0, redraw=False), mode="immediate")
# ]
# }
# ],
# "pad": {"r": 10, "t": 70},
# "showactive": True,
# "x": 0.1,
# "xanchor": "right",
# "y": 0,
# "yanchor": "top"
# }]
)
# --------------------------------------------------------
# Show the figure
# --------------------------------------------------------
fig.show()
# ------------------------------------------------------------------------------
# 1) Define symbolic variables & symbolic form of F(x,y)
# ------------------------------------------------------------------------------
x, y = symbols('x y', real=True)
A, x1, y1, t, L = symbols('A x1 y1 t L', real=True)
# convenience definitions:
# u = (x - x1)*cos(t) + (y - y1)*sin(t)
# v = -(x - x1)*sin(t) + (y - y1)*cos(t)
u_expr = (x - x1)*cos(t) + (y - y1)*sin(t)
v_expr = -(x - x1)*sin(t) + (y - y1)*cos(t)
# Define F(x,y) symbolically
F_expr = A*v_expr - sin( (2*pi/L) * u_expr )
# Symbolic partial derivatives
Fx_expr = F_expr.diff(x)
Fy_expr = F_expr.diff(y)
# Lambdify them to produce fast numpy-callable functions:
F_lam = lambdify((x, y, A, x1, y1, t, L), F_expr, "numpy")
Fx_lam = lambdify((x, y, A, x1, y1, t, L), Fx_expr, "numpy")
Fy_lam = lambdify((x, y, A, x1, y1, t, L), Fy_expr, "numpy")
def gradF_lam(xv, yv, A_, x1_, y1_, t_, L_):
"""
Return numpy array [dF/dx, dF/dy] at (xv,yv).
"""
return np.array([Fx_lam(xv, yv, A_, x1_, y1_, t_, L_),
Fy_lam(xv, yv, A_, x1_, y1_, t_, L_)])
# ------------------------------------------------------------------------------
# 2) Newton-like projection of a 2D point onto F=0
# ------------------------------------------------------------------------------
def project_onto_curve(
p_guess, A_, x1_, y1_, t_, L_,
max_iter=20, tol=1e-10
):
"""
Given an initial guess p_guess=(xg,yg), project it onto the curve F=0
using a few iterations of a 1D-Newton-like approach in 2D.
Returns (x_proj, y_proj).
"""
xk, yk = p_guess
for _ in range(max_iter):
valF = F_lam(xk, yk, A_, x1_, y1_, t_, L_)
if abs(valF) < tol:
# close enough to zero => done
break
gF = gradF_lam(xk, yk, A_, x1_, y1_, t_, L_)
grad_norm_sq = gF[0]*gF[0] + gF[1]*gF[1]
if grad_norm_sq < 1e-20:
# gradient is essentially zero => can't do normal step
break
# Newton-like update along the normal direction: p_{k+1} = p_k - (F / ||gradF||^2)*gradF
step = valF / grad_norm_sq
xk -= step*gF[0]
yk -= step*gF[1]
return np.array([xk, yk])
# ------------------------------------------------------------------------------
# 3) Main "step + project" traversal
# ------------------------------------------------------------------------------
def traverse_implicit_curve(
A_, x1_, y1_, t_, L_,
start_pt, end_pt,
ds_initial=1.0,
ds_min=1e-3, ds_max=10.0,
tolerance=1e-6
):
"""
Returns a list of points [ (x0,y0), (x1,y1), ... ]
that lie on F=0 from near start_pt to near end_pt
in ~uniform arc-length steps.
Args:
A_, x1_, y1_, t_, L_: parameters for F(x,y).
start_pt, end_pt: (x, y) pairs.
ds_initial: initial guess for step size.
ds_min, ds_max: bounds for step size adaptation.
tolerance: stop if we're within 'tolerance' of end_pt.
Returns:
points_on_curve: list of (x, y) samples on the curve
"""
# 1) Project start point onto the curve F=0
p_current = project_onto_curve(start_pt, A_, x1_, y1_, t_, L_)
points = [p_current]
ds = ds_initial # current step size
while True:
# 2) Check distance to end:
dist_to_end = np.hypot(p_current[0] - end_pt[0],
p_current[1] - end_pt[1])
if dist_to_end < tolerance:
# close enough -> done
break
if dist_to_end < ds:
# The next step is larger than the distance left,
# so we'll just quit or do a partial step. Let's quit for simplicity:
break
# 3) Compute gradient & tangent at p_current
gF = gradF_lam(p_current[0], p_current[1], A_, x1_, y1_, t_, L_)
Fx, Fy = gF[0], gF[1]
denom = np.sqrt(Fx*Fx + Fy*Fy)
if denom < 1e-14:
# tangent undefined => break
break
# Tangent direction is perpendicular to gradF => ( -Fy, Fx ) or ( Fy, -Fx )
# We'll pick the sign that goes "towards" end_pt
tx = -Fy / denom
ty = Fx / denom
dir_to_end = np.array([end_pt[0] - p_current[0],
end_pt[1] - p_current[1]])
dot_val = tx*dir_to_end[0] + ty*dir_to_end[1]
if dot_val < 0.0:
# flip direction if it points away from end_pt
tx *= -1
ty *= -1
# 4) Guess next point
p_guess = p_current + ds*np.array([tx, ty])
# 5) Project guess onto F=0
p_next = project_onto_curve(p_guess, A_, x1_, y1_, t_, L_)
# 6) Measure actual step
actual_ds = np.hypot(p_next[0] - p_current[0],
p_next[1] - p_current[1])
if actual_ds < 1e-12:
# stuck
break
# 7) Adapt ds if actual_ds != ds by a noticeable margin
ratio = ds / actual_ds
# If ratio > 1.05 => actual_ds < 0.95*ds => we can enlarge ds
if ratio > 1.05:
ds = min(ds*1.1, ds_max)
# If ratio < 0.95 => actual_ds > 1.05*ds => we reduce ds
elif ratio < 0.95:
ds = max(ds*0.9, ds_min)
# 8) Accept new point
p_current = p_next
points.append(p_current)
# 9) Safety to prevent infinite loops
if len(points) > 100000:
print("Warning: Too many points. Stopping.")
break
return points
# Set parameters
A_val = 0.06
x1_val = 0.0
y1_val = 0.0
t_val = np.arctan(150/100)
L_val = np.linalg.norm([100, 150])
start_pt = (0.0, 0.0)
end_pt = (100.0, 150.0)
# Traverse
pts = traverse_implicit_curve(
A_val, x1_val, y1_val, t_val, L_val,
start_pt, end_pt,
ds_initial=3,
ds_min=1e-2, ds_max=10,
tolerance=1e-3
)
pts.append(np.array([100, 150]))
pts = np.array(pts)
# --------------------------------------------------------------------------------
# 1) Define U(s,v) and partial derivatives
# --------------------------------------------------------------------------------
s_sym, v_sym = sp.symbols("s v", real=True)
a, b, c, offset = 0.01, 0.005, 150, 100
u_expr = a*s_sym**2 + b*(v_sym - c)**2 + offset
u_func = sp.lambdify((s_sym, v_sym), u_expr, 'numpy')
path_u = u_func(pts[:, 0], pts[:, 1])
du_ds_expr = sp.diff(u_expr, s_sym)
du_dv_expr = sp.diff(u_expr, v_sym)
du_ds_func = sp.lambdify(s_sym, du_ds_expr, 'numpy')
du_dv_func = sp.lambdify(v_sym, du_dv_expr, 'numpy')
def tangent_plane_z(s0, v0, s_grid, v_grid):
"""
z = U(s0, v0)
+ (dU/ds)(s0)*(s_grid - s0)
+ (dU/dv)(v0)*(v_grid - v0)
"""
return (
u_func(s0, v0)
+ du_ds_func(s0)*(s_grid - s0)
+ du_dv_func(v0)*(v_grid - v0)
)
def tangent_z_at_point(s0, v0, x, y):
"""
Tangent-plane Z value at point (x,y),
given the tangent plane defined at (s0,v0).
"""
return (
u_func(s0, v0)
+ du_ds_func(s0)*(x - s0)
+ du_dv_func(v0)*(y - v0)
)
# --------------------------------------------------------------------------------
# 2) Build the main U(s,v) surface on a grid
# --------------------------------------------------------------------------------
s_vals = np.linspace(0, 100, 50)
v_vals = np.linspace(0, 150, 50)
S, V = np.meshgrid(s_vals, v_vals)
U = u_func(S, V)
# --------------------------------------------------------------------------------
# 3) Single parameter t in [0,1]: s(t)=100*t, v(t)=150*t
# --------------------------------------------------------------------------------
t_init = 0.0
s_init = 100.0 * t_init
v_init = 150.0 * t_init
T_plane_init = tangent_plane_z(s_init, v_init, S, V)
# --------------------------------------------------------------------------------
# 4) Curves U(S)|_{v=v_init} and U(V)|_{s=s_init}
# --------------------------------------------------------------------------------
s_line = np.linspace(0, 100, 50)
U_S_line_z_init = u_func(s_line, v_init)
U_S_line_x_init = s_line
U_S_line_y_init = np.full_like(s_line, v_init)
v_line = np.linspace(0, 150, 50)
U_V_line_z_init = u_func(s_init, v_line)
U_V_line_x_init = np.full_like(v_line, s_init)
U_V_line_y_init = v_line
# --------------------------------------------------------------------------------
# 5) Tangent lines for F, H, G intercepts
# --------------------------------------------------------------------------------
def line_in_tangent_plane(s0, v0, x1, y1, steps=2):
"""
Return arrays (x_arr, y_arr, z_arr) for a line in the tangent plane
from (s0, v0) to (x1, y1).
"""
t_arr = np.linspace(0, 1, steps)
x_arr = s0 + t_arr*(x1 - s0)
y_arr = v0 + t_arr*(y1 - v0)
z_arr = [tangent_z_at_point(s0, v0, xx, yy) for xx, yy in zip(x_arr, y_arr)]
return x_arr, y_arr, z_arr
# Lines at t_init
f_line_x_init, f_line_y_init, f_line_z_init = line_in_tangent_plane(s_init, v_init, 0, v_init)
h_line_x_init, h_line_y_init, h_line_z_init = line_in_tangent_plane(s_init, v_init, s_init, 0)
g_line_x_init, g_line_y_init, g_line_z_init = line_in_tangent_plane(s_init, v_init, 0, 0)
# Intercept markers at t_init:
f_marker_z_init = tangent_z_at_point(s_init, v_init, 0, v_init)
h_marker_z_init = tangent_z_at_point(s_init, v_init, s_init, 0)
g_marker_z_init = tangent_z_at_point(s_init, v_init, 0, 0)
# --------------------------------------------------------------------------------
# 6) Construct initial figure with multiple traces
# --------------------------------------------------------------------------------
# directed axis
cone_coords = np.array([
[100, 0, 0], # S axis
[0, 150, 0], # V axis
[0, 0, 350], # U axis
])
cone_vecs = np.array([
[1, 0, 0], # V axis
[0, 1.5, 0], # T axis
[0, 0, 3.5], # P axis
])
fig = go.Figure(
data=[
# (0) The 3D surface U(s,v)
go.Surface(
x=S,
y=V,
z=U,
colorscale="Viridis",
opacity=0.8,
name="U(s,v)",
showscale=False,
showlegend=True
),
# (1) The tangent plane
go.Surface(
x=S,
y=V,
z=T_plane_init,
colorscale="gray",
opacity=0.5,
name="Tangent Plane",
showscale=False,
showlegend=True
),
# (2) The point (s,v) on the surface
go.Scatter3d(
x=[s_init],
y=[v_init],
z=[u_func(s_init, v_init)],
mode="markers+text",
text=["U(S,V)"],
textfont=dict(color="red"),
marker=dict(size=5, color="red"),
name="U(S,V)"
),
# (3) U(S)|_{v=const} curve (red)
go.Scatter3d(
x=U_S_line_x_init,
y=U_S_line_y_init,
z=U_S_line_z_init,
mode="lines",
line=dict(color="red", width=5),
name="U(S)|_{v=const}"
),
# (4) U(V)|_{s=const} curve (blue)
go.Scatter3d(
x=U_V_line_x_init,
y=U_V_line_y_init,
z=U_V_line_z_init,
mode="lines",
line=dict(color="blue", width=5),
name="U(V)|_{s=const}"
),
# (5) F-line: from (s,v) to (0,v)
go.Scatter3d(
x=f_line_x_init,
y=f_line_y_init,
z=f_line_z_init,
mode="lines+markers",
line=dict(color="orange", width=4),
marker=dict(size=2, color="orange"),
name="F-line"
),
# (6) H-line: from (s,v) to (s,0)
go.Scatter3d(
x=h_line_x_init,
y=h_line_y_init,
z=h_line_z_init,
mode="lines",
line=dict(color="magenta", width=4),
# marker=dict(size=2, color="magenta"),
name="H-line"
),
# (7) G-line: from (s,v) to (0,0)
go.Scatter3d(
x=g_line_x_init,
y=g_line_y_init,
z=g_line_z_init,
mode="lines",
line=dict(color="green", width=4),
# marker=dict(size=2, color="green"),
name="G-line"
),
# (8) F-marker at intercept (0,v)
go.Scatter3d(
x=[0],
y=[v_init],
z=[f_marker_z_init],
mode="markers+text",
text=["F(T,V)"],
textposition="top center",
textfont=dict(color="orange"),
marker=dict(color="orange", size=5),
name="F(T,V)"
),
# (9) H-marker at intercept (s,0)
go.Scatter3d(
x=[s_init],
y=[0],
z=[h_marker_z_init],
mode="markers+text",
text=["H(S,P)"],
textposition="top center",
textfont=dict(color="magenta"),
marker=dict(color="magenta", size=5),
name="H(S,P)"
),
# (10) G-marker at intercept (0,0)
go.Scatter3d(
x=[0],
y=[0],
z=[g_marker_z_init],
mode="markers+text",
text=["G(T,P)"],
textposition="top center",
textfont=dict(color="green"),
marker=dict(color="green", size=5),
name="G(T,P)"
),
# Axes cones
go.Cone(
x=cone_coords[:, 0],
y=cone_coords[:, 1],
z=cone_coords[:, 2],
u=cone_vecs[:, 0],
v=cone_vecs[:, 1],
w=cone_vecs[:, 2],
showlegend=False,
showscale=False,
sizemode="absolute",
sizeref=0.25,
colorscale=[[0, "black"], [1, "black"]],
hoverinfo="skip"
),
# Axes lines
go.Scatter3d(
x=[0, 100, None, 0, 0, None, 0, 0],
y=[0, 0, None, 0, 150, None, 0, 0],
z=[0, 0, None, 0, 0, None, 0, 350],
mode="lines",
line=dict(
color="black",
width=5
),
showlegend=False,
hoverinfo="skip"
),
# Axes labels
go.Scatter3d(
x=[100, 0, 0],
y=[0, 150, 0],
z=[0, 0, 350],
mode="text",
text=["S", "V", "U"],
showlegend=False,
hoverinfo="skip"
),
# Trace out the path on the s,v plane
go.Scatter3d(
x=pts[:, 0],
y=pts[:, 1],
z=np.zeros(pts.shape[0]),
mode="lines",
line=dict(color="grey", width=3),
showlegend=False,
hoverinfo="skip"
),
# Trace out the path on the u(s, v) surface
go.Scatter3d(
x=pts[:, 0],
y=pts[:, 1],
z=path_u,
mode="lines",
line=dict(color="grey", width=3),
showlegend=False,
hoverinfo="skip"
),
# The path dot on the s,v plane
go.Scatter3d(
x=[pts[0, 0]],
y=[pts[0, 1]],
z=[0],
mode="markers",
marker=dict(color="grey", size=5),
showlegend=False,
hoverinfo="skip"
),
# Dashed line connecting the U(S, V) to the path dot tracker
go.Scatter3d(
x=[pts[0, 0], pts[0, 0]],
y=[pts[0, 1], pts[0, 1]],
z=[0, path_u[0]],
mode="lines",
line=dict(color="grey", width=3, dash="dash"),
showlegend=False,
hoverinfo="skip"
),
],
layout=go.Layout(
title=f"s={s_init:.1f}, v={v_init:.1f}",
scene=dict(
xaxis_title='',
yaxis_title='',
zaxis_title='',
xaxis=dict(range=[0, 100], autorange=False, showticklabels=False,
showbackground=False, showgrid=False, ticks="",
showline=False, zeroline=False),
yaxis=dict(range=[0, 150], autorange=False, showticklabels=False,
showbackground=False, showgrid=False, ticks="",
showline=False, zeroline=False),
zaxis=dict(range=[0, 350], autorange=False, showticklabels=False,
showbackground=False, showgrid=False, ticks="",
showline=False, zeroline=False),
aspectmode="cube"
),
scene_camera=dict(
eye=dict(x=-1.7, y=-0.5, z=0.6),
projection=dict(
type="orthographic"
)
),
)
)
# --------------------------------------------------------------------------------
# 7) Build the slider steps
# --------------------------------------------------------------------------------
slider_steps = []
n_steps = len(pts) // 2 + 1
for i in range(n_steps):
# t = i/(n_steps - 1) # from 0..1
t = np.clip(0 + i * 2, a_min=0, a_max=pts.shape[0]-1)
s_val = pts[t, 0]
v_val = pts[t, 1]
# (a) Tangent plane
T_plane = tangent_plane_z(s_val, v_val, S, V)
# (b) The point on surface
point_z = [u_func(s_val, v_val)]
# (c) The 1D slices
U_S_line_z = u_func(s_line, v_val)
U_S_line_x = s_line
U_S_line_y = np.full_like(s_line, v_val)
U_V_line_z = u_func(s_val, v_line)
U_V_line_x = np.full_like(v_line, s_val)
U_V_line_y = v_line
# (d) Tangent lines
f_line_x, f_line_y, f_line_z = line_in_tangent_plane(s_val, v_val, 0, v_val)
h_line_x, h_line_y, h_line_z = line_in_tangent_plane(s_val, v_val, s_val, 0)
g_line_x, g_line_y, g_line_z = line_in_tangent_plane(s_val, v_val, 0, 0)
# (e) F, H, G intercept markers
f_marker_z = [tangent_z_at_point(s_val, v_val, 0, v_val)]
h_marker_z = [tangent_z_at_point(s_val, v_val, s_val, 0)]
g_marker_z = [tangent_z_at_point(s_val, v_val, 0, 0)]
step = dict(
method="update",
args=[
# First dict: updating the data for all 11 traces
{
"x": [
S, # (0) main surface
S, # (1) tangent plane
[s_val], # (2) the point
U_S_line_x, # (3) U(S)
U_V_line_x, # (4) U(V)
f_line_x, # (5) F-line
h_line_x, # (6) H-line
g_line_x, # (7) G-line
[0], # (8) F-marker x
[s_val], # (9) H-marker x
[0], # (10) G-marker x,
cone_coords[:, 0],
[0, 100, None, 0, 0, None, 0, 0],
[100, 0, 0],
pts[:, 0],
pts[:, 0],
[s_val],
[s_val, s_val],
],
"y": [
V, # (0) main surface
V, # (1) tangent plane
[v_val], # (2) the point
U_S_line_y, # (3) U(S)
U_V_line_y, # (4) U(V)
f_line_y, # (5) F-line
h_line_y, # (6) H-line
g_line_y, # (7) G-line
[v_val], # (8) F-marker y
[0], # (9) H-marker y
[0], # (10) G-marker y
cone_coords[:, 1],
[0, 0, None, 0, 150, None, 0, 0],
[0, 150, 0],
pts[:, 1],
pts[:, 1],
[v_val],
[v_val, v_val],
],
"z": [
U, # (0) main surface
T_plane, # (1) tangent plane
point_z, # (2) the point
U_S_line_z, # (3) U(S)
U_V_line_z, # (4) U(V)
f_line_z, # (5) F-line
h_line_z, # (6) H-line
g_line_z, # (7) G-line
f_marker_z, # (8) F-marker
h_marker_z, # (9) H-marker
g_marker_z, # (10) G-marker
cone_coords[:, 2],
[0, 0, None, 0, 0, None, 0, 350],
[0, 0, 350],
np.zeros(pts.shape[0]),
path_u,
[0],
[0, path_u[t]]
],
},
# Second dict: layout updates (title, etc.)
{
"title": f"s={s_val:.1f}, v={v_val:.1f}"
}
],
# label=f"Path={t:.1f}"
)
slider_steps.append(step)
single_slider = dict(
active=0,
# pad={"t": 20},
# currentvalue={"prefix": "t: "},
steps=slider_steps
)
fig.update_layout(
width=700,
height=500,
sliders=[single_slider],
template="simple_white",
legend=dict(
orientation="h",
xanchor="right",
yanchor="bottom",
x=1, y=1.02,
)
)
fig.show()