differ-hug / app.py
componavt's picture
works well 04
e3446a9
Raw
History Blame Contribute Delete
28.5 kB
"""
Gradio UI for the ODE Research Platform.
This module implements a complete Gradio interface for the gene regulatory
ODE system, including parameter controls, simulation execution, and
visualization of results.
"""
import gradio as gr
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import io
import sys
from typing import Tuple, Dict, Any, Optional
from differ_hug.compute import (
build_angles,
build_initial_conditions,
gene_regulatory_rhs,
solve_trajectory_set,
compute_local_zscore,
compute_anomaly_score,
compute_shadowing_diagnostics,
SciPySolver,
)
from differ_hug.plotting import (
make_phase_portrait_figure,
make_time_series_figure,
make_shadowing_figure,
)
from differ_hug.params import parameters_to_text, text_to_parameters
from differ_hug.docs import documentation_markdown, system_latex, get_system_description
# Default parameter values
DEFAULTS = {
"t_number": 100,
"t_train_end": 1.0,
"t_full_end": 3.0,
"alpha": 0.001,
"K": 1.0,
"b": 1.0,
"gamma1": 1.0,
"gamma2": 1.0,
"initial_radius": 0.01,
"num_points": 12,
"circle_start": 0,
"circle_end": 360,
"center_x": 1.0,
"center_y": 1.0,
"solver_type": "SciPy DOP853",
}
DEFAULT_PARAMS_TEXT = parameters_to_text(DEFAULTS)
def guarded_run_simulation(
is_batch_update,
t_number: int,
t_train_end: float,
t_full_end: float,
alpha: float,
K: float,
b: float,
gamma1: float,
gamma2: float,
initial_radius: float,
num_points: int,
circle_start: int,
circle_end: int,
center_x: float,
center_y: float,
solver_type: str,
show_connections: bool,
connection_stride: int,
selected_indices: list,
) -> Tuple[Any, Any, Any, Any, Any, str, str, str, Any]:
"""
Guarded version of run_simulation that skips computation when is_batch_update is True.
"""
if is_batch_update:
return tuple(gr.update() for _ in range(9))
return run_simulation(
t_number, t_train_end, t_full_end,
alpha, K, b, gamma1, gamma2,
initial_radius, num_points, circle_start, circle_end,
center_x, center_y, solver_type, show_connections, connection_stride,
selected_indices
)
def run_simulation(
t_number: int,
t_train_end: float,
t_full_end: float,
alpha: float,
K: float,
b: float,
gamma1: float,
gamma2: float,
initial_radius: float,
num_points: int,
circle_start: int,
circle_end: int,
center_x: float,
center_y: float,
solver_type: str,
show_connections: bool,
connection_stride: int,
selected_indices: list,
) -> Tuple[Any, Any, Any, Any, Any, str, str, str, Any]:
"""
Run the ODE simulation and generate all outputs.
"""
error = validate_parameters(t_number, t_train_end, t_full_end, alpha, num_points)
if error:
return (
None, None, None, None, None,
f"ERROR: {error}",
DEFAULT_PARAMS_TEXT,
system_latex(),
gr.update(choices=[], value=[])
)
diagnostics = []
diagnostics.append("Starting simulation...")
try:
diagnostics.append("Step 1: Building angles and initial conditions...")
angles = build_angles(num_points, circle_start, circle_end)
initial_conditions = build_initial_conditions(center_x, center_y, initial_radius, angles)
diagnostics.append(f" Generated {len(initial_conditions)} initial conditions")
diagnostics.append("Step 2: Creating ODE RHS function...")
rhs_func = gene_regulatory_rhs(alpha, K, b, gamma1, gamma2)
diagnostics.append("Step 3: Setting up solver...")
if solver_type == "SciPy DOP853":
solver_method = "DOP853"
else:
solver_method = "RK45"
t_full = np.linspace(0, t_full_end, t_number)
t_train = np.linspace(0, t_train_end, max(50, t_number // 2))
diagnostics.append("Step 4: Solving trajectories...")
solutions, metrics = solve_trajectory_set(
rhs_func, initial_conditions, t_full, t_train, solver_method
)
diagnostics.append(f" Successfully solved {len(solutions)} trajectories")
if not solutions:
diagnostics.append(" ERROR: No trajectories solved successfully")
return (
None, None, None, None, None,
"\n".join(diagnostics),
DEFAULT_PARAMS_TEXT,
system_latex(),
gr.update(choices=[], value=[])
)
diagnostics.append("Step 5: Computing local z-score...")
local_z = compute_local_zscore(metrics)
for i, m in enumerate(metrics):
m["curv_radius_local_zscore"] = float(local_z[i]) if local_z[i] is not None else np.nan
diagnostics.append("Step 6: Computing anomaly scores...")
df_metrics = pd.DataFrame(metrics)
df_metrics["anomaly_score"] = compute_anomaly_score(df_metrics)
diagnostics.append("Step 7: Building trajectory choices...")
trajectory_update = build_trajectory_choices(df_metrics)
diagnostics.append("Step 8: Creating figures...")
phase_fig = make_phase_portrait_figure(
solutions, selected_indices, solver_type,
t_train_end, t_full_end, t_number,
show_connections, connection_stride
)
time_fig = make_time_series_figure(
solutions, selected_indices, solver_type,
t_train_end, t_full_end, t_number
)
shadowing_fig = make_shadowing_figure(
solutions, selected_indices, solver_type,
t_train_end, t_full_end, t_number, rhs_func
)
if rhs_func is None:
diagnostics.append("WARNING: rhs_func is None, shadowing figure will use zero vector field as fallback (reduced accuracy)")
diagnostics.append("Step 9: Creating metrics table...")
df_to_display = df_metrics.copy()
column_rename_map = {
'curv_radius_mean': 'curv_rad_mn',
'curv_radius_median': 'curv_rad_med',
'curv_radius_std': 'curv_rad_std',
'curv_radius_local_zscore': 'curv_rad_lcl_z',
'curv_count_finite': 'curv_ct_fin'
}
df_to_display = df_to_display.rename(columns=column_rename_map)
df_rounded = df_metrics.round(3)
csv_path = "export_metrics_rounded.csv"
df_rounded.to_csv(csv_path, index=False, float_format="%.3f")
metrics_table_value = df_to_display
diagnostics.append("Step 10: Preparing outputs...")
params_text = parameters_to_text({
"t_number": t_number, "t_train_end": t_train_end, "t_full_end": t_full_end,
"alpha": alpha, "K": K, "b": b, "gamma1": gamma1, "gamma2": gamma2,
"initial_radius": initial_radius, "num_points": num_points,
"circle_start": circle_start, "circle_end": circle_end,
"center_x": center_x, "center_y": center_y, "solver_type": solver_type,
})
status = f"Simulation completed. Solved {len(solutions)} trajectories."
diagnostics.append(status)
diagnostics_text = "\n".join(diagnostics)
return (
phase_fig,
time_fig,
shadowing_fig,
metrics_table_value,
csv_path,
diagnostics_text,
params_text,
system_latex(),
trajectory_update
)
except Exception as e:
error_msg = f"ERROR: {str(e)}"
diagnostics.append(error_msg)
return (
None, None, None, None, None,
"\n".join(diagnostics),
DEFAULT_PARAMS_TEXT,
system_latex(),
gr.update(choices=[], value=[])
)
def apply_text_to_controls(
params_text: str,
t_number: int,
t_train_end: float,
t_full_end: float,
alpha: float,
K: float,
b: float,
gamma1: float,
gamma2: float,
initial_radius: float,
num_points: int,
circle_start: int,
circle_end: int,
center_x: float,
center_y: float,
solver_type: str,
) -> Tuple:
"""
Parse text parameters and update control values.
"""
parsed = text_to_parameters(params_text)
if not parsed:
normalized_text = parameters_to_text({
"t_number": t_number, "t_train_end": t_train_end, "t_full_end": t_full_end,
"alpha": alpha, "K": K, "b": b, "gamma1": gamma1, "gamma2": gamma2,
"initial_radius": initial_radius, "num_points": num_points,
"circle_start": circle_start, "circle_end": circle_end,
"center_x": center_x, "center_y": center_y, "solver_type": solver_type,
})
selected_update = gr.update()
return (
t_number, t_train_end, t_full_end, alpha, K, b,
gamma1, gamma2, initial_radius, num_points,
circle_start, circle_end, center_x, center_y, solver_type,
normalized_text, selected_update
)
int_keys = {"t_number", "num_points", "circle_start", "circle_end"}
float_keys = {
"t_train_end", "t_full_end", "alpha", "K", "b",
"gamma1", "gamma2", "initial_radius", "center_x", "center_y"
}
result = {
"t_number": t_number, "t_train_end": t_train_end, "t_full_end": t_full_end,
"alpha": alpha, "K": K, "b": b, "gamma1": gamma1, "gamma2": gamma2,
"initial_radius": initial_radius, "num_points": num_points,
"circle_start": circle_start, "circle_end": circle_end,
"center_x": center_x, "center_y": center_y, "solver_type": solver_type,
}
for key, val in parsed.items():
if key in int_keys:
try:
result[key] = int(val)
except (TypeError, ValueError):
pass
elif key in float_keys:
try:
result[key] = float(val)
except (TypeError, ValueError):
pass
elif key == "solver_type":
result["solver_type"] = val
elif key == "circle_start_end":
cs, ce = val
result["circle_start"] = cs
result["circle_end"] = ce
if "circle_start" in parsed and "circle_end" in parsed:
cs = int(parsed["circle_start"])
ce = int(parsed["circle_end"])
result["circle_start"] = cs
result["circle_end"] = ce
selected_update = gr.update()
if "num_points" in parsed:
selected_update = gr.update(
choices=list(range(result["num_points"])),
value=list(range(min(5, result["num_points"])))
)
normalized_text = parameters_to_text(result)
return (
result["t_number"], result["t_train_end"], result["t_full_end"],
result["alpha"], result["K"], result["b"], result["gamma1"], result["gamma2"],
result["initial_radius"], result["num_points"],
result["circle_start"], result["circle_end"],
result["center_x"], result["center_y"], result["solver_type"],
normalized_text, selected_update
)
def read_controls_to_text(
t_number: int,
t_train_end: float,
t_full_end: float,
alpha: float,
K: float,
b: float,
gamma1: float,
gamma2: float,
initial_radius: float,
num_points: int,
circle_start: int,
circle_end: int,
center_x: float,
center_y: float,
solver_type: str,
) -> str:
"""
Read current control values and convert to text format.
"""
params = {
"t_number": int(t_number),
"t_train_end": float(t_train_end),
"t_full_end": float(t_full_end),
"alpha": float(alpha),
"K": float(K),
"b": float(b),
"gamma1": float(gamma1),
"gamma2": float(gamma2),
"initial_radius": float(initial_radius),
"num_points": int(num_points),
"circle_start": int(circle_start),
"circle_end": int(circle_end),
"center_x": float(center_x),
"center_y": float(center_y),
"solver_type": solver_type,
}
return parameters_to_text(params)
def update_selected_indices(num_points: int) -> "gr.update":
"""Update selected indices based on num_points."""
n = min(int(num_points), 50)
default_ids = list(range(min(5, n)))
return gr.update(choices=list(range(n)), value=default_ids)
def validate_parameters(t_number, t_train_end, t_full_end, alpha, num_points) -> Optional[str]:
"""Return an error message if parameters are invalid, otherwise None."""
if alpha <= 0:
return "alpha must be strictly positive."
if t_number < 10:
return "t_number must be at least 10."
if num_points < 1:
return "num_points must be at least 1."
if t_full_end <= t_train_end:
return "t_full_end must be greater than t_train_end."
return None
def build_trajectory_choices(df_metrics: pd.DataFrame, top_n: int = 5):
"""Build (choices, default_value) for the trajectory CheckboxGroup, sorted by anomaly_score desc."""
if df_metrics.empty:
return gr.update(choices=[], value=[])
df_sorted = df_metrics.sort_values(by="anomaly_score", ascending=False, na_position="last")
choices = []
for _, row in df_sorted.iterrows():
idx = int(row["idx"])
label = f"{idx}: H={row.get('hurst', float('nan')):.3g} | score={row.get('anomaly_score', float('nan')):.3g}"
choices.append((label, idx))
default_value = [c[1] for c in choices[:top_n]]
return gr.update(choices=choices, value=default_value)
with gr.Blocks(title="Differ Hug: ODE Research Platform") as demo:
gr.Markdown("# Differ Hug: ODE Research Platform")
gr.Markdown("Interactive Gene Regulatory ODE System")
is_batch_update = gr.State(value=False)
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("### Parameters")
t_number = gr.Slider(
label="t_number (time points)", minimum=10, maximum=1000, step=10,
value=DEFAULTS["t_number"]
)
t_train_end = gr.Slider(
label="t_train_end (training end)", minimum=0.1, maximum=5.0, step=0.1,
value=DEFAULTS["t_train_end"]
)
t_full_end = gr.Slider(
label="t_full_end (full integration end)", minimum=0.1, maximum=10.0, step=0.1,
value=DEFAULTS["t_full_end"]
)
gr.Markdown("### System Parameters")
alpha = gr.Number(label="alpha (1/alpha exponent)", value=DEFAULTS["alpha"])
K = gr.Slider(label="K", minimum=0.1, maximum=5.0, step=0.1, value=DEFAULTS["K"])
b = gr.Number(label="b", value=DEFAULTS["b"])
gamma1 = gr.Number(label="gamma1", value=DEFAULTS["gamma1"])
gamma2 = gr.Number(label="gamma2", value=DEFAULTS["gamma2"])
gr.Markdown("### Initial Conditions")
initial_radius = gr.Number(label="Initial radius (R)", value=DEFAULTS["initial_radius"])
num_points = gr.Slider(label="Number of trajectories", minimum=3, maximum=50, step=1,
value=DEFAULTS["num_points"])
circle_start = gr.Slider(label="Circle start (degrees)", minimum=0, maximum=360, step=1,
value=DEFAULTS["circle_start"])
circle_end = gr.Slider(label="Circle end (degrees)", minimum=0, maximum=360, step=1,
value=DEFAULTS["circle_end"])
center_x = gr.Number(label="Center X", value=DEFAULTS["center_x"])
center_y = gr.Number(label="Center Y", value=DEFAULTS["center_y"])
gr.Markdown("### Solver Options")
solver_type = gr.Radio(
choices=["SciPy DOP853", "SciPy RK45"],
label="Solver Type",
value=DEFAULTS["solver_type"]
)
show_connections = gr.Checkbox(
label="Show DOP853 ↔ NN connection lines on phase portrait",
value=False,
info="Controls whether connection lines between DOP853 and NN trajectories are shown on the phase portrait. This does not affect the Shadowing tab."
)
connection_stride = gr.Slider(
label="Connection stride", minimum=1, maximum=20, step=1,
value=5
)
selected_indices = gr.CheckboxGroup(
label="Select trajectories to display (sorted by anomaly score)",
choices=[],
value=[],
interactive=True
)
run_button = gr.Button("Recompute now", variant="secondary")
gr.Markdown("### Plain Text Parameters")
params_text = gr.Textbox(
label="Parameters (edit and apply)",
value=DEFAULT_PARAMS_TEXT,
lines=3,
max_lines=10
)
with gr.Row():
apply_text_btn = gr.Button("Apply text → controls")
read_text_btn = gr.Button("Read controls → text")
with gr.Column(scale=2):
gr.Markdown("### Outputs")
with gr.Tabs():
with gr.TabItem("Phase Portrait"):
phase_fig = gr.Plot(label="Phase Portrait")
with gr.TabItem("Time Series"):
time_fig = gr.Plot(label="Time Series")
with gr.TabItem("Shadowing"):
shadowing_fig = gr.Plot(label="Shadowing Analysis")
with gr.TabItem("Metrics"):
metrics_table = gr.Dataframe(label="Metrics Table", interactive=False)
metrics_csv_file = gr.File(label="Download metrics CSV (rounded)")
with gr.TabItem("Documentation"):
documentation = gr.Markdown(documentation_markdown())
with gr.TabItem("System Equation"):
latex_eq = gr.Markdown(system_latex())
diagnostics_text = gr.Textbox(label="Diagnostics", lines=10)
gr.Markdown("---")
gr.Markdown("**Notes:**")
gr.Markdown("- Anomaly score combines FTLE, path length, max curvature and R² reliability")
gr.Markdown("- Top 5 anomalous trajectories are selected by default")
run_button.click(
fn=guarded_run_simulation,
inputs=[
is_batch_update,
t_number, t_train_end, t_full_end, alpha, K, b, gamma1, gamma2,
initial_radius, num_points, circle_start, circle_end,
center_x, center_y, solver_type, show_connections, connection_stride,
selected_indices
],
outputs=[
phase_fig, time_fig, shadowing_fig, metrics_table, metrics_csv_file,
diagnostics_text, params_text, latex_eq, selected_indices
]
)
sim_inputs = [
t_number, t_train_end, t_full_end, alpha, K, b, gamma1, gamma2,
initial_radius, num_points, circle_start, circle_end,
center_x, center_y, solver_type, show_connections, connection_stride,
selected_indices
]
sim_outputs = [
phase_fig, time_fig, shadowing_fig, metrics_table, metrics_csv_file,
diagnostics_text, params_text, latex_eq, selected_indices
]
for comp in sim_inputs:
comp.change(
fn=guarded_run_simulation,
inputs=[is_batch_update] + sim_inputs,
outputs=sim_outputs
)
apply_text_btn.click(
fn=lambda: True,
inputs=[],
outputs=[is_batch_update]
).then(
fn=apply_text_to_controls,
inputs=[
params_text, t_number, t_train_end, t_full_end, alpha, K, b,
gamma1, gamma2, initial_radius, num_points, circle_start, circle_end,
center_x, center_y, solver_type
],
outputs=[
t_number, t_train_end, t_full_end, alpha, K, b,
gamma1, gamma2, initial_radius, num_points,
circle_start, circle_end, center_x, center_y, solver_type,
params_text, selected_indices
]
).then(
fn=lambda: False,
inputs=[],
outputs=[is_batch_update]
).then(
fn=run_simulation,
inputs=sim_inputs,
outputs=sim_outputs
)
read_text_btn.click(
fn=read_controls_to_text,
inputs=[
t_number, t_train_end, t_full_end, alpha, K, b, gamma1, gamma2,
initial_radius, num_points, circle_start, circle_end,
center_x, center_y, solver_type
],
outputs=params_text
)
num_points.change(
fn=guarded_run_simulation,
inputs=[is_batch_update, t_number, t_train_end, t_full_end, alpha, K, b, gamma1, gamma2,
initial_radius, num_points, circle_start, circle_end, center_x, center_y, solver_type,
show_connections, connection_stride, selected_indices],
outputs=sim_outputs
)
t_train_end.change(
fn=guarded_run_simulation,
inputs=[is_batch_update, t_number, t_train_end, t_full_end, alpha, K, b, gamma1, gamma2,
initial_radius, num_points, circle_start, circle_end, center_x, center_y, solver_type,
show_connections, connection_stride, selected_indices],
outputs=sim_outputs
)
t_full_end.change(
fn=guarded_run_simulation,
inputs=[is_batch_update, t_number, t_train_end, t_full_end, alpha, K, b, gamma1, gamma2,
initial_radius, num_points, circle_start, circle_end, center_x, center_y, solver_type,
show_connections, connection_stride, selected_indices],
outputs=sim_outputs
)
alpha.change(
fn=guarded_run_simulation,
inputs=[is_batch_update, t_number, t_train_end, t_full_end, alpha, K, b, gamma1, gamma2,
initial_radius, num_points, circle_start, circle_end, center_x, center_y, solver_type,
show_connections, connection_stride, selected_indices],
outputs=sim_outputs
)
K.change(
fn=guarded_run_simulation,
inputs=[is_batch_update, t_number, t_train_end, t_full_end, alpha, K, b, gamma1, gamma2,
initial_radius, num_points, circle_start, circle_end, center_x, center_y, solver_type,
show_connections, connection_stride, selected_indices],
outputs=sim_outputs
)
b.change(
fn=guarded_run_simulation,
inputs=[is_batch_update, t_number, t_train_end, t_full_end, alpha, K, b, gamma1, gamma2,
initial_radius, num_points, circle_start, circle_end, center_x, center_y, solver_type,
show_connections, connection_stride, selected_indices],
outputs=sim_outputs
)
gamma1.change(
fn=guarded_run_simulation,
inputs=[is_batch_update, t_number, t_train_end, t_full_end, alpha, K, b, gamma1, gamma2,
initial_radius, num_points, circle_start, circle_end, center_x, center_y, solver_type,
show_connections, connection_stride, selected_indices],
outputs=sim_outputs
)
gamma2.change(
fn=guarded_run_simulation,
inputs=[is_batch_update, t_number, t_train_end, t_full_end, alpha, K, b, gamma1, gamma2,
initial_radius, num_points, circle_start, circle_end, center_x, center_y, solver_type,
show_connections, connection_stride, selected_indices],
outputs=sim_outputs
)
initial_radius.change(
fn=guarded_run_simulation,
inputs=[is_batch_update, t_number, t_train_end, t_full_end, alpha, K, b, gamma1, gamma2,
initial_radius, num_points, circle_start, circle_end, center_x, center_y, solver_type,
show_connections, connection_stride, selected_indices],
outputs=sim_outputs
)
circle_start.change(
fn=guarded_run_simulation,
inputs=[is_batch_update, t_number, t_train_end, t_full_end, alpha, K, b, gamma1, gamma2,
initial_radius, num_points, circle_start, circle_end, center_x, center_y, solver_type,
show_connections, connection_stride, selected_indices],
outputs=sim_outputs
)
circle_end.change(
fn=guarded_run_simulation,
inputs=[is_batch_update, t_number, t_train_end, t_full_end, alpha, K, b, gamma1, gamma2,
initial_radius, num_points, circle_start, circle_end, center_x, center_y, solver_type,
show_connections, connection_stride, selected_indices],
outputs=sim_outputs
)
center_x.change(
fn=guarded_run_simulation,
inputs=[is_batch_update, t_number, t_train_end, t_full_end, alpha, K, b, gamma1, gamma2,
initial_radius, num_points, circle_start, circle_end, center_x, center_y, solver_type,
show_connections, connection_stride, selected_indices],
outputs=sim_outputs
)
center_y.change(
fn=guarded_run_simulation,
inputs=[is_batch_update, t_number, t_train_end, t_full_end, alpha, K, b, gamma1, gamma2,
initial_radius, num_points, circle_start, circle_end, center_x, center_y, solver_type,
show_connections, connection_stride, selected_indices],
outputs=sim_outputs
)
solver_type.change(
fn=guarded_run_simulation,
inputs=[is_batch_update, t_number, t_train_end, t_full_end, alpha, K, b, gamma1, gamma2,
initial_radius, num_points, circle_start, circle_end, center_x, center_y, solver_type,
show_connections, connection_stride, selected_indices],
outputs=sim_outputs
)
show_connections.change(
fn=guarded_run_simulation,
inputs=[is_batch_update, t_number, t_train_end, t_full_end, alpha, K, b, gamma1, gamma2,
initial_radius, num_points, circle_start, circle_end, center_x, center_y, solver_type,
show_connections, connection_stride, selected_indices],
outputs=sim_outputs
)
connection_stride.change(
fn=guarded_run_simulation,
inputs=[is_batch_update, t_number, t_train_end, t_full_end, alpha, K, b, gamma1, gamma2,
initial_radius, num_points, circle_start, circle_end, center_x, center_y, solver_type,
show_connections, connection_stride, selected_indices],
outputs=sim_outputs
)
selected_indices.change(
fn=guarded_run_simulation,
inputs=[is_batch_update, t_number, t_train_end, t_full_end, alpha, K, b, gamma1, gamma2,
initial_radius, num_points, circle_start, circle_end, center_x, center_y, solver_type,
show_connections, connection_stride, selected_indices],
outputs=sim_outputs
)
demo.load(
fn=lambda: (gr.update(value=DEFAULTS["t_number"]),
gr.update(value=DEFAULTS["t_train_end"]),
gr.update(value=DEFAULTS["t_full_end"]),
gr.update(value=DEFAULTS["alpha"]),
gr.update(value=DEFAULTS["K"]),
gr.update(value=DEFAULTS["b"]),
gr.update(value=DEFAULTS["gamma1"]),
gr.update(value=DEFAULTS["gamma2"]),
gr.update(value=DEFAULTS["initial_radius"]),
gr.update(value=DEFAULTS["num_points"]),
gr.update(value=DEFAULTS["circle_start"]),
gr.update(value=DEFAULTS["circle_end"]),
gr.update(value=DEFAULTS["center_x"]),
gr.update(value=DEFAULTS["center_y"])),
outputs=[
t_number, t_train_end, t_full_end, alpha, K, b,
gamma1, gamma2, initial_radius, num_points,
circle_start, circle_end, center_x, center_y
]
)
if __name__ == "__main__":
demo.queue(default_concurrency_limit=1, max_size=20)
demo.launch(server_name="0.0.0.0", server_port=7860, share=False, show_error=True)