File size: 11,839 Bytes
6a18893 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 | <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Interactive Lesson: Damped SDOF under Harmonic Load</title>
<!-- Plotly (verified CDN) -->
<script src="https://cdn.plot.ly/plotly-2.35.2.min.js"></script>
<!-- MathJax for equations -->
<script defer src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
<style>
:root { --bg:#0b1020; --card:#121a32; --ink:#e8eefc; --muted:#9fb1ff; --line:#273154; --accent:#8ec7ff;}
html,body{background:var(--bg); color:var(--ink); font-family:system-ui,Segoe UI,Roboto,Arial,sans-serif; margin:0}
header{padding:22px 20px 10px}
h1{margin:0 0 6px; font-size:20px}
.wrap{padding:0 20px 24px}
.card{background:var(--card); border:1px solid #293456; border-radius:16px; padding:16px; margin:12px 0; box-shadow:0 6px 22px rgba(0,0,0,.25)}
.row{display:grid; grid-template-columns:repeat(auto-fit,minmax(180px,1fr)); gap:12px}
label{display:block; font-size:13px; color:var(--muted); margin:8px 0 4px}
input,select{width:100%; padding:10px; border-radius:10px; border:1px solid #2c3a60; background:#0f1630; color:var(--ink)}
button{padding:10px 14px; border-radius:10px; border:1px solid #2c3a60; background:#1b2650; color:var(--ink); cursor:pointer}
button:hover{filter:brightness(1.12)}
.tiny{color:var(--muted); font-size:12px}
.kpi{display:flex; flex-wrap:wrap; gap:16px; margin-top:6px}
.kpi div{background:#0f1630; border:1px dashed #2c3a60; border-radius:10px; padding:8px 10px; font-variant-numeric:tabular-nums}
details{background:#0f1630; border:1px solid #2c3a60; border-radius:10px; padding:10px 12px}
details summary{cursor:pointer; color:var(--accent)}
a{color:var(--accent)}
</style>
</head>
<body>
<header>
<h1>Interactive Lesson: Damped SDOF under Harmonic Load</h1>
<div class="tiny">Problem → Theory → Interactive Solution → Quick Check — all in your browser.</div>
</header>
<div class="wrap">
<!-- Problem Statement -->
<section class="card">
<h2 style="margin:0 0 8px;font-size:18px">1) Problem</h2>
<p>
A single-degree-of-freedom system with mass \(m\), stiffness \(k\), and damping ratio \(\zeta\) is subjected
to a sinusoidal force \(F(t)=F_0\sin(\omega t)\).
Determine and visualize the displacement response \(x(t)\), and study the steady-state
frequency response.
</p>
<p class="tiny">Governing ODE: \(\ddot x + 2\zeta\omega_n \dot x + \omega_n^2 x = \dfrac{F_0}{m}\sin(\omega t)\), where \(\omega_n=\sqrt{k/m}\).</p>
</section>
<!-- Theory -->
<section class="card">
<h2 style="margin:0 0 8px;font-size:18px">2) Theory</h2>
<p>
The steady-state amplitude under harmonic excitation is
\[
|X(\omega)| = \frac{F_0/k}{\sqrt{(1-r^2)^2+(2\zeta r)^2}},\quad r=\frac{\omega}{\omega_n}.
\]
The phase lag is
\[
\phi(\omega)=\tan^{-1}\!\left(\frac{2\zeta r}{1-r^2}\right).
\]
</p>
<details>
<summary>Show derivation (outline)</summary>
<p class="tiny">
Assume steady state \(x_p=A\sin(\omega t-\phi)\), substitute in ODE, match sine/cosine terms to get
amplitude and phase. The complete response is \(x(t)=x_h(t)+x_p(t)\); the homogeneous part decays for \(\zeta>0\).
</p>
</details>
</section>
<!-- Interactive Controls -->
<section class="card">
<h2 style="margin:0 0 8px;font-size:18px">3) Interactive Solution</h2>
<div class="row">
<div><label>Preset</label>
<select id="preset">
<option value="custom">— custom —</option>
<option value="light">Light damping (ζ=0.02, resonance scan)</option>
<option value="moderate">Moderate damping (ζ=0.07)</option>
<option value="heavy">Heavy damping (ζ=0.2)</option>
</select>
</div>
<div><label>Mass m (kg)</label><input id="m" type="number" step="any" value="1"></div>
<div><label>Stiffness k (N/m)</label><input id="k" type="number" step="any" value="100"></div>
<div><label>Damping ratio ζ</label><input id="zeta" type="number" step="any" value="0.05"></div>
<div><label>Force amplitude F₀ (N)</label><input id="F0" type="number" step="any" value="1"></div>
<div><label>Excitation ω (rad/s)</label><input id="omegaF" type="number" step="any" value="5"></div>
<div><label>Sim time T (s)</label><input id="T" type="number" step="any" value="20"></div>
<div><label>Δt (s)</label><input id="dt" type="number" step="any" value="0.002"></div>
<div><label>x(0)</label><input id="x0" type="number" step="any" value="0"></div>
<div><label>ẋ(0)</label><input id="v0" type="number" step="any" value="0"></div>
</div>
<div style="display:flex;gap:8px;flex-wrap:wrap;margin-top:10px">
<button id="runBtn">Run time response</button>
<button id="frfBtn">Plot frequency response</button>
<button id="csvBtn">Download time history (CSV)</button>
<span class="tiny">Everything is computed locally with RK4 + closed-form FRF.</span>
</div>
<div class="kpi">
<div>ωₙ = <span id="wn">—</span> rad/s</div>
<div>fₙ = <span id="fn">—</span> Hz</div>
<div>c = <span id="c">—</span> N·s/m</div>
<div>r = ω/ωₙ = <span id="r">—</span></div>
</div>
</section>
<!-- Plots -->
<section class="card">
<h2 style="margin:0 0 8px;font-size:18px">4) Plots</h2>
<div id="timePlot" style="height:380px"></div>
<div id="frfPlot" style="height:380px;margin-top:10px"></div>
</section>
<!-- Quick Check -->
<section class="card">
<h2 style="margin:0 0 8px;font-size:18px">5) Quick Check</h2>
<p class="tiny">Compute the natural frequency and critical damping for the current parameters.</p>
<div class="row">
<div><label>Your ωₙ (rad/s)</label><input id="qc_wn" type="number" step="any"></div>
<div><label>Your c<sub>crit</sub> (N·s/m)</label><input id="qc_ccrit" type="number" step="any"></div>
</div>
<div style="margin-top:10px;display:flex;gap:8px;align-items:center;flex-wrap:wrap">
<button id="checkBtn">Check answers</button>
<span id="qc_msg" class="tiny"></span>
</div>
</section>
<footer class="tiny" style="text-align:center;opacity:.9;margin-top:12px">
Built with HTML + JavaScript + Plotly + MathJax. Share this file and it will run offline.
</footer>
</div>
<script>
// ------- Helpers -------
const g = { ts:[], xs:[], vs:[] }; // for CSV export
const val = id => parseFloat(document.getElementById(id).value);
const setText = (id, t) => document.getElementById(id).textContent = t;
function updateDerived() {
const m = val('m'), k = val('k'), z = val('zeta'), w = val('omegaF');
const wn = Math.sqrt(k/m);
const fn = wn/(2*Math.PI);
const c = 2*z*wn*m;
const r = w/wn;
setText('wn', isFinite(wn)?wn.toFixed(4):'—');
setText('fn', isFinite(fn)?fn.toFixed(4):'—');
setText('c', isFinite(c)?c.toExponential(4):'—');
setText('r', isFinite(r)?r.toFixed(4):'—');
}
['m','k','zeta','omegaF'].forEach(id => document.getElementById(id).addEventListener('input', updateDerived));
// ------- ODE pieces -------
function rhs(t, y, p) {
const [x,v] = y;
const a = (p.F0/p.m)*Math.sin(p.omega*t) - 2*p.zeta*p.wn*v - (p.wn*p.wn)*x;
return [v, a];
}
function rk4_step(f,t,y,h,p){
const k1=f(t,y,p);
const y2=[y[0]+0.5*h*k1[0], y[1]+0.5*h*k1[1]];
const k2=f(t+0.5*h,y2,p);
const y3=[y[0]+0.5*h*k2[0], y[1]+0.5*h*k2[1]];
const k3=f(t+0.5*h,y3,p);
const y4=[y[0]+h*k3[0], y[1]+h*k3[1]];
const k4=f(t+h,y4,p);
return [
y[0]+(h/6)*(k1[0]+2*k2[0]+2*k3[0]+k4[0]),
y[1]+(h/6)*(k1[1]+2*k2[1]+2*k3[1]+k4[1])
];
}
function simulate(){
const p = {
m:val('m'), k:val('k'), zeta:val('zeta'), F0:val('F0'),
omega:val('omegaF'), T:val('T'), dt:val('dt'),
wn: Math.sqrt(val('k')/val('m'))
};
let t=0, y=[val('x0'), val('v0')];
const N=Math.max(1,Math.floor(p.T/p.dt));
const ts=[], xs=[], vs=[];
for(let i=0;i<=N;i++){
ts.push(t); xs.push(y[0]); vs.push(y[1]);
y=rk4_step(rhs,t,y,p.dt,p); t+=p.dt;
}
g.ts=ts; g.xs=xs; g.vs=vs;
Plotly.newPlot('timePlot',[
{x:ts,y:xs,mode:'lines',name:'x(t) [m]'},
{x:ts,y:vs,mode:'lines',name:'v(t) [m/s]',yaxis:'y2'}
],{
paper_bgcolor:'#121a32',plot_bgcolor:'#0f1630',showlegend:true,
margin:{l:60,r:60,t:10,b:40},
xaxis:{title:'t [s]',gridcolor:'#273154',zerolinecolor:'#273154'},
yaxis:{title:'x [m]',gridcolor:'#273154',zerolinecolor:'#273154'},
yaxis2:{title:'v [m/s]',overlaying:'y',side:'right',gridcolor:'#273154',zerolinecolor:'#273154'}
},{displayModeBar:true,responsive:true});
updateDerived();
}
function frf(){
const m=val('m'), k=val('k'), z=val('zeta');
const wn=Math.sqrt(k/m);
const wMin=0.01*wn, wMax=3*wn, N=600;
const r=[], A=[], Phi=[];
for(let i=0;i<N;i++){
const w=wMin+(wMax-wMin)*i/(N-1);
const rr=w/wn;
const den=Math.sqrt((1-rr*rr)**2+(2*z*rr)**2);
r.push(rr); A.push((1/den)); // normalized by (F0/k)
Phi.push(-Math.atan2(2*z*rr,(1-rr*rr))*180/Math.PI);
}
Plotly.newPlot('frfPlot',[
{x:r,y:A,mode:'lines',name:'|X| / (F0/k)'},
{x:r,y:Phi,mode:'lines',name:'Phase [deg]',yaxis:'y2'}
],{
paper_bgcolor:'#121a32',plot_bgcolor:'#0f1630',showlegend:true,
margin:{l:70,r:70,t:10,b:40},
xaxis:{title:'r = ω/ωₙ',gridcolor:'#273154',zerolinecolor:'#273154'},
yaxis:{title:'Amplitude',gridcolor:'#273154',zerolinecolor:'#273154'},
yaxis2:{title:'Phase [deg]',overlaying:'y',side:'right',gridcolor:'#273154',zerolinecolor:'#273154'}
},{displayModeBar:true,responsive:true});
updateDerived();
}
// CSV export
function downloadCSV(){
if(!g.ts.length){ simulate(); }
let csv="t,x,v\n";
for(let i=0;i<g.ts.length;i++){
csv+=`${g.ts[i]},${g.xs[i]},${g.vs[i]}\n`;
}
const blob=new Blob([csv],{type:'text/csv'});
const url=URL.createObjectURL(blob);
const a=document.createElement('a');
a.href=url; a.download='sdof_time_history.csv';
document.body.appendChild(a); a.click();
a.remove(); URL.revokeObjectURL(url);
}
// Presets
document.getElementById('preset').addEventListener('change', e=>{
const m = document.getElementById('m'), k=document.getElementById('k'),
z=document.getElementById('zeta'), w=document.getElementById('omegaF');
if(e.target.value==='light'){ m.value=1; k.value=100; z.value=0.02; w.value=Math.sqrt(100/1); }
else if(e.target.value==='moderate'){ m.value=1; k.value=100; z.value=0.07; w.value=0.8*Math.sqrt(100/1); }
else if(e.target.value==='heavy'){ m.value=1; k.value=100; z.value=0.2; w.value=0.6*Math.sqrt(100/1); }
updateDerived(); simulate(); frf();
});
// Quick check (ωn and ccrit)
document.getElementById('checkBtn').addEventListener('click', ()=>{
const wn_true = Math.sqrt(val('k')/val('m'));
const ccrit_true = 2*val('m')*wn_true;
const ok1 = Math.abs(val('qc_wn')-wn_true) <= 0.01*wn_true;
const ok2 = Math.abs(val('qc_ccrit')-ccrit_true) <= 0.02*ccrit_true;
const msg = `ωₙ ${(ok1?'✅':'❌')} (true ${wn_true.toFixed(4)}), c_crit ${(ok2?'✅':'❌')} (true ${ccrit_true.toExponential(4)})`;
document.getElementById('qc_msg').textContent = msg;
});
// Buttons
document.getElementById('runBtn').addEventListener('click', simulate);
document.getElementById('frfBtn').addEventListener('click', frf);
document.getElementById('csvBtn').addEventListener('click', downloadCSV);
// Initial render
updateDerived(); simulate(); frf();
</script>
</body>
</html> |