• Home
  • CV
  • Numerical Analysis
    • Syllabus
    • Notes
  • Monodromy Tool

Monodromy

#| '!! shinylive warning !!': |
#|   shinylive does not work in self-contained HTML documents.
#|   Please set `embed-resources: false` in your metadata.
#| standalone: true
#| echo: false
from shiny import App, ui, render, reactive
from shinywidgets import output_widget, render_widget
import matplotlib.pyplot as plt
import plotly.graph_objects as go
import numpy as np
#aaa
default_f = "x^3"
default_df = "3*x^2"
default_root = "2"
default_xmin = "-9"
default_xmax = "9"
default_ymin = "-9"
default_ymax = "9"

def monodromy_list(x0,gamma,f,df,n):
    N = len(gamma)-1
    dt = 1/N
    dgamma = [gamma[t+1]-gamma[t] for t in range(0,N)]
    xs = [x0]
    for k in range(0,N):
        xs += [xs[-1] + dgamma[k]/df(xs[-1])]
        for j in range(0,n):
            xs[-1] = xs[-1] - (f(xs[-1]) - gamma[k])/df(xs[-1])
    xreals = [real(x) for x in xs]
    ximags = [imag(x) for x in xs]
    preals = list(map(real,gamma))
    pimags = list(map(imag,gamma))
    return preals,pimags,xreals,ximags

j = complex(0,1)
real = np.real
imag = np.imag
pi = np.pi
e = np.e
sin = np.sin
cos = np.cos
tan = np.tan
ln = np.log
exp = np.exp
log = ln


def monodromy_plotter(preals,pimags,xreals,ximags,style="together",lineshape="spline"):
    color_bg = "#C8E0FF"
    color_button_border = '#0054A9'
    color_path = "#A95400"
    color_lift = "#0054A9"
    dot_size = 10
    N = len(preals)

    base_path = go.Scatter(
        name="base path",
        showlegend=False,
        x = preals,
        y = pimags,
        mode="lines",
        line={"shape": lineshape,
              "color": color_path}
    )
    root_path = go.Scatter(
        name="root path",
        showlegend=False,
        x = xreals,
        y = ximags,
        mode="lines",
        line={"shape": lineshape,
              "color": color_lift}
    )
    base_pt = go.Scatter(
        name="base",
        showlegend=False,
        x = preals[0:1],
        y = pimags[0:1],
        mode="markers",
        marker={"color":color_path,
                "size": dot_size}
    )
    root_pt = go.Scatter(
        name="root",
        showlegend=False,
        x = xreals[0:1],
        y = ximags[0:1],
        mode="markers",
        marker={"color":color_lift,
                "size": dot_size}
    )

    fig = go.Figure()



    if style=="together":
        fig.update(data=[base_pt,root_pt,base_path,root_path])
        fig.update(frames=[go.Frame(traces=[0,1],
                                    data=[go.Scatter(x=preals[k:k+1],y=pimags[k:k+1]),
                                          go.Scatter(x=xreals[k:k+1],y=ximags[k:k+1])]
                                   ) for k in range(0,N+1)
                          ]
                  )
        fig.update_xaxes({"title": "real part",
                          "color": "black",
                          "showgrid": False,
                          "zerolinecolor": "black",
                          "range": [min(np.min(xreals),np.min(preals))-1,
                                    max(np.max(xreals),np.max(preals))+1,],
                          "autorange": False})
        fig.update_yaxes({"title": "imaginary part",
                          "color": "black",
                          "showgrid": False,
                          "zerolinecolor": "black",
                          "range": [min(np.min(ximags),np.min(pimags))-1,
                                    max(np.max(ximags),np.max(pimags))+1,],
                          "autorange": False})
        fig.update_layout(height=700)
    elif style=="separate":
        fig.set_subplots(rows=1,cols=2,subplot_titles=("Lifted Path", "Base Path"))
        fig.add_trace(base_pt,row=1,col=2)
        fig.add_trace(root_pt,row=1,col=1)
        fig.add_trace(base_path,row=1,col=2)
        fig.add_trace(root_path,row=1,col=1)
        fig.update(frames=[go.Frame(traces=[0,1],
                                    data=[go.Scatter(x=preals[k:k+1],y=pimags[k:k+1]),
                                          go.Scatter(x=xreals[k:k+1],y=ximags[k:k+1])]
                                   ) for k in range(0,N+1)
                          ]
                  )
        fig.update_xaxes({"title": "real part",
                          "color": "black",
                          "showgrid": False,
                          "zerolinecolor": "black",
                          "range": [np.min(preals)-1,
                                    np.max(preals)+1,],
                          "autorange": False},
                         row=1, col=2)
        fig.update_yaxes({"title": "imaginary part",
                          "color": "black",
                          "showgrid": False,
                          "zerolinecolor": "black",
                          "range": [np.min(pimags)-1,
                                    np.max(pimags)+1,],
                          "autorange": False},
                         row=1, col=2)
        fig.update_xaxes({"title": "real part",
                          "color": "black",
                          "showgrid": False,
                          "zerolinecolor": "black",
                          "range": [np.min(xreals)-1,
                                    np.max(xreals)+1,],
                          "autorange": False},
                         row=1, col=1)
        fig.update_yaxes({"title": "imaginary part",
                          "color": "black",
                          "showgrid": False,
                          "zerolinecolor": "black",
                          "range": [np.min(ximags)-1,
                                    np.max(ximags)+1,],
                          "autorange": False},
                         row=1, col=1)
        fig.update_layout(height=600)

    fig.update_layout(title={"font":{"color":"black"}},
                        plot_bgcolor=color_bg,
                        paper_bgcolor=color_bg,
                        margin={"l":0,"r":50,"b":0,"t":50},
                        legend={"x": 0.025,
                                "y": 0.975,
                                "font": {"color": "black"},
                                "bgcolor": color_bg},
                        updatemenus = [{"type": "buttons",
                                        "buttons": [{"args": [None,
                                                              {"frame": {"duration": 4000/N,
                                                                         "redraw": True},
                                                                         "fromcurrent": False,
                                                                         "transition": {"duration": 4000/N,
                                                                              "easing": "linear"}}],
                                                     "label": "Animate",
                                                     "method": "animate"}],
                                         "font": {"color": "black"},
                                         "pad": {"l":0,"r":0,"b":0,"t":50},
                                         "bordercolor": "rgba(0,0,0,0)",
                                         "x": 0,
                                         "xanchor": "left",
                                         "y": 1,
                                         "yanchor": "middle"
                                        }]
    )

    return fig

color_bg = "#C8E0FF"
color_dark_bg = "#99CBFF"
color_root = '#0054A9'
color_path = '#A95400'

# styling
katex_and_css = 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; margin: auto; display: block; }
        .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; }
    }

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

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

        .updatemenu-container:hover > .updatemenu-header-group > .updatemenu-button > .updatemenu-item-rect {
          fill: #0054A9 !important;
        }

        .updatemenu-container:hover > .updatemenu-header-group > .updatemenu-button > .updatemenu-item-text {
          fill: #FFFFFF !important;
        }

        .updatemenu-item-rect {
          fill: #99CBFF !important;
          width: 10em;
          height: 44.5px;
          x: -5em;
          y: -22.25px;
        }

        .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;}
        """
    )
)

j = complex(0,1)


fin_card = ui.card(
    ui.card_header("function info"),
    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>"""),
    ui.input_text("root_in","fiber",default_root),
    fill=False
)

path_dim_card = ui.card(
    ui.card_header("path drawing info"),
    ui.layout_columns(
        ui.input_text("xmax_in","xmax",default_xmax,width="6em"),
        ui.input_text("xmin_in","xmin",default_xmin,width="6em"),
        ui.input_text("ymax_in","ymax",default_ymax,width="6em"),
        ui.input_text("ymin_in","ymin",default_ymin,width="6em"),
    col_widths={"sm": (6,6)}
    ),
    fill=False
)

app_ui = ui.page_sidebar(
    ui.sidebar(
        ui.navset_pill(
            ui.nav_panel("path",
                ui.output_plot("draw_plot",
                               height='600px',
                               click=True,
                               hover=ui.hover_opts(delay=10,delay_type="throttle")),
                   ui.input_action_button("run_btn","Set Path",width="10em")
            ),
            ui.nav_panel("function",
                ui.layout_columns(
                    fin_card,
                    path_dim_card,
                    ui.input_action_button("set_btn","Set Info",width="10em"),
                    col_widths={"sm": (12)})
            ),
            ui.nav_panel("about",
                ui.markdown(
"""
A fun little tool to see what monodromy looks like!

There are two other tabs:

- Set the cover and fiber, plus the dimensions of the path drawing interface.
- Draw the "downstairs" path. When the background is dark, it's following your mouse.

Try drawing a loop which goes around zero, then one that doesn't. Then try other directions, or looping multiple times.

Note: it can be very slow and will not work for all display sizes. The implementation is Euler's method with one step of Newton's method path correction.
"""
                            )
                )
        ),
        width="30%",
        position="right",
        bg="#C8E0FF"
    ),
    katex_and_css,
    ui.output_ui("monodromy_plot", height='800px')
)


plot_points = [[],[]]

def server(input, output, session):
        points = reactive.Value([[],[]])
        tracing = reactive.Value(False)
        f = reactive.Value(lambda x : x**3)
        df = reactive.Value(lambda x : 3*x**2)
        xmin = reactive.Value(-8.5)
        xmax = reactive.Value(8.5)
        ymin = reactive.Value(-8.5)
        ymax = reactive.Value(8.5)
        root = reactive.Value(2)

        @reactive.Effect
        @reactive.event(input.set_btn, ignore_none=False)
        def _():
            fstr = input.f_in().replace("^","**")
            dfstr = input.df_in().replace("^","**")
            def fs(x):
                return eval(fstr)
            def dfs(x):
                return eval(dfstr)
            f.set(fs)
            df.set(dfs)
            xmin.set(float(input.xmin_in()))
            xmax.set(float(input.xmax_in()))
            ymin.set(float(input.ymin_in()))
            ymax.set(float(input.ymax_in()))

        @render.plot
        def draw_plot():
            t = tracing.get()

            fig, ax = plt.subplots()

            fig.suptitle("Draw a path for the monodromy method.\n Click to start tracing, click again to stop.")
            fig.set_facecolor(color_bg)

            ax.set_xlim([xmin.get(),xmax.get()])
            ax.set_ylim([ymin.get(),ymax.get()])
            ax.set_box_aspect(1)

            if t:
                ax.set_facecolor(color_dark_bg)
            else:
                ax.set_facecolor(color_bg)

            fr = f.get()(root.get())
            ax.plot(real(fr),imag(fr),'o',color=color_path)

            cx,cy = plot_points
            ax.plot(cx, cy, '-', color=color_path)

            return fig

        @reactive.Effect
        @reactive.event(input.draw_plot_click)
        def _():
            global plot_points
            t = tracing.get()
            if not t:
                plot_points = [[input.draw_plot_hover()['x']],[input.draw_plot_hover()['y']]]
                points.set(plot_points)
            tracing.set(not t)

        @reactive.Effect
        @reactive.event(input.draw_plot_hover)
        def _():
            global plot_points
            if tracing.get():
                currx,curry = points.get()
                currx += [input.draw_plot_hover()['x']]
                curry += [input.draw_plot_hover()['y']]
                plot_points = [currx,curry]
                points.set(plot_points)

        @render.ui
        @reactive.event(input.run_btn)
        def monodromy_plot():
            with reactive.isolate():
                xs,ys = points.get()
                gamma = [xs[k] + j* ys[k] for k in range(0,len(xs))]
                plot_html = monodromy_plotter(
                                *monodromy_list(root.get(),gamma,f.get(),df.get(),1),
                                style="separate",
                                lineshape="spline").to_html(full_html=False,
                                                            auto_play=False)
                return ui.HTML(plot_html)

app = App(app_ui, server)