Approaching a target in one simulation does not establish stability under every condition. Even two stable responses can differ greatly in speed and overshoot. This chapter builds on the states and exponential responses in Dynamic models.
Free and forced responses
A linear response separates into a contribution from the initial state and one from the input. For constant heating,
θ(t)=θ∞+(θ0−θ∞)e−t/τ.
The second term captures decay of the initial deviation. Changing initial temperature and changing the reference are different experiments. Record initial conditions, inputs, disturbances, observation time and actuator limits.
For proportional thermal control without delay or saturation,
x˙=−Cthk+PmaxKpx.
With positive parameters and Kp=0.04, the closed-loop time constant is 2000/(20+40)=33.33 s. The response approaches equilibrium monotonically. Increasing positive gain does not create another state. Real high-gain oscillations require investigation of omitted filters, actuator dynamics, integral action or delay.
Two states allow oscillation
A mass–spring–damper obeys mq¨+bq˙+ksq=F. Position and velocity are separate states: the mass can cross equilibrium while still moving. Damping dissipates energy. A normalized second-order model is
y¨+2ζωny˙+ωn2y=ωn2r,
with ωn=ks/m in rad/s and dimensionless ζ=b/(2mks). It is underdamped for 0<ζ<1, critically damped at 1, and overdamped above 1.
For a unit step, zero initial state and 0<ζ<1, let ωd=ωn1−ζ2. Then
y(t)=1−e−ζωnt[cos(ωdt)+1−ζ2ζsin(ωdt)].
The sinusoid oscillates within a decaying envelope. Overshoot is Mp=e−πζ/1−ζ2. For ωn=1,ζ=0.5, the first peak occurs at tp=π/ωd≈3.63 s and overshoot is 16.3%.
Preparing the visual
Response and stability · Experiment
Zero initial state, unit step, standard second-order model. Velocity vanishes at output extrema. The overshoot formula applies to 0<ζ<1; a 20 s trace is not a general stability proof.
{"id":"control-03","title":"Response and stability · Experiment","summary":"Zero initial state, unit step, standard second-order model. Velocity vanishes at output extrema. The overshoot formula applies to 0<ζ<1; a 20 s trace is not a general stability proof.","height":820,"html":"<p class=\"intro\" data-i18n=\"scope\"></p><div class=\"controls\"><div class=\"control\"><label for=\"parameter-0\" data-i18n=\"zeta\"></label><output id=\"value-0\" for=\"parameter-0\"></output><input type=\"range\" id=\"parameter-0\" data-parameter=\"0\" min=\"0.1\" max=\"2\" step=\"0.1\" value=\"0.5\"/></div><div class=\"control\"><label for=\"parameter-1\" data-i18n=\"omega\"></label><output id=\"value-1\" for=\"parameter-1\"></output><input type=\"range\" id=\"parameter-1\" data-parameter=\"1\" min=\"0.5\" max=\"2\" step=\"0.25\" value=\"1\"/></div></div><div class=\"tools\"><button id=\"reset\" data-i18n=\"reset\"></button></div><div id=\"plots\"></div><div id=\"metrics\"></div><p id=\"status\" role=\"status\" aria-live=\"polite\"></p>","css":".plotarea{display:grid;grid-template-columns:max-content minmax(0,1fr);gap:8px}.yticks{display:flex;flex-direction:column;justify-content:space-between;padding:3px 0;font-size:13px;font-variant-numeric:tabular-nums;color:var(--muted)}*{box-sizing:border-box}.intro{margin:0 0 16px;color:var(--muted);line-height:1.65}.controls{display:grid;gap:14px}.control{display:grid;grid-template-columns:1fr auto;gap:6px 12px;align-items:center}.control label,.control > span{font-size:16px;line-height:1.5}.control input{grid-column:1/-1;width:100%;min-height:28px}.control output{font-variant-numeric:tabular-nums}.control:has(.presets){grid-template-columns:1fr}.control .presets{grid-column:1/-1;display:flex;flex-wrap:wrap;width:100%;min-width:0}.tools{margin:14px 0}.tools button{font:inherit;padding:8px 14px;min-height:42px}.chart{margin:22px 0}.chart h3{font-size:16px;margin:0 0 6px;line-height:1.5}.chart svg{display:block;width:100%;height:160px;overflow:hidden}.scale,.axes{font-size:14px;color:var(--muted);font-variant-numeric:tabular-nums}.axes{display:flex;justify-content:space-between;align-items:start;gap:8px;margin-top:6px}.axes span:nth-child(2){text-align:center;flex:1;min-width:0}.legend{display:flex;flex-wrap:wrap;gap:8px 18px;margin-top:8px;font-size:15px}.legend span{display:inline-flex;align-items:center;gap:7px}.legend i{display:inline-block;width:22px;flex-shrink:0}#metrics{border-top:1px solid var(--rule);padding-top:12px;display:grid;gap:8px}#metrics>div{display:flex;justify-content:space-between;gap:16px;font-size:15px;line-height:1.5}#metrics strong{font-weight:600;font-variant-numeric:tabular-nums;flex-shrink:0}#status{font-size:14px;color:var(--muted);min-height:3em;margin:14px 0 0}button:focus-visible,input:focus-visible{outline:2px solid var(--accent);outline-offset:3px}@media(max-width:420px){.chart svg{height:145px}#metrics>div{flex-wrap:wrap;gap:3px 12px}}","js":"const MODEL_ID=3, DEFAULTS=[0.5, 1], METRICS=[\"overshoot\", \"peakTime\"];\n/* Deterministic teaching models. Time and integration are independent of rendering. */\nfunction controlModel(id, p) {\n const clip=(x,a,b)=>Math.max(a,Math.min(b,x));\n const rk4=(x,t,h,f)=>{const a=f(x,t),b=f(x.map((v,i)=>v+h*a[i]/2),t+h/2),c=f(x.map((v,i)=>v+h*b[i]/2),t+h/2),d=f(x.map((v,i)=>v+h*c[i]),t+h);return x.map((v,i)=>v+h*(a[i]+2*b[i]+2*c[i]+d[i])/6);};\n const result={plots:[],metrics:{}};\n const series=(name,points)=>({name,points});\n const graph=(title,curves,extra={})=>result.plots.push({title,curves,...extra});\n if(id===1){\n const amb=p[0], kp=p[1], times=Array.from({length:401},(_,i)=>i*2), temps=[], actions=[];\n for(let mode=0;mode<3;mode++){\n let eq=mode===0?amb+20:mode===1?(amb+20+50*kp*40)/(1+50*kp):40;\n let tau=mode===1?100/(1+50*kp):100;\n const temp=times.map(t=>[t,t<=100?40:eq+(40-eq)*Math.exp(-(t-100)/tau)]);\n temps.push(series(['open','feedback','feedforward'][mode],temp));\n actions.push(series(['open','feedback','feedforward'][mode],temp.map(([t,y])=>[t,t<100?.4:mode===0?.4:mode===1?.4+kp*(40-y):(40-amb)/50])));\n }\n graph('temperature',temps,{event:100});graph('action',actions,{event:100});\n result.metrics={openFinal:amb+20,feedbackFinal:(amb+20+50*kp*40)/(1+50*kp),feedforwardFinal:40};\n } else if(id===2){\n const cap=p[0], loss=p[1], tau=cap/loss, eq=20+400/loss;\n const curve=Array.from({length:401},(_,i)=>{const t=i*2;return[t,eq+(20-eq)*Math.exp(-t/tau)];});\n graph('temperature',[series('temperature',curve),series('equilibrium',[[0,eq],[800,eq]])]);\n graph('netpower',[series('netpower',curve.map(([t,y])=>[t,400-loss*(y-20)]))]);\n result.metrics={tau,equilibriumFinal:eq,at100:eq+(20-eq)*Math.exp(-100/tau)};\n } else if(id===3){\n const z=p[0],w=p[1];let x=[0,0],ys=[],vs=[];const dt=.01;\n for(let j=0;j<=2000;j++){const t=j*dt;if(j%5===0){ys.push([t,x[0]]);vs.push([t,x[1]]);}x=rk4(x,t,dt,a=>[a[1],w*w*(1-a[0])-2*z*w*a[1]]);}\n graph('response',[series('response',ys),series('target',[[0,1],[20,1]])]);graph('velocity',[series('velocity',vs)]);\n result.metrics={zeta:z,omega:w,overshoot:z>0&&z<1?100*Math.exp(-Math.PI*z/Math.sqrt(1-z*z)):0,peakTime:z>0&&z<1?Math.PI/(w*Math.sqrt(1-z*z)):null,finalWindow:ys.at(-1)[1]};\n } else if(id===4 || id===8){\n const wind=id===8,mode=wind?p[0]:p[0],kp=wind?.04:p[1],ki=wind?.001:p[2],kd=!wind&&mode===2?1:0,tt=wind?p[1]:20,dt=p[3]||.2;\n let x=[40,0,40];const yy=[],req=[],act=[],pp=[],ii=[],dd=[];\n const target=t=>wind?(t<200?80:40):40;\n const ambient=t=>wind?20:(t<100?20:10);\n const read=(a,t)=>{const e=target(t)-a[0],P=kp*e,I=wind||mode>0?a[1]:0,D=-kd*(a[0]-a[2])/5,uc=.4+P+I+D,u=wind?clip(uc,0,1):uc;return{e,P,I,D,uc,u};};\n for(let j=0;j<=Math.round(800/dt);j++){\n const t=j*dt,r=read(x,t);\n if(j%Math.max(1,Math.round(2/dt))===0){yy.push([t,x[0]]);req.push([t,r.uc]);act.push([t,r.u]);pp.push([t,r.P]);ii.push([t,r.I]);dd.push([t,r.D]);}\n // Event occurs at a step boundary. Freeze exogenous inputs across this step.\n const ref=target(t+1e-8),amb=ambient(t+1e-8);\n x=rk4(x,t,dt,a=>{const e=ref-a[0],P=kp*e,I=wind||mode>0?a[1]:0,D=-kd*(a[0]-a[2])/5,uc=.4+P+I+D,u=wind?clip(uc,0,1):uc;return[(1000*u-20*(a[0]-amb))/2000,(wind||mode>0)?ki*e+(wind&&mode===1?(u-uc)/tt:0):0,(a[0]-a[2])/5];});\n }\n graph('temperature',[series('temperature',yy),series('target',wind?[[0,80],[200,80],[200,40],[800,40]]:[[0,40],[800,40]])],{event:wind?200:100});\n graph('action',[series('request',req),series('applied',act)],{event:wind?200:100});\n graph('components',[series('proportional',pp),series('integral',ii),series('derivative',dd)],{event:wind?200:100});\n result.metrics={final:yy.at(-1)[1],integral:ii.at(-1)[1],maxRequest:Math.max(...req.map(x=>x[1])),minRequest:Math.min(...req.map(x=>x[1])),at300:yy.find(x=>Math.abs(x[0]-300)<.01)?.[1],physical:req.every(x=>x[1]>=0&&x[1]<=1)};\n } else if(id===5){\n const ki=p[0],b=.03,c=.5*ki,disc=b*b-4*c;\n const roots=disc>=0?[[-b/2+Math.sqrt(disc)/2,0],[-b/2-Math.sqrt(disc)/2,0]]:[[-b/2,Math.sqrt(-disc)/2],[-b/2,-Math.sqrt(-disc)/2]];\n graph('poleplane',[series('poles',roots)],{scatter:true,xrange:[-.035,.005],yrange:[-.04,.04]});\n result.metrics={real1:roots[0][0],imag1:roots[0][1],real2:roots[1][0],imag2:roots[1][1],discriminant:disc};\n } else if(id===6){\n const delay=p[0],omega=p[1],wc=Math.sqrt(3),mag=[],phase=[];\n for(let i=0;i<=240;i++){const l=-2+i/80,w=10**l;mag.push([l,20*Math.log10(2/Math.hypot(1,w))]);phase.push([l,(-Math.atan(w)-w*delay)*180/Math.PI]);}\n graph('magnitude',[series('loop',mag),series('zeroDb',[[-2,0],[1,0]])],{logx:true,event:Math.log10(wc)});\n graph('phase',[series('loop',phase),series('minus180',[[-2,-180],[1,-180]])],{logx:true,event:Math.log10(wc)});\n const amp=2/Math.hypot(1,omega),ph=-Math.atan(omega)-omega*delay,points=Array.from({length:241},(_,i)=>i*4*Math.PI/240/omega);\n graph('sine',[series('input',points.map(t=>[t,Math.sin(omega*t)])),series('output',points.map(t=>[t,amp*Math.sin(omega*t+ph)]))]);\n result.metrics={crossover:wc,phaseMargin:120-wc*delay*180/Math.PI,amplitude:amp,phase:ph*180/Math.PI,criticalDelay:2*Math.PI/(3*wc)};\n } else if(id===7){\n const ts=p[0],delay=p[1],a=Math.exp(-ts),b=1-a,K=2;let x=1,prev=1;const continuous=[],samples=[],us=[];\n for(let k=0;k*ts<12-1e-8;k++){\n const t=k*ts,u=-K*(delay?prev:x);samples.push([t,x]);\n for(let j=0;j<=12;j++){const h=Math.min(j*ts/12,12-t);if(h<0)break;continuous.push([t+h,Math.exp(-h)*x+(1-Math.exp(-h))*u]);if(t+h>=12)break;}\n us.push([t,u],[Math.min(t+ts,12),u]);const next=a*x+b*u;prev=x;x=next;\n }\n graph('state',[series('state',continuous),series('samples',samples)],{dots:1});graph('action',[series('held',us)]);\n const disc=a*a-4*b*K,roots=delay?(disc>=0?[(a+Math.sqrt(disc))/2,(a-Math.sqrt(disc))/2]:[Math.sqrt(b*K),Math.sqrt(b*K)]):[a-b*K];\n result.metrics={coefficient:a-b*K,radius:Math.max(...roots.map(Math.abs)),period:ts,delay,initial:1};\n } else if(id===9){\n const delay=p[0],window=p[1],clear=p[2];let q=0,n=2,command=2,pending=[],history=[],changes=0,cost=0,maxQ=0;const qs=[],ns=[],cs=[],ds=[],ls=[];\n for(let t=0;t<=240;t++){\n while(pending.length&&pending[0].at<=t)n=pending.shift().value;\n const rate=t>=20&&t<100?70:20;let desired=Math.min(12,Math.max(1,Math.ceil((rate+q/clear)/10)));\n if(t%10===0){history.push({t,value:desired});history=history.filter(x=>x.t>=t-window);let next=desired<command?Math.max(...history.map(x=>x.value)):desired;\n if(next!==command){command=next;changes++;if(delay===0)n=command;else pending.push({at:t+delay,value:command});}}\n qs.push([t,q]);ns.push([t,n]);cs.push([t,command]);ds.push([t,desired]);ls.push([t,rate]);maxQ=Math.max(q,maxQ);\n if(t<240){cost+=n;q=Math.max(0,q+rate-10*n);}\n }\n graph('backlog',[series('backlog',qs)]);graph('replicas',[series('available',ns),series('command',cs),series('suggested',ds)]);graph('rate',[series('arrival',ls),series('capacity',ns.map(([t,n])=>[t,n*10]))]);\n result.metrics={maxBacklog:maxQ,instanceSeconds:cost,changes,finalBacklog:q};\n } else if(id===10){\n const alpha=p[0],noise=p[1],dt=p[2]||.005;let x=[1,0,0,0];const pos=[],est=[],err=[],vel=[],ev=[],u=[];\n for(let j=0;j<=Math.round(10/dt);j++){\n const t=j*dt;if(j%Math.max(1,Math.round(.025/dt))===0){pos.push([t,x[0]]);est.push([t,x[2]]);err.push([t,x[1]-x[3]]);vel.push([t,x[1]]);ev.push([t,x[3]]);u.push([t,-2*x[2]-3*x[3]]);}\n x=rk4(x,t,dt,(a,t)=>{const input=-2*a[2]-3*a[3],innovation=a[0]+noise*Math.sin(30*t)-a[2];return[a[1],input,a[3]+2*alpha*innovation,input+alpha*alpha*innovation];});\n }\n graph('position',[series('actual',pos),series('estimate',est)]);graph('velocity',[series('actual',vel),series('estimate',ev)]);graph('action',[series('action',u)]);\n result.metrics={finalPosition:pos.at(-1)[1],finalVelocityError:err.at(-1)[1],peakAction:Math.max(...u.map(x=>Math.abs(x[1]))),observerPole:-alpha};\n } else if(id===11){\n const horizon=p[0],rho=p[1],disturb=p[2],inputs=[0,-.5,.5,-1,1];\n const optimize=x=>{let best={cost:Infinity,path:[]};\n function walk(state,depth,cost,path){if(cost>best.cost+1e-10)return;if(depth===horizon){if(cost<best.cost-1e-10)best={cost,path};return;}for(const u of inputs){const next=state+u;walk(next,depth+1,cost+next*next+rho*u*u,[...path,u]);}}\n walk(x,0,0,[]);return best;};\n let x=2.5;const first=optimize(x),pred=[[0,x]],real=[[0,x]],actions=[];let px=x;\n first.path.forEach((u,i)=>{px+=u;pred.push([i+1,px]);});\n for(let k=0;k<12;k++){const plan=optimize(x),u=plan.path[0];actions.push([k,u],[k+1,u]);x+=u;if(k===2)x+=disturb;real.push([k+1,x]);}\n graph('prediction',[series('realized',real),series('firstPrediction',pred)],{event:3});graph('action',[series('action',actions)]);\n result.metrics={firstAction:first.path[0],firstCost:first.cost,final:x,horizon,candidates:5**horizon};\n }\n return result;\n}\nif(typeof module!=='undefined')module.exports={controlModel};\n\nconst $=s=>document.querySelector(s), colors=['var(--accent)','#b77818','#269282'],dash=['','7 4','2 4'];\nconst number=x=>Number.isFinite(x)?viz.number(x,Number.isInteger(x)?0:Math.abs(x)<.01&&x!==0?5:3):'—';\nfunction plot(parent,spec){\n const all=spec.curves.flatMap(s=>s.points),xx=all.map(p=>p[0]),yy=all.map(p=>p[1]);\n const zero=['action','components','rate','netpower','backlog','replicas'].includes(spec.title);\n let lo=zero?Math.min(0,...yy):Math.min(...yy),hi=zero?Math.max(0,...yy):Math.max(...yy),xmin=Math.min(...xx),xmax=Math.max(...xx);\n if(hi===lo)hi=lo+1;if(xmin===xmax)xmax=xmin+1;\n const pad=(hi-lo)*.08;lo-=pad;hi+=pad;\n if(spec.xrange)[xmin,xmax]=spec.xrange;if(spec.yrange)[lo,hi]=spec.yrange;\n const sx=x=>8+(x-xmin)/(xmax-xmin)*584,sy=y=>8+(hi-y)/(hi-lo)*144;\n const section=document.createElement('section');section.className='chart';\n const title=document.createElement('h3');title.textContent=viz.t(spec.title);section.append(title);\n const scale=document.createElement('div');scale.className='scale';scale.textContent=viz.t('vertical')+' '+number(lo)+' … '+number(hi);section.append(scale);\n const ns='http://www.w3.org/2000/svg',svg=document.createElementNS(ns,'svg');svg.setAttribute('viewBox','0 0 600 160');svg.setAttribute('preserveAspectRatio','none');svg.setAttribute('role','img');svg.setAttribute('aria-label',viz.t(spec.title));\n function node(name,attrs){const n=document.createElementNS(ns,name);for(const [k,v]of Object.entries(attrs))n.setAttribute(k,v);svg.append(n);return n;}\n for(let j=0;j<=4;j++)node('line',{x1:8,x2:592,y1:8+j*36,y2:8+j*36,stroke:'var(--rule)','stroke-width':1});\n if(lo<=0&&hi>=0)node('line',{x1:8,x2:592,y1:sy(0),y2:sy(0),stroke:'var(--muted)','stroke-width':1});\n if(spec.scatter)node('line',{x1:sx(0),x2:sx(0),y1:8,y2:152,stroke:'var(--muted)','stroke-width':1});\n if(spec.event!==undefined&&spec.event>=xmin&&spec.event<=xmax)node('line',{x1:sx(spec.event),x2:sx(spec.event),y1:8,y2:152,stroke:'var(--muted)','stroke-dasharray':'3 3'});\n spec.curves.forEach((s,i)=>{if(spec.scatter||spec.dots===i){for(const [x,y] of s.points)node('circle',{cx:sx(x),cy:sy(y),r:spec.scatter?5:2.8,fill:colors[i%3]});}else{let pts=s.points;if(spec.title==='replicas')pts=pts.flatMap((p,j)=>j?[[p[0],pts[j-1][1]],p]:[p]);node('path',{d:pts.map(([x,y],j)=>(j?'L':'M')+sx(x).toFixed(2)+' '+sy(y).toFixed(2)).join(' '),fill:'none',stroke:colors[i%3],'stroke-width':2,'stroke-dasharray':dash[i%3],'vector-effect':'non-scaling-stroke'});}});\n const area=document.createElement('div');area.className='plotarea';const ticks=document.createElement('div');ticks.className='yticks';for(const v of [hi,(hi+lo)/2,lo]){const label=document.createElement('span');label.textContent=number(v);ticks.append(label);}area.append(ticks,svg);section.append(area);\n const axes=document.createElement('div');axes.className='axes';const fmt=x=>spec.logx?number(10**x):number(x);\n for(const text of [fmt(xmin),viz.t(spec.scatter?'realAxis':spec.logx?'frequencyAxis':MODEL_ID===11?'stepAxis':'timeAxis'),fmt(xmax)]){const a=document.createElement('span');a.textContent=text;axes.append(a);}section.append(axes);\n const legend=document.createElement('div');legend.className='legend';spec.curves.forEach((s,i)=>{const el=document.createElement('span'),sw=document.createElement('i');sw.style.borderTop=`3px ${i===0?'solid':i===1?'dashed':'dotted'} ${colors[i%3]}`;el.append(sw,document.createTextNode(viz.t(s.name)));legend.append(el);});section.append(legend);parent.append(section);\n}\nfunction parameterValue(el){\n const pressed=el.matches('.presets')&&el.querySelector('[aria-pressed=\"true\"]');\n return +(pressed?pressed.value:el.value);\n}\nfunction setParameter(el,value){\n if(el.matches('.presets'))[...el.querySelectorAll('button')].forEach(b=>b.setAttribute('aria-pressed',String(+b.value===value)));\n else el.value=value;\n}\nfunction update(){\n const values=[...document.querySelectorAll('[data-parameter]')].map(parameterValue);\n document.querySelectorAll('input[data-parameter]').forEach(e=>$('#value-'+e.dataset.parameter).textContent=number(+e.value));\n const r=controlModel(MODEL_ID,values);document.body.dataset.result=JSON.stringify(r.metrics);document.body.dataset.parameters=JSON.stringify(values);\n $('#plots').replaceChildren();r.plots.forEach(s=>plot($('#plots'),s));$('#metrics').replaceChildren();\n for(const [k,v]of Object.entries(r.metrics)){if(!METRICS.includes(k))continue;const line=document.createElement('div'),label=document.createElement('span'),value=document.createElement('strong');label.textContent=viz.t(k);value.textContent=typeof v==='boolean'?viz.t(v?'yes':'no'):number(v);line.append(label,value);$('#metrics').append(line);}\n $('#status').textContent=viz.t('updated');\n}\ndocument.querySelectorAll('input[data-parameter]').forEach(e=>e.addEventListener('input',update));\ndocument.querySelectorAll('.presets[data-parameter]').forEach(group=>group.addEventListener('click',e=>{\n const button=e.target.closest('button');if(!button||!group.contains(button))return;\n [...group.querySelectorAll('button')].forEach(b=>b.setAttribute('aria-pressed',String(b===button)));\n update();\n}));\n$('#reset').addEventListener('click',()=>{document.querySelectorAll('[data-parameter]').forEach((e,i)=>setParameter(e,DEFAULTS[i]));update();});update();\n","audio":false,"strings":{"frequencyAxis":"Angular frequency / rad·s⁻¹ (log)","no":"No","omega":"Natural frequency / rad·s⁻¹","overshoot":"Analytical overshoot / %","peakTime":"Analytical first peak / s","realAxis":"Real / s⁻¹; vertical: imaginary / s⁻¹","reset":"Reset experiment","response":"Normalized output y","scope":"Zero initial state, unit step, standard second-order model. Velocity vanishes at output extrema. The overshoot formula applies to 0<ζ<1; a 20 s trace is not a general stability proof.","stepAxis":"Discrete step k","target":"Reference","timeAxis":"Time / s","updated":"Full trajectories recalculated with the current parameters.","velocity":"Velocity / dy·dt⁻¹","vertical":"Vertical range","yes":"Yes","zeta":"Damping ratio ζ"}}
Compare output and velocity while changing damping. Velocity is zero at an output peak, but usually nonzero when output crosses the target. Critical damping avoids oscillation in this standard model; it is not a universal fastest-response prescription.
Which stability claim?
Concept
Question
Limitation
Lyapunov stability
Do sufficiently close initial states remain close?
Convergence is not required
Asymptotic stability
Do nearby states also converge to equilibrium?
Local does not mean global
BIBO stability
Does every bounded input give bounded output from zero initial state?
Hidden internal modes may remain
With ζ>0, the standard model's modes decay. At ζ=0, free oscillations remain bounded but do not decay; a bounded resonant sinusoid can produce an unbounded response. Bounded free motion therefore does not prove BIBO stability. Negative damping amplifies deviations.
An energy function also gives evidence: V=21mv2+21ksq2 has V˙=−bv2≤0 in the unforced mechanical system. Energy cannot increase. Asymptotic convergence requires the additional observation that the only trajectory able to remain in V˙=0 is the origin; this is the entry to invariance methods.
Response metrics need definitions
Specify rise-time thresholds, the settling tolerance, and the overshoot normalization. The estimate ts≈4/(ζωn) is an engineering approximation for a 2% band in underdamped second-order systems. A finite plot only identifies the last observed entry into a band, not a guarantee about the future. A zero target needs an absolute tolerance. Report stability, error, speed and control effort separately.
Check your understanding
Does constant-amplitude free oscillation establish asymptotic or BIBO stability?
Reasoning
It does not converge, and a single bounded trajectory says nothing about all bounded inputs. The undamped resonator is a counterexample to the BIBO inference.
At fixed ζ=0.5, what happens when ωn doubles from 1 to 2?
Reasoning
Overshoot stays 16.3% and first peak time halves to about 1.81 s, assuming the standard model without extra zeros, delays or saturation.
Further reading
The Caltech control course connects modeling, state space, phase portraits and stability. Next we introduce the controller's own dynamic states.