Proportional action reacts to current error; integral action retains a correction; derivative action responds to change. Building on Response and stability, we first analyze continuous, unsaturated control and then identify implementation requirements.
P: immediate action from error
For u=ub+Kpe, ub=0.4 and e=r−θ, the thermal equilibrium error is
e∞=k+PmaxKpk(r−θa)−Pmaxub−d.
With target 40 °C, ambient 10 °C, baseline parameters, d=0 and Kp=0.04, this is 200/60=3.33 K. More gain reduces this offset but also increases noise in the action and exposure to limits.
P control does not universally require a steady-state offset. A plant with integral dynamics may track a step without offset in a stable proportional loop. State the plant, input, bias action and stability conditions.
I: store the correction in a state
Let z denote the integral contribution itself:
uc=ub+Kpe+z,z˙=Kie.
Ki has units K−1s−1; z is dimensionless duty ratio. Positive error increases z. Zero error stops its change, rather than erasing it. At ambient 10 °C, maintaining 40 °C needs u=0.6, so equilibrium stores z=0.2. Zero error requires stability, feasibility, unbiased sensing and no persistent saturation.
PI adds a state. Its thermal characteristic polynomial is
Cths2+(k+PmaxKp)s+PmaxKi=0.
For Kp=0.04,Ki=0.001, it becomes s2+0.03s+0.0005, with roots −0.015±0.01658j. The oscillatory modes come from integral action, not an unexplained extra inertia in the original plant.
Preparing the visual
PID control · Experiment
Reference 40 °C; ambient 20→10 °C at 100 s; integral starts at zero. PID differentiates measurement with Kd=1 s/K and a 5 s filter. Requests are not clipped. Component curves exclude the 0.4 baseline.
{"id":"control-04","title":"PID control · Experiment","summary":"Reference 40 °C; ambient 20→10 °C at 100 s; integral starts at zero. PID differentiates measurement with Kd=1 s/K and a 5 s filter. Requests are not clipped. Component curves exclude the 0.4 baseline.","height":1050,"html":"<p class=\"intro\" data-i18n=\"scope\"></p><div class=\"controls\"><div class=\"control\"><span id=\"parameter-0-label\" data-i18n=\"mode\"></span><div class=\"presets\" role=\"group\" aria-labelledby=\"parameter-0-label\" data-parameter=\"0\"><button type=\"button\" value=\"0\" aria-pressed=\"false\" data-i18n=\"p\"></button><button type=\"button\" value=\"1\" aria-pressed=\"true\" data-i18n=\"pi\"></button><button type=\"button\" value=\"2\" aria-pressed=\"false\" data-i18n=\"pid\"></button></div></div><div class=\"control\"><label for=\"parameter-1\" data-i18n=\"kp\"></label><output id=\"value-1\" for=\"parameter-1\"></output><input type=\"range\" id=\"parameter-1\" data-parameter=\"1\" min=\"0.01\" max=\"0.08\" step=\"0.01\" value=\"0.04\"/></div><div class=\"control\"><label for=\"parameter-2\" data-i18n=\"ki\"></label><output id=\"value-2\" for=\"parameter-2\"></output><input type=\"range\" id=\"parameter-2\" data-parameter=\"2\" min=\"0.0002\" max=\"0.002\" step=\"0.0002\" value=\"0.001\"/></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=4, DEFAULTS=[1, 0.04, 0.001], METRICS=[\"final\", \"integral\", \"maxRequest\", \"physical\"];\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":{"action":"Control action / u","applied":"Applied action","components":"P / I / D contributions (duty ratio)","derivative":"D contribution","final":"Temperature at window end / °C","frequencyAxis":"Angular frequency / rad·s⁻¹ (log)","integral":"I contribution","ki":"Integral gain Ki / K⁻¹·s⁻¹","kp":"Proportional gain Kp / K⁻¹","maxRequest":"Maximum request / u","mode":"Control strategy","no":"No","p":"P","physical":"All requests within 0–1","pi":"PI","pid":"PID","proportional":"P contribution","realAxis":"Real / s⁻¹; vertical: imaginary / s⁻¹","request":"Unclipped request","reset":"Reset experiment","scope":"Reference 40 °C; ambient 20→10 °C at 100 s; integral starts at zero. PID differentiates measurement with Kd=1 s/K and a 5 s filter. Requests are not clipped. Component curves exclude the 0.4 baseline.","stepAxis":"Discrete step k","target":"Reference","temperature":"Temperature / °C","timeAxis":"Time / s","updated":"Full trajectories recalculated with the current parameters.","vertical":"Vertical range","yes":"Yes"}}
Ambient falls from 20 to 10 °C at 100 s while the reference stays 40 °C. Compare P, PI and filtered PID, including separate P/I/D contributions. Requests are not clipped; the experiment reports whether they remain physically within 0–1. Constraints adds actuator saturation.
D: filter the measured trend
Ideal parallel PID is uc=ub+Kpe+Ki∫edt+Kde˙. Differentiating a step reference produces derivative kick. A common alternative differentiates the measurement through a filter:
τfy˙f=ym−yf,uD=−Kdτfym−yf.
Its measurement-to-action transfer is −Kds/(τfs+1), with finite high-frequency gain. Derivative on measurement and derivative on error are different structures: only the latter directly differentiates reference changes.
For Kd=1 s/K, τf=5 s, ym=38 °C and yf=37.5 °C, uD=−0.1. Rising temperature reduces heating. Initialize the filter consistently, often from the first reading, to avoid an artificial startup spike. D uses observed trends; it does not predict unknown future disturbances.
Work through one update
For e=2 K, z=0.15, uD=−0.1 and Kp=0.04, the request is 0.4+0.08+0.15−0.1=0.53. A 1 s forward integral update gives zj+1=0.15+0.001×2×1=0.152. This convention calculates the current action before advancing the state; using updated states is another implementation that must be specified consistently.
Parameter forms also matter. Kp[1+1/(Tis)+Tds] corresponds to Ki=Kp/Ti, Kd=KpTd, with filtering specified separately. Discrete coefficients depend on the sample period.
Tune against several requirements
First verify feedback direction, plant time scales and feasible actions. Start with P or PI; add D for an identified dynamic need. Test reference changes, load changes, measurement noise, initial-state errors and saturation. Record requested and applied action and integral state, not output alone. Empirical tuning is a starting point with assumptions, not a universal guarantee.
Check your understanding
Why can PI deliver more than the baseline after error becomes zero?
Reasoning
Zero error stops integration but retains z=0.2, supplying the extra 200 W needed for the new heat loss.
Should noisy measurements automatically lead to more D?
Reasoning
No. Differentiation emphasizes rapid measurement changes. Distinguish actual dynamics from noise and compare filtering, bandwidth and action cost before adding derivative gain.