5 log_dir = "/home/wking/rsrch/data/temperature"
7 class error (Exception) :
8 "Errors with the temperature controller"
11 class errorMmelcor (error) :
13 class errorOutOfRange (error) :
16 def _check1(functionCall) :
17 (err, val) = functionCall
22 def _check0(functionCall) :
28 def melcor2double(value) :
29 (err, doub) = Melcor.melcor2double(value)
31 raise errorMelcor, "Error converting melcor to double"
33 def double2melcor(doub) :
34 (err, val) = Melcor.double2melcor(doub)
36 raise errorMelcor, "Error converting double to melcor"
39 def check_range(raw_output, min, max) :
41 raise errorOutOfRange, '%g < %g' % (raw_output, min)
43 raise errorOutOfRange, '%g > %g' % (raw_output, max)
45 class tempController :
46 "Pretty wrappers for controlling a Melcor MTCA Temperature Controller"
47 def __init__(self, controller=1, device='/dev/ttyS0', maxCurrent=0.2) :
49 (controller, device, maxCurrent) -> (tempController instance)
50 controller : MTCA controller Id
51 device : serial port you're using to connect to the controller
52 maxCurrent : initial maximum allowed current (in Amps)
53 Set maxCurrent = None if you don't want to adjust from it's prev. value.
55 0.2 A is the default max current since it seems ok to use without fluid
56 cooled heatsink. If you are cooling the heatsink, use 1.0 A, which seems
57 safely below the peltier's 1.2 A limit.
60 self.setpoint = 20.0 # degrees C
61 self.Tmin = 5.0 # setup some protective bounds for sanity checks
63 self.specMaxCur = 4.0 # Amps, the rated max current from controller specs
64 self.T = Melcor.tempController(controller, device)
65 if maxCurrent != None : # if None, just leave maxCurrent at it's prev. val.
66 self.setMaxCurrent(maxCurrent) # Amps
68 "Returns the current process temperature in degrees Celsius"
69 val = self.read(Melcor.REG_HIGH_RESOLUTION)
72 def getAmbientTemp(self) :
73 "Returns room temperature in degrees Celsius"
74 val = self.read(Melcor.REG_AMBIENT_TEMPERATURE)
75 # convert (Fahrenheit*10) to Celsius
76 return (val/10.0 - 32)/1.8
77 def setSetpoint(self, setpoint) :
78 "Set the temperature setpoint in degrees Celsius"
79 val = double2melcor(setpoint)
80 self.write(Melcor.REG_SET_POINT_1, val)
81 def getSetpoint(self) :
82 "Get the temperature setpoint in degrees Celsius"
83 val = self.read(Melcor.REG_SET_POINT_1)
84 return melcor2double(val)
85 def setMaxCurrent(self, maxCur) :
87 Set the max current in Amps.
88 (Note to Melcor enthusiasts: set's both the 'above' and 'below' limits)
90 maxPercent = maxCur / self.specMaxCur * 100
91 val = double2melcor(maxPercent)
92 self.write(Melcor.REG_HIGH_POWER_LIMIT_ABOVE, val)
93 self.write(Melcor.REG_HIGH_POWER_LIMIT_BELOW, val)
94 self.maxCurrent = maxCur
95 def getMaxCurrent(self) :
97 () -> (currentLimitAbove (A), currentLimitBelow (A), currentLimitSetpoint (deg C))
99 per = self.read(Melcor.REG_HIGH_POWER_LIMIT_ABOVE)
100 curLimAbove = melcor2double(per)/100.0 * self.specMaxCur
101 per = self.read(Melcor.REG_HIGH_POWER_LIMIT_BELOW)
102 curLimBelow = melcor2double(per)/100.0 * self.specMaxCur
103 val = self.read(Melcor.REG_POWER_LIMIT_SETPOINT)
104 curLimSet = melcor2double(val)
105 return (curLimAbove, curLimBelow, curLimSet)
106 def getPercentCurrent(self) :
108 Returns the percent of rated max current being output.
111 val = self.read(Melcor.REG_PERCENT_OUTPUT)
113 def getCurrent(self) :
115 The returned current is not the actual current,
116 but the current that the temperature controller
117 calculates it should generate.
118 If the voltage required to generate that current
119 exceeds the controllers max voltage (15V on mine),
120 then the physical current will be less than the
123 percentOutput = self.getPercentCurrent()
124 return self.specMaxCur * percentOutput / 100
125 def setCoolingGains(self, propband=0.1, integral=0, derivative=0) :
127 (propband, integral, derivative, dead_band) -> None
128 propband : propotional gain band in degrees C
129 integral : integral weight in minutes (0.00 to 99.99)
130 derivative : derivative weight in minutes (? to ?)
131 See 5.10 and the pages afterwards in the manual for Melcor's explaination.
132 Formula (from Cornell BioPhys El Producto Beamline notes)
133 P_cout = -1/T_prop * [ (T_samp - T_set)
134 + 1/t_int * int_-inf^t (T_samp(t')-T_set(t')) dt'
135 + t_deriv * dT_samp/dt
136 Where P_cout is the percent of the rated max current that the controller
137 would like to output if you weren't limiting it,
138 T_prop is the propband input to this function,
139 T_samp is the measured temperature of the sample in deg C,
140 T_set is the setpoint in deg C,
141 t_int is the integral input to this function,
142 the integral with respect to t' is actually only from the time that
143 T_samp has been with T_prop of T_set (not -inf), and
144 t_deriv is the derivative input to this function.
148 check_range(propband, 0, 99.9)
149 check_range(integral, 0, 99.99)
150 check_range(derivative, 0, 99.99)
152 val = double2melcor(propband)
153 self.write(Melcor.REG_PROPBAND_1, val)
154 val = int(integral * 100)
155 self.write(Melcor.REG_INTEGRAL_1, val)
156 val = int(derivative * 100)
157 self.write(Melcor.REG_DERIVATIVE_1, val)
158 def getCoolingGains(self) :
159 "() -> (propband, integral, derivative)"
160 val = self.read(Melcor.REG_PROPBAND_1)
161 propband = melcor2double(val)
162 val = self.read(Melcor.REG_INTEGRAL_1)
164 val = self.read(Melcor.REG_DERIVATIVE_1)
165 derivative = val/100.0
166 return (propband, integral, derivative)
167 def setHeatingGains(self, propband=0.1, integral=0, derivative=0) :
169 (propband, integral, derivative, dead_band) -> None
170 propband : propotional gain band in degrees C
171 integral : integral weight in minutes (0.00 to 99.99)
172 derivative : derivative weight in minutes (? to ?)
173 Don't use derivative, dead time.
177 See 5.10 and the pages afterwards in the manual for Melcor's explaination.
178 Formula (from Cornell BioPhys El Producto Beamline notes)
179 P_cout = -1/T_prop * [ (T_samp - T_set)
180 + 1/t_int * int_-inf^t (T_samp(t')-T_set(t')) dt'
181 + t_deriv * dT_samp/dt
182 Where P_cout is the percent of the rated max current that the controller
183 would like to output if you weren't limiting it,
184 T_prop is the propband input to this function,
185 T_samp is the measured temperature of the sample in deg C,
186 T_set is the setpoint in deg C,
187 t_int is the integral input to this function,
188 the integral with respect to t' is actually only from the time that
189 T_samp has been with T_prop of T_set (not -inf), and
190 t_deriv is the derivative input to this function.
194 check_range(propband, 0, 99.9)
195 check_range(integral, 0, 99.99)
196 check_range(derivative, 0, 99.99)
198 val = double2melcor(propband)
199 self.write(Melcor.REG_PROPBAND_2, val)
200 val = int(integral * 100)
201 self.write(Melcor.REG_INTEGRAL_2, val)
202 val = int(derivative * 100)
203 self.write(Melcor.REG_DERIVATIVE_2, val)
204 def getHeatingGains(self) :
205 "() -> (propband, integral, derivative)"
206 val = self.read(Melcor.REG_PROPBAND_2)
207 propband = melcor2double(val)
208 val = self.read(Melcor.REG_INTEGRAL_2)
210 val = self.read(Melcor.REG_DERIVATIVE_2)
211 derivative = val/100.0
212 return (propband, integral, derivative)
213 def getFeedbackTerms(self) :
217 pid = self.read(Melcor.REG_PID_POWER_1)
218 prop = self.read(Melcor.REG_PROP_TERM_1)
219 ntgrl = self.read(Melcor.REG_INTEGRAL_TERM_1)
220 deriv = self.read(Melcor.REG_DERIVATIVE_TERM_1)
221 pout = self.getPercentCurrent()
222 temp = self.getTemp()
223 tset = self.getSetpoint()
224 print "pid %g =? sum %g =? cur %g" % (pid, prop+ntgrl+deriv, pout)
225 print "meas: prop %d, integral %d, deriv %d" % (prop, ntgrl, deriv)
226 print "my calcs: prop %d" % (temp-tset)
227 def setTemp(self, setpoint, tolerance=0.3, time=10.0) :
229 Changes setpoint to SETPOINT and waits for stability
231 self.setSetpoint(setpoint)
232 while self.isStable(setpoint, tolerance, time) != True :
234 def setTemp_funkygain(self, setpoint, dead_time, heat_rate, cool_rate,
235 peltier_efficiency_fn, outside_equilib_rate,
236 tolerance=0.3, time=10.0) :
238 Highly experimental, see diffusion.py
243 print "full steam ahead"
244 self.setSetpoint(setpoint)
245 self.setHeatingGains(0.1, 0, 0)
246 self.setCoolingGains(0.1, 0, 0)
249 self._heat_until_close(setpoint, dead_time, heat_rate)
252 self._cool_until_close(setpoint, dead_time, cool_rate)
254 print "coast while temperature equilibrates"
255 self.setHeatingGains(100, 0, 0)
256 self.setCoolingGains(100, 0, 0)
257 time.sleep(dead_time*2)
258 cool_prop, heat_prop = self.calcPropBands()
259 print "calculated prop bands: c %g, h %g deg C" % (cool_prop, heat_prop)
260 print "reset integral gain, and bump to predicted props"
261 # pop down to reset integral gain, could also jump setpoint...
262 self.setHeatingGains(0.1, 0, 0)
263 self.setHeatingGains(heat_prop, 0, 0)
264 self.setCoolingGains(0.1, 0, 0)
265 self.setCoolingGains(cool_prop, 0, 0)
266 time.sleep(dead_time*4)
267 # now add in some integral to reduce droop
268 print "set integral gains to %g" % (dead_time*4)
269 self.setHeatingGains(heat_prop, dead_time*4, 0)
270 self.setCoolingGains(cool_prop, dead_time*4, 0)
271 time.sleep(dead_time*8)
272 print "wait to enter tolerance band"
273 while (self.getTemp()-setpoint) :
274 time.sleep(dead_time)
275 print "should be stable now"
276 if not self.isStable(setpoint, tolerance, time) :
277 raise error, "Algorithm broken ;)"
278 def _heat_until_close(self, setpoint, dead_time, heat_rate) :
279 while self.getTemp() < setpoint - 0.5*rate*dead_time :
280 time.sleep(dead_time/10.0)
281 def calcPropBands(setpoint, peltier_efficiency_fn, outside_equilib_rate) :
282 heat_loss = outside_equilib_rate * (setpoint - self.getAmbientTemp())
283 required_current = heat_loss / peltier_efficiency_fn(setpoint)
284 if required_current > self.maxCurrent :
285 raise errorOutOfRange, "Can't source %g Amps", required_current
286 fraction_current = required_current / self.maxCurrent
287 droop = 0.5 # expected droop in deg C on only proporitional gain
288 # droop / T_prop = fraction current
289 T_prop = droop / fraction_current
290 if setpoint > self.getAmbientTemp()+5 : # heating
291 return (T_prop*10, T_prop)
292 elif setpoint < self.getAmbientTemp()+5 : # cooling
293 return (T_prop, T_prop*10)
294 else : # right about room temperature
295 return (T_prop, T_prop)
296 def isStable(self, setpoint, tolerance=0.3, maxTime=10.0) :
298 Counts how long the temperature stays within
299 TOLERANCE of SETPOINT.
300 Returns when temp goes bad, or MAXTIME elapses.
303 startTime = time.time()
305 while abs(self.getTemp() - setpoint) < tolerance :
306 stopTime = time.time()
307 if (stopTime-startTime) > maxTime :
308 print "Stable for long enough"
310 if stopTime-startTime > maxTime :
314 def setFilterTime(self, seconds) :
316 Positive values to affect only monitored values.
317 Negative values affect both monitored and control values.
319 decSeconds = int(seconds*10)
320 if decSeconds < 0 : # convert (unsigned int) -> (2's compliment signed)
322 self.write(Melcor.REG_INPUT_SOFTWARE_FILTER_1, decSeconds)
323 def getFilterTime(self) :
325 Positive values to affect only monitored values.
326 Negative values affect both monitored and control values.
328 val = self.read(Melcor.REG_INPUT_SOFTWARE_FILTER_1)
329 if val >= 2**15 : # convert (2's complement signed) -> (unsigned int)
332 def sanityCheck(self) :
333 "Check that some key registers have the values we expect"
334 _sanityCheck(Melcor.REG_UNITS_TYPE, 2) # SI
335 _sanityCheck(Melcor.REG_C_OR_F, 1) # C
336 _sanityCheck(Melcor.REG_FAILURE_MODE, 2) # off
337 _sanityCheck(Melcor.REG_RAMPING_MODE, 0) # off
338 _sanityCheck(Melcor.REG_OUTPUT_1, 1) # cool
339 _sanityCheck(Melcor.REG_OUTPUT_2, 1) # heat
340 def _sanityCheck(self, register, expected_value) :
341 val = self.read(register)
342 if val != expected_value :
343 print "Register %d, expected %d, was %d" % (register,
346 raise error, "Controller settings error"
347 def read(self, register) :
349 (register) -> (value)
350 Returns the value of the specified memory register on the controller.
351 Registers are defined in the Melcor module.
352 See melcor_registers.h for a pointers on meanings and manual page nums.
354 (err, val) = self.T.read(register)
358 def write(self, register, value) :
360 (register, value) -> None
361 Sets the value of the specified memory register on the controller.
362 Registers are defined in the Melcor module.
363 See melcor_registers.h for a pointers on meanings and manual page nums.
365 err = self.T.write(register, value)
368 def getDeadtimeData(self, num_oscillations=10, curHysteresis=0.8, log=True) :
369 orig_heat_gains = self.getHeatingGains()
370 orig_cool_gains = self.getCoolingGains()
372 print "Measuring dead time"
373 print " go to bang-bang"
374 self.setHeatingGains(0.1, 0, 0)
375 self.setCoolingGains(0.1, 0, 0)
377 if cur > curHysteresis :
379 elif cur < -curHysteresis :
385 temp = self.getTemp()
386 cur = self.getCurrent()
387 heat_first = isHeating(cur)
388 start_time = time.time()
391 print " Wait to exit hysteresis region"
392 while heat_first == None and tm < 30:
395 heat_first = isHeating(temp, cur)
396 tm = time.time()-start_time
398 raise error, "after 30 seconds, still inside hysteresis region"
400 print " Read oscillations"
402 start_time = time.time()
406 print "Temp %g\t(%g),\tCur %g,\tTime %d" % (temp, temp-Tset, cur, 0)
407 while i < numOscillations*2 :
409 tm = time.time()-start_time
414 check_signs(temp,cur)
415 if heating == True and isHeating(temp, cur) == False :
416 print "Transition to cooling (i=%d)" % i
419 elif heating == False and isHeating(temp, cur) == True :
420 print "Transition to heating (i=%d)" % i
424 print " Restoring gains"
425 self.setHeatingGains(*orig_heat_gains)
426 self.setCoolingGains(*orig_cool_gains)
430 def _test_tempController() :
431 t = tempController(controller=1, maxCurrent=0.1)
433 print "Temp = %g" % t.getTemp()
434 print "Current = %g" % t.getCurrent()
435 print "Setpoint = %g" % t.getSetpoint()
437 print "Setting setpoint to 5.0 deg C"
440 print "Setpoint = %g" % sp
442 raise Exception, "Setpoint in %g != setpoint out %g" % (sp, 5.0)
443 time.sleep(10) # give the controller some time to overcome any integral gain
445 print "Current = %g" % c
446 mca, mcb, mct = t.getMaxCurrent()
447 if t.getTemp() < mct : # we're below the high power limit setpoint, use mcb
449 raise Exception, "Current not at max %g, and we're shooting for a big temp" % mcb
452 raise Exception, "Current not at max %g, and we're shooting for a big temp" % mca
455 print "Setting setpoint to 50.0 deg C"
458 print "Setpoint = %g" % sp
460 raise Exception, "Setpoint in %g != setpoint out %g" % (sp, 5.0)
463 print "Current = %g" % c
465 mca, mcb, mct = t.getMaxCurrent()
466 if t.getTemp() < mct : # we're below the high power limit setpoint, use mcb
468 raise Exception, "Current not at min %g, and we're shooting for a big temp" % (-mcb)
471 raise Exception, "Current not at min %g, and we're shooting for a big temp" % (-mca)
474 _test_tempController()
476 if __name__ == "__main__" :