Spaces:
Sleeping
Sleeping
import streamlit as st | |
import jax | |
import jax.numpy as jnp | |
import matplotlib.pyplot as plt | |
def parabola_fn(x): | |
return x**0.5 | |
def circle_fn(x): | |
return (1 - x**2) ** 0.5 | |
d_parabola_fn = jax.grad(parabola_fn) | |
d_circle_fn = jax.grad(circle_fn) | |
def loss_fn(params): | |
x1 = params["x1"] | |
x2 = params["x2"] | |
# parpendicular line to the tangent of the parabola: y = m1 * x + c1 | |
m1 = -1 / d_parabola_fn(x1) | |
c1 = parabola_fn(x1) - m1 * x1 | |
def perpendicular_parabola_fn(x): | |
return m1 * x + c1 | |
# parpendicular line to the tangent of the circle: y = m2 * x + c2 | |
m2 = -1 / d_circle_fn(x2) | |
c2 = circle_fn(x2) - m2 * x2 | |
def perpendicular_circle_fn(x): | |
return m2 * x + c2 | |
# x_star and y_star are the intersection of the two lines | |
x_star = (c2 - c1) / (m1 - m2) | |
y_star = m1 * x_star + c1 | |
# three quantities should be equal to each other | |
# 1. distance between intersection and parabola | |
# 2. distance between intersection and circle | |
# 3. distance between intersection and x=0 line | |
d1 = (x_star - x1) ** 2 + (y_star - parabola_fn(x1)) ** 2 | |
d2 = (x_star - x2) ** 2 + (y_star - circle_fn(x2)) ** 2 | |
d3 = x_star**2 | |
aux = { | |
"x_star": x_star, | |
"y_star": y_star, | |
"perpendicular_parabola_fn": perpendicular_parabola_fn, | |
"perpendicular_circle_fn": perpendicular_circle_fn, | |
"r": d1**0.5, | |
} | |
# final loss | |
loss = (d1 - d2) ** 2 + (d1 - d3) ** 2 + (d2 - d3) ** 2 | |
return loss, aux | |
x = jnp.linspace(0, 1, 100) | |
st.title("Radius of the Circle: Optimization Playground") | |
st.markdown( | |
r""" | |
Inspired from: https://twitter.com/iwontoffendyou/status/1704935240907518367 | |
Optimize the radius of the circle such that it is tangent to the parabola, unit circle and the x=0 line | |
Method: | |
- The inner circle is tangent to the parabola at $x=x_1$ and tangent to the unit circle at $x=x_2$. | |
- Let's call the center of the inner circle as $(x^*, y^*)$. | |
- We know that ditances between $(x^*, y^*)$ and the parabola, unit circle and the x=0 line should be equal to each other. | |
- First, we can find analytical forms of perpendicular lines shown in the figure. They have the form of $y = m * x + c$ where $m = -\frac{1}{f'(x)}$ and $c = f(x) - m * x$. | |
- Perpendicular line to the parabola: $y = m_1 * x + c_1$ | |
- Perpendicular line to the unit circle: $y = m_2 * x + c_2$ | |
- The intersection of the two lines is $(x^*, y^*)$. $x^* = \frac{c_2 - c_1}{m_1 -m_2}$ and $y^* = m_1 * x^* + c_1$ or $y^* = m_2 * x^* + c_2$. | |
- We define three distances: $d_1 = (x^* - x_1)^2 + (y^* - f(x_1))^2$, $d_2 = (x^* - x_2)^2 + (y^* - f(x_2))^2$ and $d_3 = {x^*}^2$. | |
- Our loss function is $L = (d_1 - d_2)^2 + (d_1 - d_3)^2 + (d_2 - d_3)^2$. | |
""" | |
) | |
col1, col2 = st.columns(2) | |
x1 = col1.slider("initial x1 (x intersection with parabola)", 0.0, 1.0, 0.5) | |
x2 = col1.slider("initial x2 (x intersection with the circle)", 0.0, 1.0, 0.5) | |
n_epochs = col2.slider("n_epochs", 0, 1000, 50) | |
lr = col2.slider("lr", 0.0, 1.0, value=0.1, step=0.01) | |
# submit button | |
submit = st.button("submit") | |
# when submit button is clicked run the following code | |
params = {"x1": x1, "x2": x2} | |
losses = [] | |
value_and_grad_fn = jax.value_and_grad(loss_fn, has_aux=True) | |
# initialize plot | |
fig, axes = plt.subplots(1, 2, figsize=(12, 6)) | |
axes[0].set_xlim(0, 1) | |
axes[0].set_ylim(0, 1) | |
axes[0].set_aspect("equal") | |
value, aux = loss_fn(params) | |
(pbola_plot,) = axes[0].plot(x, parabola_fn(x), color="red") | |
(pbola_perpendicular_plot,) = axes[0].plot(x, aux["perpendicular_parabola_fn"](x), color="red", linestyle="--") | |
(cicle_plot,) = axes[0].plot(x, circle_fn(x), color="blue") | |
(circle_perpendicular_plot,) = axes[0].plot(x, aux["perpendicular_circle_fn"](x), color="blue", linestyle="--") | |
x_star, y_star = aux["x_star"], aux["y_star"] | |
(x0_perpendicular_plot,) = axes[0].plot([0, 1], [y_star, y_star], color="black", linestyle="--") | |
radius = aux["r"] | |
axes[0].add_patch(plt.Circle((x_star, y_star), radius, fill=False)) | |
axes[1].set_xlim(0, n_epochs) | |
axes[1].set_ylim(0, value) | |
(loss_plot,) = axes[1].plot(losses, color="black") | |
pbar = st.progress(0) | |
with st.empty(): | |
st.pyplot(fig) | |
if submit: | |
for i in range(n_epochs): | |
(value, _), grad = value_and_grad_fn(params) | |
params["x1"] -= lr * grad["x1"] | |
params["x2"] -= lr * grad["x2"] | |
losses.append(value) | |
_, aux = loss_fn(params) | |
print(params, grad, lr) | |
pbola_plot.set_data(x, parabola_fn(x)) | |
pbola_perpendicular_plot.set_data(x, aux["perpendicular_parabola_fn"](x)) | |
cicle_plot.set_data(x, circle_fn(x)) | |
circle_perpendicular_plot.set_data(x, aux["perpendicular_circle_fn"](x)) | |
x_star, y_star = aux["x_star"], aux["y_star"] | |
x0_perpendicular_plot.set_data([0, 1], [y_star, y_star]) | |
radius = aux["r"] | |
axes[0].add_patch(plt.Circle((x_star, y_star), radius, fill=False)) | |
loss_plot.set_data(range(len(losses)), losses) | |
pbar.progress(i / n_epochs) | |
axes[0].set_title(f"x1: {params['x1']:.3f}, x2: {params['x2']:.3f} \n r: {radius:.4f}") | |
axes[1].set_title(f"epoch: {i}, loss: {value:.5f}") | |
st.pyplot(fig) | |