CoolFace
Apppublic

hexiongwu1995/Wave_on_a_String

sourceHugging Facemitupdated 5mo agoView on Hugging Face
0likes
app.py207 linesDownload Raw Back to root
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)