hexiongwu1995/Wave_on_a_String
0
1"""2弦上波交互式可视化 Web 应用3===============================4使用 Plotly Dash 构建,对两端固定弦在初始三角波激励下的自由振动进行可视化。5针对 Hugging Face Spaces 部署优化。6 7数学模型:8 y(x,t) = Σ b_n · sin(k_n · x) · cos(ω_n · t)9 b_n = (2 A L₀²) / (x₀(L₀-x₀) π² n²) · sin(n π x₀ / L₀)10 k_n = nπ / L₀, ω_n = √(T₀/μ₀) · k_n, c = √(T₀/μ₀)11"""12 13import numpy as np14import plotly.graph_objects as go15from dash import Dash, dcc, html, Input, Output, State, callback, ctx16import dash_bootstrap_components as dbc17 18# =============================================================================19# 核心计算模块20# =============================================================================21 22def compute_displacement(L0, A, x0, T0, mu0, n_terms, t, n_x=500):23 """计算在给定时间 t 时,弦上各点的位移 y(x, t)。"""24 x = np.linspace(0, L0, n_x)25 n = np.arange(1, n_terms + 1).reshape(-1, 1)26 k_n = n * np.pi / L027 omega_n = np.sqrt(T0 / mu0) * k_n28 b_n = (2 * A * L0**2) / (x0 * (L0 - x0) * np.pi**2 * n**2) * np.sin(n * np.pi * x0 / L0)29 y = np.sum(b_n.reshape(-1, 1) * np.sin(k_n * x) * np.cos(omega_n * t), axis=0)30 return x, y31 32 33def compute_frequencies(L0, T0, mu0, n_terms=5):34 """计算波速和各阶模态频率。"""35 c = np.sqrt(T0 / mu0)36 freqs = []37 for n in range(1, n_terms + 1):38 omega_n = c * n * np.pi / L039 freqs.append((n, omega_n / (2 * np.pi)))40 return c, freqs41 42 43def _initial_triangle(L0, A, x0, n_points=100):44 """生成初始三角波形状。"""45 x_left = np.linspace(0, x0, n_points // 2)46 x_right = np.linspace(x0, L0, n_points // 2)47 return (np.concatenate([x_left, x_right]),48 np.concatenate([(A / x0) * x_left, (A / (L0 - x0)) * (L0 - x_right)]))49 50 51def build_figure(L0, A, x0, T0, mu0, n_terms, t):52 """构建 Plotly 图形。"""53 x, y = compute_displacement(L0, A, x0, T0, mu0, n_terms, t)54 x_tri, y_tri = _initial_triangle(L0, A, x0)55 56 fig = go.Figure()57 fig.add_trace(go.Scatter(x=x, y=y, mode="lines", name="y(x, t)",58 line=dict(color="#0066ff", width=3),59 hovertemplate="x = %{x:.2f}<br>y = %{y:.4f}<extra></extra>"))60 fig.add_trace(go.Scatter(x=x_tri, y=y_tri, mode="lines", name="初始波形 (t=0)",61 line=dict(color="gray", width=1.5, dash="dot"),62 hovertemplate="x = %{x:.2f}<br>y_initial = %{y:.4f}<extra></extra>"))63 fig.add_trace(go.Scatter(x=[0, L0], y=[0, 0], mode="markers",64 marker=dict(color="black", size=10), showlegend=False))65 66 fig.update_layout(67 xaxis=dict(title="x (m)", range=[-0.5, L0 + 0.5], zerolinecolor="lightgray", gridcolor="lightgray"),68 yaxis=dict(title="y (m)", range=[-A * 1.3, A * 1.3], zerolinecolor="lightgray",69 gridcolor="lightgray", scaleanchor="x", scaleratio=1),70 margin=dict(l=40, r=20, t=20, b=40), hovermode="x unified",71 legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),72 plot_bgcolor="white", paper_bgcolor="white",73 )74 return fig75 76 77# =============================================================================78# Dash 应用初始化79# =============================================================================80 81app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP],82 title="弦上波可视化 - Wave on a String", update_title=None)83server = app.server84 85# =============================================================================86# 布局87# =============================================================================88 89slider_config = [90 ("弦长 L₀", "slider-L0", 1, 30, 0.5, 15, {1: "1", 10: "10", 20: "20", 30: "30"}),91 ("初始振幅 A", "slider-A", 0.05, 2.0, 0.05, 0.5, {0.05: "0.05", 0.5: "0.5", 1: "1", 2: "2"}),92 ("拨弦位置 x₀", "slider-x0", 0.5, 29.5, 0.5, 5, {0.5: "0.5", 5: "5", 10: "10", 15: "15", 20: "20", 29.5: "29.5"}),93 ("预张力 T₀", "slider-T0", 10, 500, 10, 100, {10: "10", 100: "100", 200: "200", 300: "300", 500: "500"}),94 ("线密度 μ₀", "slider-mu0", 0.01, 1.0, 0.01, 0.1, {0.01: "0.01", 0.1: "0.1", 0.5: "0.5", 1: "1"}),95 ("模态项数 N", "slider-n_terms", 1, 30, 1, 15, {1: "1", 10: "10", 20: "20", 30: "30"}),96]97 98sliders = []99for label, sid, mn, mx, step, val, marks in slider_config:100 sliders.append(html.Label(label, className="form-label mb-0 mt-3"))101 sliders.append(dcc.Slider(id=sid, min=mn, max=mx, step=step, value=val,102 marks=marks, tooltip={"placement": "bottom", "always_visible": True}))103 104app.layout = dbc.Container([105 dbc.Row(dbc.Col(html.H1("弦上波可视化 · Wave on a String",106 className="text-center text-primary my-3 fw-bold"))),107 dbc.Row([108 dbc.Col([109 dbc.Card([dbc.CardHeader("参数控制", className="fw-bold text-primary"),110 dbc.CardBody(sliders)], className="shadow-sm"),111 html.Br(),112 dbc.Card([113 dbc.CardHeader("波速与频率信息", className="fw-bold text-success"),114 dbc.CardBody([html.Div(id="info-wave-speed", className="mb-1"),115 html.Hr(className="my-2"), html.Div(id="info-frequencies")]),116 ], className="shadow-sm"),117 ], width=3, className="pe-0"),118 dbc.Col([119 dcc.Graph(id="graph-wave", config={"displayModeBar": False}, style={"height": "550px"}),120 html.Br(),121 dbc.Card([122 dbc.CardBody([123 dbc.Row([124 dbc.Col(dbc.Button("▶ 播放", id="btn-play", color="primary", className="me-2"), width="auto"),125 dbc.Col(dbc.Button("⏹ 暂停", id="btn-pause", color="secondary", className="me-2"), width="auto"),126 dbc.Col(dbc.Button("⟲ 重置", id="btn-reset", color="info"), width="auto"),127 dbc.Col(html.Div([html.Span("时间 t = "), html.Span(id="display-time", className="fw-bold"),128 html.Span(" s")], className="d-flex align-items-center"), width="auto"),129 ], className="align-items-center"),130 dbc.Row(dbc.Col(dcc.Slider(id="slider-time", min=0, max=10, step=0.01, value=0,131 marks={0: "0", 2: "2", 4: "4", 6: "6", 8: "8", 10: "10"},132 tooltip={"placement": "bottom", "always_visible": True}), width=12),133 className="mt-2"),134 ])135 ], className="shadow-sm"),136 ], width=9),137 ]),138 dcc.Interval(id="interval-animation", interval=50, n_intervals=0, disabled=True),139], fluid=True, className="bg-light min-vh-100")140 141 142# =============================================================================143# 回调函数144# =============================================================================145 146@callback(147 Output("slider-time", "max"),148 Output("info-wave-speed", "children"),149 Output("info-frequencies", "children"),150 Input("slider-L0", "value"), Input("slider-A", "value"), Input("slider-x0", "value"),151 Input("slider-T0", "value"), Input("slider-mu0", "value"), Input("slider-n_terms", "value"),152)153def update_info(L0, A, x0, T0, mu0, n_terms):154 x0 = max(0.1, min(x0, L0 - 0.1))155 c, freqs = compute_frequencies(L0, T0, mu0, 5)156 t_max = max(1 / freqs[0][1] * 5, 2.0) if freqs and freqs[0][1] > 0 else 10.0157 freq_html = [html.Div("前 5 阶模态频率:", className="mb-1")]158 for n, f_n in freqs:159 freq_html.append(html.Div([html.Span(f" f{n} = "), html.Span(f"{f_n:.2f} Hz", className="text-success")],160 className="small"))161 return t_max, [html.Span("波速 c = ", className="fw-bold"), html.Span(f"{c:.2f} m/s", className="text-primary fw-bold")], freq_html162 163 164@callback(165 Output("graph-wave", "figure"),166 Output("display-time", "children"),167 Input("slider-L0", "value"), Input("slider-A", "value"), Input("slider-x0", "value"),168 Input("slider-T0", "value"), Input("slider-mu0", "value"), Input("slider-n_terms", "value"),169 Input("slider-time", "value"),170)171def update_graph(L0, A, x0, T0, mu0, n_terms, t):172 x0 = max(0.1, min(x0, L0 - 0.1))173 return build_figure(L0, A, x0, T0, mu0, n_terms, t or 0), f"{t or 0:.2f}"174 175 176@callback(177 Output("slider-time", "value"),178 Output("interval-animation", "disabled"),179 Input("btn-play", "n_clicks"), Input("btn-pause", "n_clicks"), Input("btn-reset", "n_clicks"),180 Input("interval-animation", "n_intervals"),181 State("slider-time", "value"), State("slider-time", "max"),182 State("interval-animation", "disabled"),183 prevent_initial_call=True,184)185def animate(play_clicks, pause_clicks, reset_clicks, n_intervals, t, t_max, disabled):186 triggered_id = ctx.triggered_id187 188 if triggered_id == "btn-reset":189 return 0, True190 if triggered_id == "btn-pause":191 return t, True192 if triggered_id == "btn-play":193 return t, False194 if triggered_id == "interval-animation" and not disabled:195 new_t = t + 0.05196 if new_t >= t_max:197 return 0, True198 return new_t, False199 return t, disabled200 201 202# =============================================================================203# 入口204# =============================================================================205 206if __name__ == "__main__":207 app.run(debug=False, host="0.0.0.0", port=7860)