Pole placement specifies dynamics, but not the relative cost of error and action. Optimal control makes that tradeoff explicit; MPC includes a finite future and constraints in repeated decisions. This chapter builds on state feedback , digital control and quadratic functions.
LQR: define the objective
For continuous x ˙ = A x + B u , consider
J = ∫ 0 ∞ ( x T Q x + u T R u ) d t , Q ⪰ 0 , R ≻ 0.
Q penalizes state deviation and R action. Coordinates and units matter: changing meters to centimeters without changing weights changes the physical preference. Allowable errors and actions can provide normalization scales.
Under standard stabilizability and detectability conditions, the stabilizing Riccati solution X satisfies
A T X + X A − XB R − 1 B T X + Q = 0.
The feedback is u = − K x , K = R − 1 B T X . Here X is a cost matrix, not the state. Basic LQR has no hard actuator constraints; clipping its result changes its optimality and guarantees.
A hand-computable optimum
For x ˙ = u , Q = q > 0 , R = ρ > 0 , the equation is − X 2 / ρ + q = 0 . The stabilizing solution gives
X = qρ , u = − q / ρ x .
At q = ρ = 1 , x = e − t x 0 . Raising ρ to 4 reduces gain to 0.5 and slows convergence. For x 0 = 2 , the respective optimal costs are 4 and 8. Their objectives differ, so those numbers cannot directly rank the controllers under one common preference.
MPC: optimize a sequence, execute one action
For discrete x k + 1 = A x k + B u k , solve at each instant
u 0 : N − 1 min i = 0 ∑ N − 1 ( x i T Q x i + u i T R u i ) + V f ( x N )
subject to x 0 = x ^ k , dynamics, x i ∈ X , u i ∈ U , and any terminal constraint. Execute only the first action, then measure or estimate again and reoptimize.
This incorporates new information after disturbances or prediction errors and can use known future references. Predicted and realized trajectories are different objects. The do-mpc introduction describes finite horizons, estimation and constraints.
Inspect a finite-action example
Our dimensionless model is x k + 1 = x k + u k , x 0 = 2.5 , target 0, with actions { − 1 , − 0.5 , 0 , 0.5 , 1 } . Define the specific cost
J N = i = 0 ∑ N − 1 ( x i + 1 2 + ρ u i 2 ) .
There is no extra terminal term. Unlike the preceding notation, this penalizes each post-action state. Searching all 5 N sequences finds the global optimum within this finite set, not the general continuous-input MPC optimum.
Preparing the visual Try again
Advanced: optimal control and MPC · Experiment x(k+1)=x(k)+u(k), x0=2.5; actions 0, ±0.5, ±1; cost Σ[x(i+1)²+ρu(i)²]. Search all 5^N candidates, execute the first action, and reoptimize. Dashed initial prediction has no advance knowledge of the disturbance.
{"id":"control-11","title":"Advanced: optimal control and MPC · Experiment","summary":"x(k+1)=x(k)+u(k), x0=2.5; actions 0, ±0.5, ±1; cost Σ[x(i+1)²+ρu(i)²]. Search all 5^N candidates, execute the first action, and reoptimize. Dashed initial prediction has no advance knowledge of the disturbance.","height":820,"html":"<p class=\"intro\" data-i18n=\"scope\"></p><div class=\"controls\"><div class=\"control\"><label for=\"parameter-0\" data-i18n=\"horizon\"></label><output id=\"value-0\" for=\"parameter-0\"></output><input type=\"range\" id=\"parameter-0\" data-parameter=\"0\" min=\"1\" max=\"5\" step=\"1\" value=\"3\"/></div><div class=\"control\"><label for=\"parameter-1\" data-i18n=\"rho\"></label><output id=\"value-1\" for=\"parameter-1\"></output><input type=\"range\" id=\"parameter-1\" data-parameter=\"1\" min=\"0.25\" max=\"4\" step=\"0.25\" value=\"1\"/></div><div class=\"control\"><span id=\"parameter-2-label\" data-i18n=\"disturb\"></span><div class=\"presets\" role=\"group\" aria-labelledby=\"parameter-2-label\" data-parameter=\"2\"><button type=\"button\" value=\"0\" aria-pressed=\"false\" data-i18n=\"off\"></button><button type=\"button\" value=\"1\" aria-pressed=\"true\" data-i18n=\"yes\"></button></div></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=11, DEFAULTS=[3, 1, 1], METRICS=[\"firstAction\", \"firstCost\", \"candidates\"];\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","candidates":"Candidate sequences","disturb":"+1 disturbance after step 3","firstAction":"First action","firstCost":"Initial minimum cost","firstPrediction":"Initial optimal prediction","frequencyAxis":"Angular frequency / rad·s⁻¹ (log)","horizon":"Prediction horizon N","no":"No","off":"Off","prediction":"State and initial prediction (dimensionless)","realAxis":"Real / s⁻¹; vertical: imaginary / s⁻¹","realized":"Realized receding-horizon trajectory","reset":"Reset experiment","rho":"Action weight ρ","scope":"x(k+1)=x(k)+u(k), x0=2.5; actions 0, ±0.5, ±1; cost Σ[x(i+1)²+ρu(i)²]. Search all 5^N candidates, execute the first action, and reoptimize. Dashed initial prediction has no advance knowledge of the disturbance.","stepAxis":"Discrete step k","timeAxis":"Time / s","updated":"Full trajectories recalculated with the current parameters.","vertical":"Vertical range","yes":"Yes"}}
Change horizon and action weight. Compare the initial prediction with the realized receding-horizon trajectory and actions. An optional +1 disturbance is applied after the third action; it is not predicted. At equal cost, prefer smaller absolute first action, then enumeration order.
For N = 1 , ρ = 1 , x = 2.5 , action −1 costs 1. 5 2 + 1 = 3.25 , while −0.5 costs 2 2 + 0.25 = 4.25 . Choose −1. For continuous input constrained to [ − 1 , 1 ] , the stationary point is u = − x / ( 1 + ρ ) , projected onto the interval. A finite set additionally requires comparing neighboring candidates.
Feasibility and stability need their own arguments
A feasible optimization now does not ensure feasibility after a disturbance: recursive feasibility is a separate property. Finite-horizon optimality also does not automatically imply infinite-time convergence. Suitable terminal costs, terminal sets and local controllers are common ingredients in guarantees, with model-specific conditions.
Longer horizons see farther but cost computation. Missing the sample deadline introduces delay. Define fallback actions for timeout or infeasibility, constraint priorities, estimation uncertainty and model updates. Soft constraints need explicit allowed violations and penalties; do not silently soften hard limits.
Check your understanding
Is a clipped LQR action still the optimum of the original unconstrained problem?
Reasoning
No. Clipping changes the loop. Analyze saturation or formulate the constrained problem, then reassess stability and feasibility.
Why not compute one sequence and execute it forever?
Reasoning
Disturbances, model errors and updated estimates change the state. Repeated measurement and optimization use that new information; computation delay remains part of the implementation.
Reproduce and continue
python-control lqr returns gain, Riccati solution and closed-loop eigenvalues. This checks the scalar example using SciPy:
import control as ct
K , X , poles = ct . lqr ( [ [ 0. 0 ] ] , [ [ 1. 0 ] ] , [ [ 1. 0 ] ] , [ [ 4. 0 ] ] , method = " scipy" )
print ( K , X , poles )
Continue with system identification for better models, nonlinear/robust control for broader guarantees, or stochastic estimation for noise. Return to the reading route to choose a path.