├── HOWTO.md ├── README.md ├── ch01.py ├── ch02.py ├── ch03.py ├── ch13-cache.py ├── ch14-adserving.py ├── ch15-ch16-serverpool-and-queue.py ├── ch17-fancontrol.py ├── ch18-gameengine.py ├── feedback.py └── janert-feedbackcontrol.zip /HOWTO.md: -------------------------------------------------------------------------------- 1 | 2 | The code for the short demos in chapters 1, 2, and 3 does not 3 | have dependencies (except for the Python standard library). 4 | 5 | The code for the Case Studies in chapters 13-18 depends on the 6 | simulation "framework" introduced in chapter 12. This common 7 | code is contained in the file "feedback.py". Place this file 8 | in the same directory as the case study you want to run, or 9 | add its location to the PYTHONPATH environment variable. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Feedback Control for Computer Systems 2 | ===================================== 3 | 4 | This is the example code than accompanies Feedback Control for Computer Systems by Philipp K. Janert (9781449361693). 5 | 6 | Visit the catalog page [here](http://shop.oreilly.com/product/0636920028970.do). 7 | 8 | See an error? Report it [here](http://oreilly.com/catalog/errata.csp?isbn=0636920028970), or simply fork and send us a pull request. 9 | -------------------------------------------------------------------------------- /ch01.py: -------------------------------------------------------------------------------- 1 | 2 | import random 3 | 4 | class Buffer: 5 | def __init__( self, max_wip, max_flow ): 6 | self.queued = 0 7 | self.wip = 0 # work-in-progress ("ready pool") 8 | 9 | self.max_wip = max_wip 10 | self.max_flow = max_flow # avg outflow is max_flow/2 11 | 12 | def work( self, u ): 13 | # Add to ready pool 14 | u = max( 0, int(round(u)) ) 15 | u = min( u, self.max_wip ) 16 | self.wip += u 17 | 18 | # Transfer from ready pool to queue 19 | r = int( round( random.uniform( 0, self.wip ) ) ) 20 | self.wip -= r 21 | self.queued += r 22 | 23 | # Release from queue to downstream process 24 | r = int( round( random.uniform( 0, self.max_flow ) ) ) 25 | r = min( r, self.queued ) 26 | self.queued -= r 27 | 28 | return self.queued 29 | 30 | class Controller: 31 | def __init__( self, kp, ki ): 32 | self.kp, self.ki = kp, ki 33 | self.i = 0 # Cumulative error ("integral") 34 | 35 | def work( self, e ): 36 | self.i += e 37 | 38 | return self.kp*e + self.ki*self.i 39 | 40 | # ============================================================ 41 | 42 | def open_loop( p, tm=5000 ): 43 | def target( t ): 44 | return 5.0 # 5.1 45 | 46 | for t in range( tm ): 47 | u = target(t) 48 | y = p.work( u ) 49 | 50 | print t, u, 0, u, y 51 | 52 | def closed_loop( c, p, tm=5000 ): 53 | def setpoint( t ): 54 | if t < 100: return 0 55 | if t < 300: return 50 56 | return 10 57 | 58 | y = 0 59 | for t in range( tm ): 60 | r = setpoint(t) 61 | e = r - y 62 | u = c.work(e) 63 | y = p.work(u) 64 | 65 | print t, r, e, u, y 66 | 67 | # ============================================================ 68 | 69 | c = Controller( 1.25, 0.01 ) 70 | p = Buffer( 50, 10 ) 71 | 72 | # open_loop( p, 1000 ) 73 | closed_loop( c, p, 1000 ) 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /ch02.py: -------------------------------------------------------------------------------- 1 | 2 | import sys 3 | import math 4 | 5 | r = 0.6 6 | k = float( sys.argv[1] ) # 50..175 7 | 8 | print "r=%f\tk=%f\n" % ( r, k ); t=0 9 | print r, 0, 0, 0, 0 10 | 11 | def cache( size ): 12 | if size < 0: 13 | hitrate = 0 14 | elif size > 100: 15 | hitrate = 1 16 | else: 17 | hitrate = size/100.0 18 | return hitrate 19 | 20 | y, c = 0, 0 21 | for _ in range( 200 ): 22 | e = r - y # tracking error 23 | c += e # cumulative error 24 | u = k*c # control action 25 | y = cache(u) # process output 26 | 27 | print r, e, c, u, y 28 | -------------------------------------------------------------------------------- /ch03.py: -------------------------------------------------------------------------------- 1 | 2 | import sys 3 | 4 | r = float(sys.argv[1]) # Reference or "setpoint" 5 | k = float(sys.argv[2]) # Controller gain 6 | 7 | u = 0 # "Previous" output 8 | for _ in range( 200 ): 9 | y = u # One-step delay: previous output 10 | 11 | e = r - y # Tracking error 12 | u = k*e # Controller ouput 13 | 14 | print r, e, 0, u, y 15 | -------------------------------------------------------------------------------- /ch13-cache.py: -------------------------------------------------------------------------------- 1 | 2 | import random 3 | import feedback as fb 4 | 5 | class Cache( fb.Component ): 6 | def __init__( self, size, demand ): 7 | self.t = 0 # internal time counter, needed for last access time 8 | 9 | self.size = size # size limit of cache 10 | self.cache = {} # actual cache: cache[key] = last_accessed_time 11 | 12 | self.demand = demand # demand function 13 | 14 | def work( self, u ): 15 | self.t += 1 16 | 17 | self.size = max( 0, int(u) ) # non-negative integer 18 | 19 | i = self.demand( self.t ) # this is the "requested item" 20 | 21 | if i in self.cache: 22 | self.cache[i] = self.t # update last access time 23 | return 1 24 | 25 | if len(self.cache) >= self.size: # must make room 26 | m = 1 + len(self.cache) - self.size # number of elements to delete 27 | 28 | tmp = {} 29 | for k in self.cache.keys(): # key by last_access_time 30 | tmp[ self.cache[k] ] = k 31 | 32 | for t in sorted( tmp.keys() ): # delete the oldest elements 33 | del self.cache[ tmp[t] ] 34 | m -= 1 35 | if m == 0: 36 | break 37 | 38 | self.cache[i] = self.t # insert into cache 39 | return 0 40 | 41 | 42 | class SmoothedCache( Cache ): 43 | def __init__( self, size, demand, avg ): 44 | Cache.__init__( self, size, demand ); 45 | self.f = fb.FixedFilter( avg ) 46 | 47 | def work( self, u ): 48 | y = Cache.work( self, u ) 49 | return self.f.work(y) 50 | 51 | # ============================================================ 52 | 53 | def statictest(demand_width): 54 | def demand( t ): 55 | return int( random.gauss( 0, demand_width ) ) 56 | 57 | fb.static_test( SmoothedCache, (0, demand, 100), 150, 100, 5, 3000 ) 58 | 59 | 60 | def stepresponse(): 61 | def demand( t ): 62 | return int( random.gauss( 0, 15 ) ) 63 | 64 | def setpoint( t ): 65 | return 40 66 | 67 | p = SmoothedCache( 0, demand, 100 ) 68 | 69 | fb.step_response( setpoint, p ) 70 | 71 | 72 | def closedloop(): 73 | def demand( t ): 74 | return int( random.gauss( 0, 15 ) ) 75 | 76 | def setpoint( t ): 77 | if t > 5000: 78 | return 0.5 79 | return 0.7 80 | 81 | p = SmoothedCache( 0, demand, 100 ) 82 | c = fb.PidController( 100, 250 ) 83 | 84 | fb.closed_loop( setpoint, c, p, 10000 ) 85 | 86 | 87 | def closedloop_jumps(): 88 | def demand( t ): 89 | if t < 3000: 90 | return int( random.gauss( 0, 15 ) ) 91 | elif t < 5000: 92 | return int( random.gauss( 0, 35 ) ) 93 | else: 94 | return int( random.gauss( 100, 15 ) ) 95 | 96 | def setpoint( t ): 97 | return 0.7 98 | 99 | p = SmoothedCache( 0, demand, 100 ) 100 | c = fb.PidController( 270, 7.5 ) # Ziegler-Nichols - closedloop1 101 | # c = fb.PidController( 100, 4.3 ) # Cohen-Coon - 2 102 | # c = fb.PidController( 80, 2.0 ) # AMIGO - 3 103 | # c = fb.PidController( 150, 2 ) # 4 104 | 105 | fb.closed_loop( setpoint, c, p, 10000 ) 106 | 107 | 108 | # ============================================================ 109 | 110 | if __name__ == '__main__': 111 | 112 | fb.DT = 1 113 | 114 | # statictest(35) # 5, 15, 35 115 | 116 | # stepresponse() 117 | 118 | # closedloop() 119 | 120 | closedloop_jumps() 121 | -------------------------------------------------------------------------------- /ch14-adserving.py: -------------------------------------------------------------------------------- 1 | 2 | import math 3 | import random 4 | import feedback as fb 5 | 6 | class AdPublisher( fb.Component ): 7 | 8 | def __init__( self, scale, min_price, relative_width=0.1 ): 9 | self.scale = scale 10 | self.min = min_price 11 | self.width = relative_width 12 | 13 | def work( self, u ): 14 | if u <= self.min: # Price below min: no impressions 15 | return 0 16 | 17 | # "demand" is the number of impressions served per day 18 | # The demand is modeled (!) as Gaussian distribution with 19 | # a mean that depends logarithmically on the price u. 20 | 21 | mean = self.scale*math.log( u/self.min ) 22 | demand = int( random.gauss( mean, self.width*mean ) ) 23 | 24 | return max( 0, demand ) # Impression demand is greater than zero 25 | 26 | 27 | class AdPublisherWithWeekend( AdPublisher ): 28 | 29 | def __init__( self, weekday, weekend, min_price, relative_width=0.1 ): 30 | AdPublisher.__init__( self, None, min_price, relative_width ) 31 | 32 | self.weekday = weekday 33 | self.weekend = weekend 34 | 35 | self.t = 0 # Internal day counter 36 | 37 | def work( self, u ): 38 | self.t += 1 39 | 40 | if self.t%7 < 2: # Weekend 41 | self.scale = self.weekend 42 | else: 43 | self.scale = self.weekday 44 | 45 | return AdPublisher.work( self, u ) 46 | 47 | # ------------------------------------------------------------ 48 | 49 | def statictest(): 50 | fb.static_test( AdPublisher, (100,2), 20, 100, 10, 5000 ) 51 | 52 | 53 | def closedloop( kp, ki, f=fb.Identity() ): 54 | def setpoint( t ): 55 | if t > 1000: 56 | return 125 57 | return 100 58 | 59 | k = 1.0/20.0 60 | 61 | p = AdPublisher( 100, 2 ) 62 | c = fb.PidController( k*kp, k*ki ) 63 | 64 | fb.closed_loop( setpoint, c, p, returnfilter=f ) 65 | 66 | 67 | accumul_goal = 0 68 | def closedloop_accumul( kp, ki ): 69 | def setpoint( t ): 70 | global accumul_goal 71 | 72 | if t > 1000: 73 | accumul_goal += 125 74 | else: 75 | accumul_goal += 100 76 | return accumul_goal 77 | 78 | k = 1.0/20.0 79 | 80 | p = AdPublisher( 100, 2 ) 81 | c = fb.PidController( k*kp, k*ki ) 82 | 83 | fb.closed_loop( setpoint, c, p, returnfilter=fb.Integrator() ) 84 | 85 | 86 | def specialsteptest(): 87 | p = AdPublisher( 100, 2 ) 88 | f = fb.RecursiveFilter(0.05) 89 | 90 | for t in range( 500 ): 91 | r = 5.50 92 | u = r 93 | y = p.work( u ) 94 | z = f.work( y ) 95 | 96 | print t, t*fb.DT, r, 0, u, u, y, z, p.monitoring() 97 | 98 | quit() 99 | 100 | # ------------------------------------------------------------ 101 | 102 | if __name__ == '__main__': 103 | 104 | fb.DT = 1 105 | 106 | # statictest() 107 | 108 | # closedloop( 0.5, 0.25 ) # default 109 | # closedloop( 0.0, 0.25 ) # w/o prop ctrl 110 | # closedloop( 0.0, 1.75 ) # ringing 111 | 112 | # closedloop( 1.0, 0.125, fb.RecursiveFilter(0.125) ) # 113 | 114 | # closedloop_accumul( 0.5, 0.125 ) 115 | -------------------------------------------------------------------------------- /ch15-ch16-serverpool-and-queue.py: -------------------------------------------------------------------------------- 1 | 2 | import math 3 | import random 4 | import feedback as fb 5 | 6 | class AbstractServerPool( fb.Component ): 7 | def __init__( self, n, server, load ): 8 | self.n = n # number of server instances 9 | self.queue = 0 # number of items in queue 10 | 11 | self.server = server # server work function 12 | self.load = load # queue-loading work function 13 | 14 | 15 | def work( self, u ): 16 | self.n = max(0, int(round(u))) # server count: non-negative integer 17 | 18 | completed = 0 19 | for _ in range(self.n): 20 | completed += self.server() # each server does some amount of work 21 | 22 | if completed >= self.queue: 23 | completed = self.queue # "trim" completed to queue length 24 | break # stop if queue is empty 25 | 26 | self.queue -= completed # reduce queue by work completed 27 | 28 | return completed 29 | 30 | 31 | def monitoring( self ): 32 | return "%d %d" % ( self.n, self.queue ) 33 | 34 | 35 | class ServerPool( AbstractServerPool ): 36 | def work( self, u ): 37 | load = self.load() # additions to the queue 38 | self.queue = load # new load replaces old load 39 | 40 | if load == 0: return 1 # no work: 100 percent completion rate 41 | 42 | completed = AbstractServerPool.work( self, u ) 43 | 44 | return completed/load # completion rate 45 | 46 | 47 | class QueueingServerPool( AbstractServerPool ): 48 | def work( self, u ): 49 | load = self.load() # additions to the queue 50 | self.queue += load # new load is added to old load 51 | 52 | completed = AbstractServerPool.work( self, u ) 53 | 54 | return load - completed # net change in queue length 55 | 56 | 57 | class ServerPoolWithLatency( ServerPool ): 58 | def __init__( self, n, server, load, latency ): 59 | ServerPool.__init__( self, n, server, load ) 60 | 61 | self.latency = latency # time steps before server becomes active 62 | self.pending = [] # list of pending servers 63 | 64 | 65 | def work( self, u ): 66 | u = max(0, int(round(u))) # server count: non-negative integer 67 | 68 | if u <= self.n: # no increase in servers: no latency 69 | return ServerPool.work( self, u ) 70 | 71 | # for servers already pending: decrement waiting time 72 | for i in range( len(self.pending) ): 73 | self.pending[i] -= 1 74 | 75 | newly_active = self.pending.count( 0 ) # how many are done waiting? 76 | del self.pending[0:newly_active] # remove from pending... 77 | self.n += newly_active # ... and add to active 78 | 79 | # now add to list of pending servers if requested by input 80 | self.pending.extend( [self.latency]*int(u-self.n) ) 81 | 82 | return ServerPool.work( self, self.n ) 83 | 84 | 85 | # -------------------------------------------------- 86 | 87 | # Load and work functions (unless defined in local scope) 88 | 89 | def load_queue(): 90 | # This is only used by closedloop2() and closedloop3() 91 | 92 | global global_time 93 | global_time += 1 94 | 95 | if global_time > 2500: 96 | return random.gauss( 1200, 5 ) 97 | 98 | if global_time > 2200: 99 | return random.gauss( 800, 5 ) 100 | 101 | return random.gauss( 1000, 5 ) 102 | 103 | def consume_queue(): 104 | a, b = 20, 2 105 | return 100*random.betavariate( a, b ) # mean: a/(a+b); var: ~b/a^2 106 | 107 | # ============================================================ 108 | 109 | # Server Pool 110 | 111 | def statictest( traffic ): 112 | def loadqueue(): 113 | return random.gauss( traffic, traffic/200 ) 114 | 115 | fb.static_test( ServerPool, ( 0, consume_queue, loadqueue ), 116 | 20, 20, 5, 1000 ) # max u, steps, trials, timesteps 117 | 118 | def closedloop1(): 119 | # Closed loop, setpoint 0.6-0.8, PID Controller 120 | 121 | def loadqueue(): 122 | global global_time 123 | global_time += 1 124 | 125 | if global_time > 2100: 126 | return random.gauss( 1200, 5 ) 127 | 128 | return random.gauss( 1000, 5 ) 129 | 130 | 131 | def setpoint( t ): 132 | if t > 2000: 133 | return 0.6 134 | else: 135 | return 0.8 136 | 137 | 138 | p = ServerPool( 8, consume_queue, loadqueue ) 139 | c = fb.PidController( 1, 5 ) 140 | fb.closed_loop( setpoint, c, p, 10000 ) 141 | 142 | 143 | def closedloop2(): 144 | # Closed loop, setpoint 0.999x, Asymm (!) Controller 145 | 146 | def setpoint( t ): 147 | if t < 1000: # Switch on slowly, to avoid initial overshoot 148 | return t/1000.0 149 | return 0.9995 150 | 151 | class AsymmController( fb.PidController ): 152 | def work( self, e ): 153 | if e > 0: 154 | e /= 20.0 155 | 156 | self.i += fb.DT*e 157 | self.d = ( self.prev - e )/fb.DT 158 | self.prev = e 159 | 160 | return self.kp*e + self.ki*self.i + self.kd*self.d 161 | 162 | p = ServerPool( 0, consume_queue, load_queue ) 163 | c = AsymmController( 10, 200 ) 164 | fb.closed_loop( setpoint, c, p ) 165 | 166 | 167 | def closedloop3(): 168 | # Closed loop, setpoint 1.0, incremental controller (non-PID) 169 | 170 | def setpoint( t ): 171 | return 1.0 172 | 173 | class SpecialController( fb.Component ): 174 | def __init__( self, period1, period2 ): 175 | self.period1 = period1 176 | self.period2 = period2 177 | self.t = 0 178 | 179 | def work( self, u ): 180 | if u > 0: 181 | self.t = self.period1 182 | return +1 183 | 184 | self.t -= 1 # At this point: u <= 0 guaranteed! 185 | 186 | if self.t == 0: 187 | self.t = self.period2 188 | return -1 189 | 190 | return 0 191 | 192 | p = ServerPool( 0, consume_queue, load_queue ) 193 | c = SpecialController( 100, 10 ) 194 | fb.closed_loop( setpoint, c, p, actuator=fb.Integrator() ) 195 | 196 | # ============================================================ 197 | 198 | # Queue Control 199 | 200 | class InnerLoop( fb.Component ): 201 | def __init__( self, kp, ki, loader ): 202 | k = 1/100. 203 | 204 | self.c = fb.PidController( kp*k, ki*k ) 205 | self.p = QueueingServerPool( 0, consume_queue, loader ) 206 | 207 | self.y = 0 208 | 209 | def work( self, u ): 210 | e = u - self.y # u is setpoint from outer loop 211 | e = -e # inverted dynamics 212 | v = self.c.work( e ) 213 | self.y = self.p.work( v ) # y is net change 214 | return self.p.queue 215 | 216 | def monitoring( self ): 217 | return "%s %d" % ( self.p.monitoring(), self.y ) # servers, queue, diff 218 | 219 | 220 | def innerloop_steptest(): 221 | def loadqueue(): 222 | return 1000 223 | 224 | def setpoint( t ): 225 | if t < 1000 or t >= 1500: 226 | return 25 227 | else: 228 | return -25 229 | 230 | p = InnerLoop( 0.5, 0.25, loadqueue ) 231 | fb.step_response( setpoint, p, tm=2000 ) 232 | 233 | 234 | def nestedloops(): 235 | def setpoint( t ): 236 | return 200 237 | 238 | if t < 2000: 239 | return 100 240 | elif t < 3000: 241 | return 125 242 | else: 243 | return 25 244 | 245 | p = InnerLoop(0.5, 0.25, load_queue) # InnerLoop is "plant" for outer loop 246 | 247 | # c = fb.PidController( 0.06, 0.001 ) 248 | c = fb.AdvController( 0.35, 0.0025, 4.5, smooth=0.15 ) 249 | 250 | # fb.closed_loop( setpoint, c, p ) 251 | fb.closed_loop( setpoint, c, p, actuator=fb.RecursiveFilter(0.5) ) 252 | 253 | 254 | 255 | # ============================================================ 256 | 257 | if __name__ == '__main__': 258 | 259 | fb.DT = 1 260 | 261 | global_time = 0 # To communicate with queue_load functions 262 | 263 | # statictest( 1000 ) 264 | # closedloop1() 265 | # closedloop2() 266 | # closedloop3() 267 | 268 | # innerloop_steptest() 269 | 270 | nestedloops() 271 | -------------------------------------------------------------------------------- /ch17-fancontrol.py: -------------------------------------------------------------------------------- 1 | 2 | import random 3 | import feedback as fb 4 | 5 | class CpuWithCooler( fb.Component ): 6 | def __init__( self, jumps=False, drift=False ): 7 | self.ambient = 20 # ambient temperature (in Celsius) 8 | self.temp = self.ambient # initial state: temperature 9 | 10 | self.wattage = 75 # processor heat output in Joule per sec 11 | self.specific_heat = 1.0/50.0 # specific heat: degree per Joule 12 | 13 | self.loss_factor = 1.0/120.0 # per second 14 | 15 | self.load_wattage_factor = 10 # addtl watts due to load 16 | self.load_change_seconds = 50 # avg seconds between load changes 17 | self.current_load = 0 18 | 19 | self.ambient_drift = 1.0/3600 # degs per second: 10 degs per 10 hrs 20 | 21 | self.jumps = jumps # Are there jumps in processor load? 22 | self.drift = drift # Is there drift in ambient temp? 23 | 24 | 25 | def work( self, u ): 26 | u = max( 0, min( u, 10 ) ) # Actuator saturation 27 | 28 | self._ambient_drift() # Drift in ambient temp, if any 29 | self._load_changes() # Load changes, if any 30 | 31 | diff = self.temp - self.ambient # Heat loss depends on temp diff 32 | loss = self.loss_factor*( 1 + u ) # Natural heat loss + fan 33 | 34 | flow = self.wattage + self.current_load # Heat inflow to processor 35 | 36 | self.temp += fb.DT*( -loss*diff + self.specific_heat*flow ) 37 | return self.temp 38 | 39 | 40 | def _load_changes( self ): 41 | if self.jumps == False: return 42 | 43 | if random.randint( 0, 2*self.load_change_seconds/fb.DT ) == 0: 44 | self.current_load = self.load_wattage_factor*random.randint( 0, 5 ) 45 | 46 | def _ambient_drift( self ): 47 | if self.drift == False: return 48 | 49 | self.ambient += fb.DT*random.gauss( 0, self.ambient_drift ) 50 | self.ambient = max( 0, min( self.ambient, 40 ) ) # limit drift 51 | 52 | 53 | def monitoring( self ): 54 | return "%f" % ( self.current_load, ) 55 | 56 | # ============================================================ 57 | 58 | def no_fan(): 59 | def setpoint(t): return 0 60 | 61 | p = CpuWithCooler() 62 | fb.step_response( setpoint, p, 60000 ) 63 | 64 | 65 | def min_fan(): 66 | def setpoint(t): return 1 67 | 68 | p = CpuWithCooler() 69 | fb.step_response( setpoint, p, 60000 ) 70 | 71 | 72 | def measurement( s ): 73 | def setpoint(t): 74 | if t<5*60/fb.DT: return 1 75 | else: return s 76 | 77 | p = CpuWithCooler() 78 | fb.step_response( setpoint, p, 60000 ) 79 | 80 | 81 | def production(): 82 | def setpoint(t): 83 | if t*fb.DT < 6*60: return 50 84 | else: return 45 85 | # if t < 40000: return 50 86 | # else: return 45 87 | 88 | p = CpuWithCooler( True, True ); p.temp = 50 # Initial temp 89 | c = fb.AdvController( 2, 0.5, 0, clamp=(0,10) ) 90 | 91 | fb.closed_loop( setpoint, c, p, 100000, inverted=True, 92 | actuator=fb.Limiter( 0, 10 ) ) 93 | 94 | 95 | if __name__ == '__main__': 96 | 97 | fb.DT = 0.01 98 | 99 | # no_fan() 100 | # min_fan() 101 | 102 | # measurement( 5 ) # fan speed: 2, 3, 4, 5 103 | production() 104 | 105 | 106 | -------------------------------------------------------------------------------- /ch18-gameengine.py: -------------------------------------------------------------------------------- 1 | 2 | import math 3 | import random 4 | import feedback as fb 5 | 6 | class GameEngine( fb.Component ): 7 | def __init__( self ): 8 | self.n = 0 # Number of game objects 9 | self.t = 0 # Steps since last change 10 | 11 | self.resolutions = [ 100, 200, 400, 800, 1600 ] # memory per game obj 12 | 13 | def work( self, u ): 14 | self.t += 1 15 | 16 | if self.t > random.expovariate( 0.1 ): # 1 chg every 10 steps on avg 17 | self.t = 0 18 | self.n += random.choice( [-1,1] ) 19 | self.n = max( 1, min( self.n, 50 ) ) # 1 <= n <= 50 20 | 21 | crr = self.resolutions[u] # current resolution 22 | return crr*self.n # current memory consumption 23 | 24 | def monitoring( self ): 25 | return "%d" % (self.n,) 26 | 27 | 28 | class DeadzoneController( fb.Component ): 29 | def __init__( self, deadzone ): 30 | self.deadzone = deadzone 31 | 32 | def work( self, u ): 33 | if abs( u ) < self.deadzone: 34 | return 0 35 | 36 | if u < 0: return -1 37 | else: return 1 38 | 39 | 40 | class ConstrainingIntegrator( fb.Component ): 41 | def __init__( self ): 42 | self.state = 0 43 | 44 | def work( self, u ): 45 | self.state += u 46 | self.state = max(0, min( self.state, 4 ) ) # Constrain to 0..4 47 | return self.state 48 | 49 | 50 | class Logarithm( fb.Component ): 51 | def work( self, u ): 52 | if u <= 0: return 0 53 | return math.log(u) 54 | 55 | 56 | if __name__ == '__main__': 57 | 58 | fb.DT = 1 59 | 60 | def setpoint(t): 61 | return 3.5*math.log( 10.0 ) 62 | 63 | c = DeadzoneController( 0.5*math.log(8.0) ) 64 | p = GameEngine() 65 | 66 | fb.closed_loop( setpoint, c, p,actuator=ConstrainingIntegrator(), 67 | returnfilter=Logarithm() ) 68 | 69 | -------------------------------------------------------------------------------- /feedback.py: -------------------------------------------------------------------------------- 1 | 2 | import math 3 | import random 4 | 5 | DT = None # Sampling interval - defaults to None, must be set explicitly 6 | 7 | # ============================================================ 8 | # Components 9 | 10 | class Component: 11 | def work( self, u ): 12 | return u 13 | 14 | def monitoring( self ): 15 | return "" # Overload, to include addtl monitoring info in output 16 | 17 | # ============================================================ 18 | # Controllers 19 | 20 | # --- PID Controllers 21 | 22 | class PidController( Component ): 23 | def __init__( self, kp, ki, kd=0 ): 24 | self.kp, self.ki, self.kd = kp, ki, kd 25 | self.i = 0 26 | self.d = 0 27 | self.prev = 0 28 | 29 | def work( self, e ): 30 | self.i += DT*e 31 | self.d = ( e - self.prev )/DT 32 | self.prev = e 33 | 34 | return self.kp*e + self.ki*self.i + self.kd*self.d 35 | 36 | class AdvController( Component ): 37 | def __init__( self, kp, ki, kd=0, clamp=(-1e10,1e10), smooth=1 ): 38 | self.kp, self.ki, self.kd = kp, ki, kd 39 | self.i = 0 40 | self.d = 0 41 | self.prev = 0 42 | 43 | self.unclamped = True 44 | self.clamp_lo, self.clamp_hi = clamp 45 | 46 | self.alpha = smooth 47 | 48 | def work( self, e ): 49 | if self.unclamped: 50 | self.i += DT*e 51 | 52 | self.d = self.alpha*(e - self.prev)/DT + (1.0-self.alpha)*self.d 53 | 54 | u = self.kp*e + self.ki*self.i + self.kd*self.d 55 | 56 | self.unclamped = ( self.clamp_lo < u < self.clamp_hi ) 57 | self.prev = e 58 | 59 | return u 60 | 61 | # --- Relay and Band Controllers 62 | 63 | class DeadbandController( Component ): 64 | def __init__( self, zone ): 65 | self.zone = zone 66 | 67 | def work( self, e ): 68 | if e>self.zone: 69 | return e - self.zone 70 | elif e<-self.zone: 71 | return e + self.zone 72 | else: 73 | return 0 74 | 75 | class RelayController( Component ): 76 | def work( self, e ): 77 | if e == 0: 78 | return 0 79 | return e/abs(e) 80 | 81 | class DeadbandRelayController( Component ): 82 | def __init__( self, zone ): 83 | self.zone = zone 84 | 85 | def work( self, e ): 86 | if e>self.zone: 87 | return 1 88 | elif e<-self.zone: 89 | return -1 90 | else: 91 | return 0 92 | 93 | class HysteresisRelayController( Component ): 94 | def __init__( self, zone ): 95 | self.zone = zone 96 | self.prev = None 97 | 98 | def work( self, e ): 99 | 100 | if e > self.prev: # raising 101 | if e < self.zone: 102 | u = 0 103 | else: 104 | u = 1 105 | else: # falling 106 | if e > -self.zone: 107 | u = 0 108 | else: 109 | u = -1 110 | 111 | self.prev = e 112 | return u 113 | 114 | # ============================================================ 115 | # Simple Systems 116 | 117 | class Boiler( Component ): 118 | # Default g: temp drops to 1/e in 100 secs (for water: approx 1 deg/sec) 119 | # Work u: input is change in temp (per sec), if no heat loss 120 | 121 | def __init__( self, g=0.01 ): 122 | self.y = 0 # initial state, "temperature" (above ambient) 123 | self.g = g # constant of proportionality (time constant) 124 | 125 | def work( self, u ): 126 | self.y += DT*( -self.g*self.y + u ) 127 | return self.y 128 | 129 | class Spring( Component ): 130 | # In mks units (defaults: 100g, 1N/m, approx 10 periods to fall to 1/e) 131 | 132 | def __init__( self, m=0.1, k=1, g=0.05 ): 133 | self.x = 0 # position 134 | self.v = 0 # velocity 135 | 136 | self.m = m # mass 137 | self.k = k # spring constant: Newton/meter 138 | self.g = g # damping factor 139 | 140 | def work( self, u ): 141 | a = ( - self.k*self.x - self.g*self.v + u )/self.m 142 | self.v += DT*a 143 | self.x += DT*self.v 144 | return self.x 145 | 146 | # ============================================================ 147 | # Filters and Actuators 148 | 149 | class Identity( Component ): 150 | def work( self, x ): return x 151 | 152 | class Limiter( Component ): 153 | def __init__( self, lo, hi ): 154 | self.lo = lo 155 | self.hi = hi 156 | 157 | def work( self, x ): 158 | return max( self.lo, min( x, self.hi ) ) 159 | 160 | class Discretizer( Component ): 161 | def __init__( self, binwidth ): 162 | self.binwidth = binwidth 163 | 164 | def work( self, u ): 165 | return self.binwidth*int( u/self.binwidth ) 166 | 167 | class Hysteresis( Component ): 168 | def __init__( self, threshold ): 169 | self.threshold = threshold 170 | self.prev = 0 171 | 172 | def work( self, u ): 173 | y = self.prev 174 | 175 | if abs(u - self.prev) > self.threshold: 176 | y = u 177 | self.prev = u 178 | 179 | return y 180 | 181 | class Integrator( Component ): 182 | def __init__( self ): 183 | self.data = 0 184 | 185 | def work( self, u ): 186 | self.data += u 187 | return DT*self.data 188 | 189 | class FixedFilter( Component ): 190 | def __init__( self, n ): 191 | self.n = n 192 | self.data = [] 193 | 194 | def work( self, x ): 195 | self.data.append(x) 196 | 197 | if len(self.data) > self.n: 198 | self.data.pop(0) 199 | 200 | return float(sum(self.data))/len(self.data) 201 | 202 | class RecursiveFilter( Component ): 203 | def __init__( self, alpha ): 204 | self.alpha = alpha 205 | self.y = 0 206 | 207 | def work( self, x ): 208 | self.y = self.alpha*x + (1-self.alpha)*self.y 209 | return self.y 210 | 211 | # ============================================================ 212 | # Setpoints 213 | 214 | def impulse( t, t0 ): 215 | if abs(t-t0) < DT: return 1 # Floating point or integer time? 216 | return 0 217 | 218 | def step( t, t0 ): 219 | if t >= t0: return 1 220 | return 0 221 | 222 | def double_step( t, t0, t1 ): 223 | if t>=t0 and t= t0 and (t-t0)%tp == 0: return 1 228 | # return 0 229 | 230 | def harmonic( t, t0, tp ): 231 | if t>=t0: return math.sin(2*math.pi*(t-t0)/tp) 232 | return 0 233 | 234 | def relay( t, t0, tp ): 235 | if t>=t0: 236 | if math.ceil(math.sin(2*math.pi*(t-t0)/tp)) > 0: 237 | return 1 238 | else: 239 | return 0 240 | return 0 241 | 242 | # ============================================================ 243 | # Loop functions 244 | 245 | # def setpoint( t ): 246 | # return step( t, 0 ) 247 | 248 | def static_test( plant_ctor, ctor_args, umax, steps, repeats, tmax ): 249 | # Complete test for static process characteristic 250 | # From u=0 to umax taking steps steps, each one repeated repeats 251 | 252 | for i in range( 0, steps ): 253 | u = float(i)*umax/float(steps) 254 | 255 | for r in range( repeats ): 256 | p = apply( plant_ctor, ctor_args ) # this is: p = Plant( a, b, c ) 257 | 258 | for t in range( tmax ): 259 | y = p.work(u) 260 | 261 | print u, y 262 | 263 | quit() 264 | 265 | def step_response( setpoint, plant, tm=5000 ): 266 | for t in range( tm ): 267 | r = setpoint(t) # This is the plant input, not really the setpoint! 268 | u = r 269 | y = plant.work( u ) 270 | 271 | print t, t*DT, r, 0, u, u, y, y, plant.monitoring() 272 | 273 | quit() 274 | 275 | def open_loop( setpoint, controller, plant, tm=5000 ): 276 | for t in range( tm ): 277 | r = setpoint(t) # This is the controller input, not really the setpt! 278 | u = controller.work( r ) 279 | y = plant.work( u ) 280 | 281 | print t, t*DT, r, 0, u, u, y, y, plant.monitoring() 282 | 283 | quit() 284 | 285 | def closed_loop( setpoint, controller, plant, tm=5000, inverted=False, 286 | actuator=Identity(), returnfilter=Identity() ): 287 | z = 0 288 | for t in range( tm ): 289 | r = setpoint(t) 290 | e = r - z 291 | if inverted == True: e = -e 292 | u = controller.work(e) 293 | v = actuator.work(u) 294 | y = plant.work(v) 295 | z = returnfilter.work(y) 296 | 297 | print t, t*DT, r, e, u, v, y, z, plant.monitoring() 298 | 299 | quit() 300 | 301 | # ============================================================ 302 | 303 | if __name__ == '__main__': 304 | 305 | def setpoint( t ): 306 | return 10*double_step( t, 1000, 6000 ) 307 | 308 | p = Boiler() 309 | c = PidController( 0.45, 0.01 ) 310 | 311 | closed_loop( setpoint, c, p, 15000 ) 312 | -------------------------------------------------------------------------------- /janert-feedbackcontrol.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oreillymedia/feedback_control_for_computer_systems/877e96053dff96fcd565230efbe16aced4137e08/janert-feedbackcontrol.zip --------------------------------------------------------------------------------