A system can be represented by differential equations, state updates or input/output transfer functions. Transfer functions simplify the composition of linear dynamic blocks. We use operating-point deviations from Dynamic models and the controller in PID control.
Turn derivatives into algebra
The unilateral Laplace transform is X(s)=∫0∞x(t)e−stdt in its convergence region. A derivative transforms as L{x˙}=sX(s)−x(0). The complex variable s=σ+jω represents exponential growth or decay together with oscillation.
Our thermal deviation equation is 2000x˙+20x=1000v. At zero initial state,
(2000s+20)X=1000V,P(s)=VX=100s+150.
Here v is a duty-ratio deviation and x a temperature deviation. A nonzero x(0) adds 2000x(0)/(2000s+20) to the transformed response. The transfer ratio alone is not the complete initial-value solution.
P(0)=50 K/duty is the static gain. The pole at −0.01 corresponds to e−0.01t decay. This LTI representation does not automatically include saturation or changing operating conditions.
Where the closed-loop denominator comes from
For unity negative feedback, E=R−Y, U=CE, and Y=PU. Rearranging gives
(1+PC)Y=PCR,RY=1+PCPC.
Clear denominators in 1+PC=0 to obtain the characteristic equation, while checking internal modes in the realization. Sensor dynamics H(s) change the denominator to 1+PCH; filtering is part of the loop.
For C=Kp,
RY=100s+1+50Kp50Kp,p=−1001+50Kp.
At Kp=0.04, the pole is −0.03 and time constant 33.33 s, but a unit reference-temperature step produces only a 2/3 K final increment. Speed and accuracy are separate properties.
PI poles move with gain
With C=Kp+Ki/s, the denominator is
100s2+(1+50Kp)s+50Ki.
Fixing Kp=0.04, Ki=0.0002 gives roots −0.00382,−0.02618; Ki=0.001 gives −0.015±0.01658j. Increasing integral action can turn two real modes into oscillatory modes. Positive coefficients ensure stability for this delay-free second-order case, not for arbitrary plants.
Preparing the visual
Transfer functions and poles · Experiment
Thermal PI characteristic equation 100s²+3s+50Ki=0, Kp=0.04. Poles coincide at Ki=0.00045, then separate along real part −0.015. These are pole locations, not physical trajectories.
{"id":"control-05","title":"Transfer functions and poles · Experiment","summary":"Thermal PI characteristic equation 100s²+3s+50Ki=0, Kp=0.04. Poles coincide at Ki=0.00045, then separate along real part −0.015. These are pole locations, not physical trajectories.","height":820,"html":"<p class=\"intro\" data-i18n=\"scope\"></p><div class=\"controls\"><div class=\"control\"><label for=\"parameter-0\" data-i18n=\"ki\"></label><output id=\"value-0\" for=\"parameter-0\"></output><input type=\"range\" id=\"parameter-0\" data-parameter=\"0\" min=\"0.0001\" max=\"0.003\" step=\"5e-05\" 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=5, DEFAULTS=[0.001], METRICS=[\"real1\", \"imag1\", \"real2\", \"imag2\"];\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)","imag1":"First root imaginary part / s⁻¹","imag2":"Second root imaginary part / s⁻¹","ki":"Integral gain Ki / K⁻¹·s⁻¹","no":"No","poleplane":"Closed-loop poles: real and imaginary","poles":"Poles","real1":"First root real part / s⁻¹","real2":"Second root real part / s⁻¹","realAxis":"Real / s⁻¹; vertical: imaginary / s⁻¹","reset":"Reset experiment","scope":"Thermal PI characteristic equation 100s²+3s+50Ki=0, Kp=0.04. Poles coincide at Ki=0.00045, then separate along real part −0.015. These are pole locations, not physical trajectories.","stepAxis":"Discrete step k","timeAxis":"Time / s","updated":"Full trajectories recalculated with the current parameters.","vertical":"Vertical range","yes":"Yes"}}
Check that the roots sum to −0.03 and multiply to 0.5Ki. They coincide when the discriminant vanishes at Ki=0.00045. A root locus shows pole locations as a parameter varies, not a physical motion path.
Zeros and hidden states matter
Compare G1(s)=1/(s+1) and G2(s)=(1−s)/(s+1). Both have pole −1 and static gain 1. Their unit-step responses are 1−e−t and 1−2e−t. The latter jumps to −1 before tending to 1; it includes a direct path. A right-half-plane zero can produce inverse response and constrain design. Stable poles alone do not determine the transient shape.
Cancellation or an unobserved mode can also hide internal instability. For x˙1=−x1+u, x˙2=x2, y=x1, the transfer is 1/(s+1), yet nonzero x2(0) grows exponentially. Check the realization as well as the reduced denominator.
Reproduce the pole calculation
This optional example uses the python-control 0.10.2 API. feedback defaults to negative feedback; the result is about the stated linear model.
Why is Y=PU insufficient for a nonzero initial temperature deviation?
Reasoning
The transfer relation assumes zero initial conditions. The derivative transform contributes an additional initial-state term, which must be retained.
Do identical poles imply identical step responses?
Reasoning
No. Zeros, gain, direct feedthrough and initial conditions matter. The two example transfers share poles and static gain but initially move in different directions.
Further reading
The python-control linear-systems guide describes transfer and state-space objects. Match their variables, units and initial-condition conventions to the physical model.