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()
Colin Ophus Lab | StanfordColin Ophus Lab | Stanford
Understanding materials, atom by atom — Colin Ophus Lab
Lab Group Website by Curvenote