def __heatLink(self, coolertemp, sampletemp): """heatflow from sample to cooler. may be negative...""" flow = (sampletemp - coolertemp) * \ ((coolertemp + sampletemp) ** 2)/400. cp = clamp(self.__coolerCP(coolertemp) * self.__sampleCP(sampletemp), 1, 10) return clamp(flow, -cp, cp)
def calculate(lam, spread, reso, shade, l, dtau, n_max): """Calculate chopper frequency and opening angle for a single setting, which needs: * lam: incoming neutron wavelength in Angstrom * spread: incoming neutron wavelength spread from selector * reso: desired wavelength resolution * shade: shade of spectrum edges * l: chopper-detector length in m * dtau: additional offset of TOF in ms * n_max: maximum number of acquisition frames Returns: (frequency in Hz, opening angle in deg) """ dlam = 2.0 * spread * lam tau_tot = l * lam * (1.0 - spread) / SPEED n = max(2, int(2 * spread / reso + 1)) n_fac = (1. - 2 * shade) / (1. - shade) n_fac = clamp(n_fac, 1.0 / n, 1.0) n_illum = clamp(int(n_fac * n), 1, n_max) tau_delta = l * dlam / ((n - 1) * SPEED) tau_win = 0.5 * tau_delta * (n + n_illum) tau0 = tau_tot + 0.5 * (n - n_illum) * tau_delta + dtau tau0 -= tau_win * int(tau0 / tau_win) freq = 1000.0 / tau_win / 2.0 angle = 180.0 / (0.5 * (n + n_illum)) return freq, angle
def doWriteLimits(self, limits): lower, upper = limits upper = clamp(upper, 1e-3, self.parallel_pumping) lower = clamp(lower, 1e-3, upper) self._attached_iodev._HW_rawCommand('set_%s_low' % self.chamber, int(lower * 1e6)) self._attached_iodev._HW_rawCommand('set_%s_high' % self.chamber, int(upper * 1e6)) return (lower, upper)
def doReadAbslimits(self): maxcurr = self._attached_currentsource.abslimits[1] maxfield = self._current2field(maxcurr) # get configured limits if any, or take max of source limits = self._config.get('abslimits', (-maxfield, maxfield)) # in any way, clamp limits to what the source can handle limits = [clamp(i, -maxfield, maxfield) for i in limits] return min(limits), max(limits)
def doWriteRamp(self, value): # this works only for the floatrange type of the ramp parameter! rampmin = self.parameters['ramp'].type.fr rampmax = self.parameters['ramp'].type.to if value == 0.0: value = rampmax self.log.warning( 'Ramp rate of 0 is deprecated, using %d ' 'K/min instead', value) self.__set_param('ramp', clamp(value, rampmin, rampmax)) return self.__get_param('ramp')
def doReadAbslimits(self): minfield, maxfield = [ self._current2field(I) for I in self._attached_currentsource.abslimits ] # include 0 in allowed range if minfield > 0: minfield = 0 if maxfield < 0: maxfield = 0 # get configured limits if any, or take max from source limits = self._config.get('abslimits', (minfield, maxfield)) # in any way, clamp limits to what the source can handle limits = [clamp(i, minfield, maxfield) for i in limits] return min(limits), max(limits)
def doReadRamp(self): # do not return a value the validator would reject, or # device creation fails ramp = self.__get_param('ramp') # this works only for the floatrange type of the ramp parameter! rampmin = self.parameters['ramp'].type.fr rampmax = self.parameters['ramp'].type.to if rampmin <= ramp <= rampmax: return ramp clampramp = clamp(ramp, rampmin, rampmax) self.log.warning( 'Ramp parameter %.3g is outside of the allowed range ' '%.3g..%.3g, setting it to %.3g', ramp, rampmin, rampmax, clampramp) # clamp read value to allowed range and re-set it return self.doWriteRamp(clampramp)
def __init__(self, dev, target): # limit the position to allowed values target = clamp(target, dev.usermin, dev.usermax) SeqDev.__init__(self, dev, target)
def __moving(self): # complex thread handling: # a) simulation of cryo (heat flow, thermal masses,....) # b) optional PID temperature controller with windup control # c) generating status+updated value+ramp # this thread is not supposed to exit! # local state keeping: regulation = self.regulation sample = self.sample timestamp = time.time() heater = 0 lastflow = 0 last_heaters = (0, 0) delta = 0 I = D = 0 lastD = 0 damper = 1 lastmode = self.mode while not self._stopflag: t = time.time() h = t - timestamp if h < self.loopdelay / damper: time.sleep(clamp(self.loopdelay / damper - h, 0.1, 60)) continue h *= self.speedup # a) sample = self.sample regulation = self.regulation heater = self.heater heatflow = self.__heatLink(regulation, sample) self.log.debug('sample = %.5f, regulation = %.5f, heatflow = %.5g', sample, regulation, heatflow) newsample = max(0, sample + (self.__sampleLeak(sample) - heatflow) / self.__sampleCP(sample) * h) # avoid instabilities due to too small CP newsample = clamp(newsample, sample, regulation) regdelta = (heater * 0.01 * self.maxpower + heatflow - self.__coolerPower(regulation)) newregulation = max(0, regulation + regdelta / self.__coolerCP(regulation) * h) # b) see # http://brettbeauregard.com/blog/2011/04/improving-the-beginners-pid-introduction/ if self.mode != 'openloop': # fix artefacts due to too big timesteps # actually i would prefer reducing loopdelay, but i have no # good idea on when to increase it back again if heatflow * lastflow != -100: if (newregulation - newsample) * (regulation - sample) < 0: # newregulation = (newregulation + regulation) / 2 # newsample = (newsample + sample) / 2 damper += 1 lastflow = heatflow error = self.setpoint - newregulation # use a simple filter to smooth delta a little delta = (delta + regulation - newregulation) / 2. kp = self.p / 10. # LakeShore P = 10*k_p ki = kp * abs(self.i) / 500. # LakeShore I = 500/T_i kd = kp * abs(self.d) / 2. # LakeShore D = 2*T_d P = kp * error I += ki * error * h D = kd * delta / h # avoid reset windup I = clamp(I, 0., 100.) # I is in % # avoid jumping heaterpower if switching back to pid mode if lastmode != self.mode: # adjust some values upon switching back on I = self.heater - P - D v = P + I + D # in damping mode, use a weighted sum of old + new heaterpower if damper > 1: v = ((damper ** 2 - 1) * self.heater + v) / damper ** 2 # damp oscillations due to D switching signs if D * lastD < -0.2: v = (v + heater) / 2. # clamp new heater power to 0..100% heater = clamp(v, 0., 100.) lastD = D self.log.debug('PID: P = %.2f, I = %.2f, D = %.2f, ' 'heater = %.2f', P, I, D, heater) # check for turn-around points to detect oscillations -> # increase damper x, y = last_heaters if (x + 0.1 < y and y > heater + 0.1) or \ (x > y + 0.1 and y + 0.1 < heater): damper += 1 last_heaters = (y, heater) else: # self.heaterpower is set manually, not by pid heater = self.heater last_heaters = (0, 0) heater = round(heater, 3) sample = newsample regulation = newregulation lastmode = self.mode # c) if self.setpoint != self.target: if self.ramp == 0: maxdelta = 10000 else: maxdelta = self.ramp / 60. * h try: self.setpoint = round(self.setpoint + clamp(self.target - self.setpoint, -maxdelta, maxdelta), 3) self.log.debug('setpoint changes to %r (target %r)', self.setpoint, self.target) except (TypeError, ValueError): # self.target might be None pass # keep max self.window seconds long history self._cacheCB('value', regulation, t) # temperature is stable when all recorded values in the window # differ from setpoint by less than tolerance with self._statusLock: if self.setpoint == self.target: self._setROParam('curstatus', (status.OK, '')) damper -= (damper - 1) / 10. # max value for damper is 11 else: self._setROParam('curstatus', (status.BUSY, 'ramping setpoint')) damper -= (damper - 1) / 20. self._setROParam('regulation', round(regulation, 3)) self._setROParam('sample', round(sample, 3)) self._setROParam('heaterpower', round(heater * self.maxpower * 0.01, 3)) self._setROParam('heater', heater) timestamp = t
def __coolerPower(self, temp): """returns cooling power in W at given temperature""" # quadratic up to 42K, is linear from 40W@42K to 100W@600K # return clamp((temp-2)**2 / 32., 0., 40.) + temp * 0.1 return clamp(15 * atan(temp * 0.01) ** 3, 0., 40.) + temp * 0.1 - 0.2
def doWriteMaxpower(self, newpower): self.heater = clamp(self.heater * self.maxpower / float(newpower), 0, 100)