15  Homotopy Lab

Experiment with your own homotopies.

#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| echo: false
#| height: 2100px
import numpy as np
import plotly.graph_objects as go
from shiny import reactive, render
from shiny.express import ui, input

# usual abbreviations
pi = np.pi
e = np.e
sin = np.sin
cos = np.cos
tan = np.tan
ln = np.log
exp = np.exp
log = ln

default_f = "20*sin(x) - 5"
default_df = "20*cos(x)"
default_g = "x^3 - 4*x"
default_dg = "3*x^2 - 4"
default_root = "2"
default_xmin = "-0.5"
default_xmax = "2.5"

# styling
with ui.tags.head():
    ui.tags.link(
        rel="stylesheet",
        href="https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/katex.min.css"
    ),
    ui.tags.script(src="https://cdn.jsdelivr.net/npm/katex@0.16.0/dist/katex.min.js")

    ui.tags.style(
        """
        body { background: #C8E0FF !important; }
        .irs-bar { background: #0054A9 !important; }
        .irs-handle { background: #0054A9 !important; }
        .irs-single { background: #0054A9 !important; color: #FFFFFF !important; }
        .irs-min { background:#99CBFF !important; color: #000000 !important; }
        .irs-max { background:#99CBFF !important; color: #000000 !important; }
        .control-label { display: block; text-align: center; }
        .shiny-input-container { margin:auto; }
        .shiny-input-select { background-color: #99CBFF; border: 1px solid #0054A9; }
        .shiny-input-number { background-color: #99CBFF; border: 1px solid #0054A9; }
        .shiny-input-number:focus { background-color: #99CBFF; border: 1px solid #0054A9; }
        .shiny-input-text { background-color: #99CBFF; border: 1px solid #0054A9; }
        .shiny-input-text:focus { background-color: #99CBFF; border: 1px solid #0054A9; }
        .action-button { background-color: #99CBFF; color: black; border: none;}
        .action-button:hover { background-color: #0054A9; color: white; border-color: none;}
        .card { background: #C8E0FF; border: 1px solid #0054A9; box-shadow: none; }
        .card-header { text-align: center; border-bottom: 1px solid #0054A9; }
    }
        .main-svg {
          background: none !important;
        }

        .plotly-graph-div {
          margin: auto;
        }

        .updatemenu-button {
          transform: translate(50%, 8%);
        }

        .updatemenu-item-rect {
          fill: #99CBFF !important;
          width: 5em;
          height: 2em;
          x: -2.5em;
          y: -1em;
        }

        .updatemenu-item-text {
          text-anchor: middle;
          dominant-baseline: central;
          font-size: 0.9375rem !important;
          transform: translate(-12px, -19.5px); /* evil hack */
        }

        .modebar-container {visibility: hidden;}
        """
    )

ui.page_opts(full_width=True)

with ui.layout_columns(col_widths=(-1,5,5,-1)):
    with ui.card(fill=False):
        ui.card_header("target function")
        ui.input_text("f_in", "$f(x)$", default_f)
        ui.HTML("""<script> katex.render("f(x)", window['f_in-label']);</script>""")
        ui.input_text("df_in", "$f'(x)$", default_df)
        ui.HTML("""<script> katex.render("f'(x)", window['df_in-label']);</script>""")
    with ui.card():
        ui.card_header("simple function")
        ui.input_text("g_in", "$g(x)$", default_g)
        ui.HTML("""<script> katex.render("g(x)", window['g_in-label']);</script>""")
        ui.input_text("dg_in", "$g'(x)$", default_dg)
        ui.HTML("""<script> katex.render("g'(x)", window['dg_in-label']);</script>""")
        ui.input_text("g_root", "root of $g(x)$", default_root)
        ui.HTML(r"""<script> katex.render("\\text{root of\\ } g(x)", window['g_root-label']);</script>""")

with ui.layout_columns(col_widths=(-1,5,5,-1)):
    with ui.card(fill=False):
        ui.card_header("method options")
        ui.input_text("N_in", "Number of EM steps:", "10")
        ui.input_text("n_in", "Number of NM steps:", "1")
    with ui.card():
        ui.card_header("rendering options")
        ui.input_text("aN_in", "EM steps for actual path", "40")
        ui.input_text("an_in", "NM steps for actual path", "2")
        ui.input_text("xmin_in", "minimum x for plotting", default_xmin)
        ui.input_text("xmax_in", "maximum x for plotting", default_xmax)
        ui.input_text("dpi", "number of points for plotting the surface", "100")

with ui.layout_columns(col_widths=(-4,4,-4)):
    ui.input_action_button("run_button", "Run")


def path_correction_step(x,t,H,dHdx):
    return t, x - H(x,t)/dHdx(x,t)

def euler_step(x,t,dx,dt):
    return t+dt, x + dt*dx(x,t)

def HM_list(x0,N,n,H,dHdx,dx):
    ts = [0]
    xs = [x0]
    dt = float(1/N) # not strictly necessary in python

    for k in range(0,N):
        t,x = ts[-1], xs[-1]
        T,X = euler_step(x,t,dx,dt)
        ts += [T]
        xs += [X]
        for j in range(0,n):
            T,X = path_correction_step(X,T,H,dHdx)
            ts += [T]
            xs += [X]
    return ts,xs


@render.ui()
@reactive.event(input.run_button,ignore_none=False)
def homotopy_method():
    try:
        def f(x):
            return eval(input.f_in().replace("^","**"))
        f(0)
    except:
        return ui.HTML("""<p>There appears to have been an issue processing your
                          <span id="err">x</span>.</p>
                          <script> katex.render("f(x)", err);</script>
                       """)
    try:
        def df(x):
            return eval(input.df_in().replace("^","**"))
        df(0)
    except:
        return ui.HTML("""<p>There appears to have been an issue processing your
                          <span id="err">x</span>.</p>
                          <script> katex.render("f'(x)", err);</script>
                       """)
    try:
        def g(x):
            return eval(input.g_in().replace("^","**"))
        g(0)
    except:
        return ui.HTML("""<p>There appears to have been an issue processing your
                          <span id="err">x</span>.</p>
                          <script> katex.render("g(x)", err);</script>
                       """)
    try:
        def dg(x):
            return eval(input.dg_in().replace("^","**"))
        dg(0)
    except:
        return ui.HTML("""<p>There appears to have been an issue processing your
                          <span id="err">x</span>.</p>
                          <script> katex.render("g'(x)", err);</script>
                       """)
    try:
        x0 = eval(input.g_root())
    except:
        return ui.HTML("""<p>There appears to have been an issue processing the root of
                          <span id="err">x</span>.</p>
                          <script> katex.render("g(x)", err);</script>
                       """)
    try:
        N = eval(input.N_in())
    except:
        return ui.HTML("""<p>There appears to have been an issue processing number of EM steps.</p>""")
    try:
        n = eval(input.n_in())
    except:
        return ui.HTML("""<p>There appears to have been an issue processing number of NM steps.</p>""")
    try:
        aN = eval(input.aN_in())
    except:
        return ui.HTML("""<p>There appears to have been an issue processing number of EM steps for the actual path.</p>""")
    try:
        an = eval(input.an_in())
    except:
        return ui.HTML("""<p>There appears to have been an issue processing number of NM steps for the actual path.</p>""")
    try:
        xmin = eval(input.xmin_in())
    except:
        return ui.HTML("""<p>There appears to have been an issue processing minimum x for your plot.</p>""")
    try:
        xmax = eval(input.xmax_in())
    except:
        return ui.HTML("""<p>There appears to have been an issue processing minimum x for your plot.</p>""")
    try:
        dpi = eval(input.dpi())
    except:
        return ui.HTML("""<p>There appears to have been an issue processing number of points to use for plotting the surface.</p>""")

    # one million input checks later, we resume.

    def H(x,t):
        return t*f(x) + (1-t)*g(x)
    def dHdx(x,t):
        return t*df(x) + (1-t)*dg(x)#
    def dHdt(x,t):
        return f(x) - g(x)
    def dx(x,t):
        return -dHdt(x,t)/dHdx(x,t)

    # wahoo
    Ts, Xs = HM_list(x0,N,n,H,dHdx,dx)
    ts, xs = HM_list(x0,aN,an,H,dHdx,dx)
    ts = ts[::an+1]
    xs = xs[::an+1]

    # colors
    color_bg = "#C8E0FF"
    color_root = '#33FF99'
    color_est = '#3399FF'
    color_button_border = '#0054A9'
    color_button_bg = "#99CBFF"
    color_fun = "#0054A9"
    color_homotopy = '#3399FF'
    pt_color = "#0054A9"
    line_thickness = 8
    marker_radius = 8
    width_slice = 16
    width_root = 16

    xsp = np.linspace(xmin,xmax,dpi)
    tsp = np.linspace(0,1,dpi)

    homotopy_surface = go.Surface(
        name="homotopy",
        colorscale=[[0,color_homotopy], [1,color_homotopy]],
        cmid=0,
        showscale=False,
        x = xsp,
        y = tsp,
        z = [[H(x,t) for x in xsp] for t in tsp])
    f_slice = go.Scatter3d(
        name="f(x)=H(x,0)",
        mode="lines",
        line={"color":color_fun, "width":width_slice},
        x = xsp,
        y = [0 for _ in xsp],
        z = [H(x,0) for x in xsp])
    g_slice = go.Scatter3d(
        name="g(x)=H(x,1)",
        mode="lines",
        line={"color":color_fun, "width":width_slice},
        x = xsp,
        y = [1 for _ in xsp],
        z = [H(x,1) for x in xsp])
    root_path = go.Scatter3d(
        name="H(x,t) = 0",
        mode="lines",
        line={"color":color_root, "width":width_root},
        x = xs,
        y = ts,
        z = len(ts)*[0])

    surface_fig = go.Figure(data = [homotopy_surface, f_slice, g_slice, root_path])
    surface_fig.update_layout(
        scene = {
            "xaxis" : {"title":"x",
                       "color":"black",
                       "gridcolor":"black",
                       "zeroline":False,
                       "backgroundcolor":color_bg},
            "yaxis" : {"title": "t",
                       "color":"black",
                       "gridcolor":"black",
                       "zeroline":False,
                       "backgroundcolor":color_bg},
            "zaxis" : {"title": "H(x,t)",
                       "color":"black",
                       "showgrid":False,
                       "zeroline":False,
                       "backgroundcolor":color_bg},
            "camera_eye": {"x": -0.8, "y": -1.2, "z": 0.8},
            "aspectratio": {"x": 1, "y": 1, "z": 0.8},
            "bgcolor": color_bg
        },
        plot_bgcolor=color_bg,
        paper_bgcolor=color_bg,
        autosize=True,
        height=600,
        margin={"l":0,"r":0,"b":0,"t":0},
        legend={"x": 0.1, "y": 0.9, "font":{"color":"black"}, "bgcolor": "rgba(255,255,255,0)"}
    )

    pt_count = N*(n + 1) + 1

    anim_fig = go.Figure(
        data = [go.Scatter(x=ts,
                           y=xs,
                           name="path from root",
                           mode="lines",
                           line={"color":color_root,
                                 "width": line_thickness}),
                go.Scatter(x=(pt_count)*[0],
                           y=(pt_count)*[x0],
                           name="estimate",
                           mode="markers",
                           marker={"color":pt_color, "size":10})])

    anim_fig.update_xaxes(linewidth=1,linecolor="black",zeroline=False)
    anim_fig.update_yaxes(linewidth=1,linecolor="black",zeroline=False)

    anim_fig.update_layout({"xaxis": {"title": "t",
                                    "color": "black",
                                    "gridcolor": "black",
                                    "range": [-0.05,1.05],
                                    "autorange": False},
                          "yaxis": {"title":"x",
                                    "color": "black",
                                    "gridcolor": "black",
                                    "range": [xmin,xmax]}},
                         plot_bgcolor=color_bg,
                         paper_bgcolor=color_bg,
                         height=700,
                         modebar={},
                         margin={"l":0,"r":0,"b":0,"t":50},
                         legend={"x": 0.05,
                                 "y": 0.95,
                                 "font": {"color": "black"},
                                 "bgcolor": color_bg},
                         updatemenus = [{"type": "buttons",
                                         "buttons": [{"args": [None,
                                                               {"frame": {"duration": 400},
                                                                "fromcurrent": True,
                                                                "transition": {"duration": 400}}],
                                                      "label": "Play",
                                                      "method": "animate"
                                                     }],
                                          "font": {"color": "black"},
                                          "pad": {"l":0,"r":0,"b":0,"t":50},
                                          "bordercolor": color_button_border,
                                          "x": 0.5,
                                          "xanchor": "center",
                                          "y": 1.1,
                                          "yanchor": "middle"
                                        }]
                        )

    anim_fig.update(frames=[go.Frame(traces=[1],
                                   data=[go.Scatter(x=[Ts[j] for j in range(0,k)] + (pt_count-k+1)*[Ts[k]],
                                                    y=[Xs[j] for j in range(0,k)] + (pt_count-k+1)*[Xs[k]],
                                                    name="estimate",
                                                    mode="markers")]) for k in range(0,pt_count)]
                 )



    return ui.HTML(surface_fig.to_html(full_html=False)), ui.HTML(anim_fig.to_html(full_html=False, auto_play=False))